From ff285e9af3926057295c4c95a827db9236ca8bc2 Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 20 Jul 2026 16:14:16 +0200 Subject: [PATCH] Verlauf-Grafik aus Sim-Schema (mehrere Linien) + Ampelbalken in allen Spalten - renderRunsLineChart nutzt wieder SIM_METRICS statt highlights -> deckt auch Module ohne Highlight-Def ab (weltkueche/fluss zeigten sonst nur Sterne); Snapshot bei einem Durchgang via renderSimMetrics-Kacheln - Modul-Tabelle: Ampelbalken auch fuer Durchgaenge, Spielzeit, Zuletzt (mehr/aktueller = gruen) -> alle Spalten konsistent grafisch Co-Authored-By: Claude Opus 4.8 --- App/teacher.html | 45 ++++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/App/teacher.html b/App/teacher.html index 0104cc9..34c543b 100644 --- a/App/teacher.html +++ b/App/teacher.html @@ -3579,6 +3579,15 @@ function renderModDetail() { var frac = (+v - st.min) / (st.max - st.min); return h.agg === 'min' ? (1 - frac) : frac; } + // Engagement-Spalten (Durchgänge, Spielzeit, Zuletzt): mehr/aktueller = grün + var maxPlays = Math.max.apply(null, [1].concat(d.students.map(function(s){ return s.plays || 0; }))); + var maxDur = Math.max.apply(null, [1].concat(d.students.map(function(s){ return s.total_duration_ms || 0; }))); + var NOW = Date.now(); + function recencyGood(lp){ + if (!lp) return null; + var days = (NOW - new Date(lp.replace(' ','T')+'Z').getTime()) / 86400000; + return Math.max(0, Math.min(1, 1 - days/21)); // heute = 1, ab ~3 Wochen = 0 + } html += ''; html += '' @@ -3604,7 +3613,7 @@ function renderModDetail() { html += '' + '' + '' - + ''; + + ''; hl.forEach(function(h) { var v = (s.highlights || {})[h.key]; var isChamp = (champs[h.key] === s.id); @@ -3613,8 +3622,8 @@ function renderModDetail() { + (isChamp && v !== null && v !== undefined ? '🏆 ' : '') + text + ampBar(hlGoodness(h, v)) + ''; }); - html += '' - + '' + html += '' + + '' + '' + ''; }); @@ -3655,7 +3664,7 @@ async function openStudentRuns(studentId) { }); } else { // Alle übrigen: Liniengrafik über die wichtigsten Kennzahlen (statt JSON) - html += renderRunsLineChart(_modDetail.data.highlights || [], r.runs); + html += renderRunsLineChart(_modDetail.moduleId, r.runs); } html += ''; box.innerHTML = html; @@ -3921,34 +3930,36 @@ function _hlGet(obj, path){ for (var i = 0; i < parts.length; i++) { if (v == null) return null; v = v[parts[i]]; } return v; } -function renderRunsLineChart(highlights, runs) { +function renderRunsLineChart(simId, runs) { var rs = runs.slice().filter(function(r){ return r.submitted_at; }) .sort(function(a,b){ return a.submitted_at < b.submitted_at ? -1 : a.submitted_at > b.submitted_at ? 1 : 0; }); - // Kennzahlen: Sterne (falls vorhanden) + alle Highlight-Spalten - var metrics = [{ key:'stars', label:'Sterne', hi:true, star:true }]; - (highlights || []).forEach(function(h){ - metrics.push({ key:h.key, label:h.label, unit:h.unit, format:h.format, hi:(h.agg !== 'min') }); + function runData(run){ return (run.results && run.results.data) ? run.results.data : (run.results || {}); } + // Kennzahlen aus dem gemeinsamen Sim-Schema (deckt alle Chart-Module ab) + var schema = (typeof simPlotMetrics === 'function') ? simPlotMetrics(simId) : []; + var metrics = schema.map(function(f){ + return { key:f.k, t:f.t, type:f.type, label:f.l, unit:f.u, + hi:(typeof metricHigherBetter === 'function' ? metricHigherBetter(f) : true) }; }); function rawVal(run, m){ - var v = m.star ? _hlGet(run.results, 'stars') : _hlGet(run.results, m.key); + var d = runData(run), v = d[m.key]; if (v === null || v === undefined || isNaN(+v)) return null; + if (m.type === 'ratio') { var t = d[m.t]; return (t > 0) ? (+v / t * 100) : null; } return +v; } function fmtMetric(m, v){ if (v === null) return '–'; - if (m.star) return Math.round(v) + '★'; - if (m.format === 'percent') return Math.round(v * 100) + ' %'; + if (m.type === 'stars' || m.type === 'starsSmall') return Math.round(v) + '★'; + if (m.type === 'pct' || m.type === 'pctInv' || m.type === 'ratio') return Math.round(v) + ' %'; + if (m.type === 'temp') return (Math.round(v*10)/10).toLocaleString('de-AT') + ' °C'; var n = Math.abs(v) >= 100 ? Math.round(v) : Math.round(v * 10) / 10; return n.toLocaleString('de-AT') + (m.unit ? ' ' + m.unit : ''); } - // Snapshot statt Linie bei nur einem Durchgang + // Snapshot (grafische Kacheln) bei nur einem Durchgang if (rs.length < 2) { - var only = (rs[0] || runs[0] || {}).results || {}; - var chips = metrics.map(function(m){ var v = rawVal(rs[0]||runs[0]||{}, m); return v===null ? '' : - '
'+esc(m.label)+''+fmtMetric(m,v)+'
'; }).join(''); + var only = runData(rs[0] || runs[0] || {}); return '

Erst ein Durchgang – die Liniengrafik erscheint ab dem zweiten. Aktueller Stand:

' - + '
'+chips+'
'; + + ((typeof renderSimMetrics === 'function') ? renderSimMetrics(simId, only) : '') + ''; } var COLORS = ['#3f7a45','#c65a49','#3a6a8a','#c9913a','#7a5a9a','#4a9a8a','#b0743a'];
'+avatar+' '+esc(s.displayName)+''+starStr+(s.plays>0?ampBar(stars/5):'')+''+s.plays+''+s.plays+(s.plays>0?ampBar(s.plays/maxPlays):'')+''+lastDate+''+dur+''+lastDate+(s.plays>0?ampBar(recencyGood(s.last_played)):'')+''+dur+(s.total_duration_ms?ampBar(s.total_duration_ms/maxDur):'')+''+(s.plays > 0 ? '' : '')+'