diff --git a/App/assets/js/sim-metrics.js b/App/assets/js/sim-metrics.js index 71610c5..e03bbbe 100644 --- a/App/assets/js/sim-metrics.js +++ b/App/assets/js/sim-metrics.js @@ -19,7 +19,7 @@ {k:'stars',type:'stars',l:'Sterne'}, {k:'final_temperature',type:'temp',l:'Temperatur'}, {k:'final_flooded_pct',type:'pctInv',l:'Überflutung'}, - {k:'final_co2',type:'num',l:'CO₂',u:'ppm'}, + {k:'final_co2',type:'num',l:'CO₂',u:'ppm',dir:'lo'}, {k:'final_budget',type:'money',l:'Budget'}, {k:'final_population',type:'num',l:'Bevölkerung'}, {k:'measures_bought',type:'num',l:'Maßnahmen'} @@ -37,7 +37,7 @@ {k:'stars',type:'stars',l:'Sterne'}, {k:'wonOrders',type:'ratio',t:'totalOrders',l:'Aufträge gewonnen'}, {k:'quizFirstTry',type:'ratio',t:'totalOrders',l:'Quiz Erstversuch'}, - {k:'avgTapKm',type:'num',l:'Ø Abweichung',u:'km'} + {k:'avgTapKm',type:'num',l:'Ø Abweichung',u:'km',dir:'lo'} ], farmer: [ {k:'stars',type:'stars',l:'Sterne'}, @@ -51,7 +51,7 @@ ], 'eu-werkstatt': [ {k:'firstTryHits',type:'ratio',t:'stepsTotal',l:'Erste Wahl'}, - {k:'muelleimerCount',type:'num',l:'verworfen'}, + {k:'muelleimerCount',type:'num',l:'verworfen',dir:'lo'}, {k:'endType',type:'text',l:'Ergebnis'} ], energiemanager: [ @@ -62,7 +62,7 @@ weltkueche: [ {k:'stars',type:'stars',l:'Sterne'}, {k:'completedDishes',type:'ratio',t:'totalDishes',l:'Gerichte'}, - {k:'mistakesInDish',type:'num',l:'Fehlklicks'}, + {k:'mistakesInDish',type:'num',l:'Fehlklicks',dir:'lo'}, {k:'dishName',type:'text',l:'Aktuelles Gericht'}, {k:'ingSolved',type:'ratio',t:'ingTotal',l:'Zutaten gelöst'}, {k:'progressPct',type:'pct',l:'Fortschritt'} @@ -70,7 +70,7 @@ logistik: [ {k:'stars',type:'stars',l:'Sterne'}, {k:'ordersDelivered',type:'ratio',t:'totalOrders',l:'Ausgeliefert'}, - {k:'ordersLate',type:'num',l:'verspätet'}, + {k:'ordersLate',type:'num',l:'verspätet',dir:'lo'}, {k:'finalBalance',type:'money',l:'Bilanz'}, {k:'balance',type:'money',l:'Konto'} ], @@ -84,7 +84,7 @@ kofferdetektiv: [ {k:'cases_solved',type:'ratio',t:'cases_total',l:'Fälle gelöst'}, {k:'correct_first_try',type:'num',l:'Erstversuch'}, - {k:'wrong_guesses',type:'num',l:'Fehlversuche'} + {k:'wrong_guesses',type:'num',l:'Fehlversuche',dir:'lo'} ], tourismustal: [ {k:'goalsDone',type:'ratio',t:'goalsTotal',l:'Ziele erreicht'}, @@ -158,7 +158,26 @@ : 'keine auswertbaren Parameter'; } - root.SIM_METRICS = SIM_METRICS; - root.renderSimMetrics = renderSimMetrics; - root.ggsStarsHtml = stars; + // Plottbare (numerische) Kennzahlen einer Sim, dedupliziert nach Label. + var PLOTTABLE = {stars:1, starsSmall:1, pct:1, pctInv:1, ratio:1, temp:1, money:1, num:1}; + function simPlotMetrics(simId){ + var seen = {}, out = []; + (SIM_METRICS[simId] || []).forEach(function(f){ + if (!PLOTTABLE[f.type] || seen[f.l]) return; + seen[f.l] = 1; out.push(f); + }); + return out; + } + // true = höher ist besser (grün oben). temp/pctInv und dir:'lo' → niedriger ist besser. + function metricHigherBetter(f){ + if (f.dir === 'lo') return false; + if (f.dir === 'hi') return true; + return !(f.type === 'pctInv' || f.type === 'temp'); + } + + root.SIM_METRICS = SIM_METRICS; + root.renderSimMetrics = renderSimMetrics; + root.ggsStarsHtml = stars; + root.simPlotMetrics = simPlotMetrics; + root.metricHigherBetter = metricHigherBetter; })(typeof window !== 'undefined' ? window : this); diff --git a/App/teacher.html b/App/teacher.html index 3bc6f72..0104cc9 100644 --- a/App/teacher.html +++ b/App/teacher.html @@ -3556,6 +3556,30 @@ function renderModDetail() { return v + (h.unit ? ' ' + h.unit : ''); } + // Spalten-Spannweite je Kennzahl (für vergleichbare Balken) + var hlStats = {}; + hl.forEach(function(h) { + var vals = d.students.map(function(s){ var v=(s.highlights||{})[h.key]; return (v===null||v===undefined)?null:+v; }) + .filter(function(v){ return v!==null && !isNaN(v); }); + hlStats[h.key] = vals.length ? { min: Math.min.apply(null, vals), max: Math.max.apply(null, vals) } : null; + }); + // Ampelbalken: g = Güte 0..1 (1 = gut = grün). Grün immer positiv. + function ampBar(g) { + if (g === null) return ''; + g = Math.max(0, Math.min(1, g)); + var col = g>=.75 ? '#3f7a45' : g>=.55 ? '#7fa856' : g>=.4 ? '#e0a838' : g>=.25 ? '#e08a3a' : '#c65a49'; + return ''; + } + // Güte eines Werts in seiner Spalte, Richtung aus h.agg ('min' = weniger ist besser) + function hlGoodness(h, v) { + if (v === null || v === undefined || isNaN(+v)) return null; + var st = hlStats[h.key]; + if (!st || st.max === st.min) return 0.5; // keine Spreizung → neutral + var frac = (+v - st.min) / (st.max - st.min); + return h.agg === 'min' ? (1 - frac) : frac; + } + html += '
| Schüler*in '+arr('name')+' | ' @@ -3579,7 +3603,7 @@ function renderModDetail() { : s.emoji; html += '||||||
|---|---|---|---|---|---|---|
| '+avatar+' '+esc(s.displayName)+' | ' - + ''+starStr+' | ' + + ''+starStr+(s.plays>0?ampBar(stars/5):'')+' | ' + ''+s.plays+' | '; hl.forEach(function(h) { var v = (s.highlights || {})[h.key]; @@ -3587,7 +3611,7 @@ function renderModDetail() { var text = fmt(h, v); html += '' + (isChamp && v !== null && v !== undefined ? '🏆 ' : '') - + text + ' | '; + + text + ampBar(hlGoodness(h, v)) + ''; }); html += ''+lastDate+' | ' + ''+dur+' | ' @@ -3624,10 +3648,14 @@ async function openStudentRuns(studentId) { } else if (_modDetail.moduleId === 'kofferdetektiv') { // Kofferdetektiv: aggregierte Auswertung + Heatmap der gelösten Länder html += renderKofferdetektivStudentAnalysis(r.runs); - } else { + } else if ({'eu-werkstatt':1,'energiemanager':1,'busfahrt':1,'fluggesellschaft':1,'heli':1}[_modDetail.moduleId]) { + // Module mit eigener, reicher Durchgangs-Karte r.runs.forEach(function(run, i) { html += renderRunDetailCard(_modDetail.moduleId, run, i); }); + } else { + // Alle übrigen: Liniengrafik über die wichtigsten Kennzahlen (statt JSON) + html += renderRunsLineChart(_modDetail.data.highlights || [], r.runs); } html += ''; box.innerHTML = html; @@ -3880,6 +3908,116 @@ function renderSonnensystemSessionTeacher(sess) { /* Sim-spezifischer Pretty-Renderer für einen einzelnen Run. Fallback (unbekannte Sim): raw JSON ausklappbar. */ +/* ──────────────────────────────────────────────────────────────────────── + * Verlaufs-Liniengrafik über die wichtigsten Kennzahlen einer Sim. + * Quelle = die Spalten-Definitionen (highlights: key/agg/unit/format) plus + * Sterne. Eine farbige Linie je Kennzahl, je Kennzahl auf ihre eigene + * Spannweite normiert (damit alle sichtbar), OBEN = besser (Richtung aus agg). + * Legende zeigt letzten Wert + Trendpfeil (grün besser / rot schlechter). + * ──────────────────────────────────────────────────────────────────────── */ +function _hlGet(obj, path){ + if (!obj) return null; + var parts = String(path).split('.'), v = obj; + for (var i = 0; i < parts.length; i++) { if (v == null) return null; v = v[parts[i]]; } + return v; +} +function renderRunsLineChart(highlights, 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 rawVal(run, m){ + var v = m.star ? _hlGet(run.results, 'stars') : _hlGet(run.results, m.key); + if (v === null || v === undefined || isNaN(+v)) return 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) + ' %'; + 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 + 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 ? '' : + '