/* Results / graphs tester — animates the paper's headline numbers the way the Manim results scene does: * bars grow in and the paper's own method is highlighted with its delta over the best prior; line series * draw left-to-right; metric cards count up. Reads the SAME chart spec the video's chart is built from * (charts.ChartSpec.as_scene_dict: kind/categories/series/highlight/…), so the graph here equals the * graph there. */ (function () { const k = MANIMO.kit; function norm(p) { const kind = String(p.kind || p.primitive || "bar").toLowerCase(); const cats = (p.categories || p.labels || []).map(String); let series = p.series || []; if (!series.length && p.values) series = [{ name: "", values: p.values }]; series = series.map((s) => ({ name: String(s.name || ""), values: (s.values || []).map((v) => { const n = Number(v); return Number.isFinite(n) ? n : 0; }), points: s.points || null })); return { kind, title: p.title || "", y: p.y_label || p.ylabel || "", unit: p.unit || "", highlight: p.highlight || null, cats, series, metrics: p.metrics || [] }; } // ── bar / line on a canvas ──────────────────────────────────────────────── function mountChart(root, concept, theme) { const spec = norm((concept && concept.params) || {}); const cv = document.createElement("canvas"); cv.width = 560; cv.height = 260; cv.className = "plot chart"; cv.setAttribute("role", "img"); cv.setAttribute("aria-label", (spec.title || "Results") + " " + spec.kind + " chart"); const log = k.panel("steps"); const isLine = spec.kind === "line"; const cats = spec.cats.length ? spec.cats : (spec.series[0] ? spec.series[0].values.map((_, i) => "x" + i) : []); const vals = spec.series[0] ? spec.series[0].values : []; const hiIdx = cats.indexOf(spec.highlight); const best = bestOther(vals, hiIdx, cats); function bars(shown, grow) { const ctx = cv.getContext("2d"), W = cv.width, H = cv.height, n = cats.length, pad = 34; ctx.fillStyle = theme.bg; ctx.fillRect(0, 0, W, H); const mx = Math.max.apply(null, vals.concat([1e-6])) * 1.15, bw = (W - pad - 14) / Math.max(1, n); ctx.strokeStyle = theme.muted; ctx.globalAlpha = 0.5; ctx.beginPath(); ctx.moveTo(pad, H - 26); ctx.lineTo(W - 6, H - 26); ctx.stroke(); ctx.globalAlpha = 1; cats.forEach((c, i) => { const full = (vals[i] || 0) / mx * (H - 56); const h = i < shown ? full : (i === shown ? full * grow : 0); const x = pad + i * bw + 6; const hot = i === hiIdx; ctx.fillStyle = hot ? theme.accent2 : theme.accent; ctx.globalAlpha = hot ? 1 : 0.85; if (hot) { ctx.shadowColor = theme.accent2; ctx.shadowBlur = 16; } ctx.fillRect(x, H - 26 - h, bw - 12, h); ctx.shadowBlur = 0; ctx.globalAlpha = 1; ctx.fillStyle = theme.ink; ctx.font = "11px system-ui"; ctx.textAlign = "center"; ctx.fillText(c, x + (bw - 12) / 2, H - 12); if (i < shown || (i === shown && grow > 0.98)) { ctx.fillStyle = hot ? theme.accent2 : theme.ink; ctx.font = "bold 12px system-ui"; ctx.fillText(k.fmt(vals[i]) + spec.unit, x + (bw - 12) / 2, H - 30 - h); } }); if (spec.title) { ctx.fillStyle = theme.muted; ctx.font = "12px system-ui"; ctx.textAlign = "left"; ctx.fillText(spec.title + (spec.y ? " (" + spec.y + ")" : ""), pad, 16); } } function line(prog) { const ctx = cv.getContext("2d"), W = cv.width, H = cv.height, pad = 34; ctx.fillStyle = theme.bg; ctx.fillRect(0, 0, W, H); const pts = (spec.series[0] && spec.series[0].points) || vals.map((v, i) => [i, v]); if (!pts.length) { // empty/malformed series → draw the frame + a note, never index an empty array if (spec.title) { ctx.fillStyle = theme.muted; ctx.font = "12px system-ui"; ctx.textAlign = "left"; ctx.fillText(spec.title + " — no data", pad, 16); } return; } const xs = pts.map((q) => q[0]), ys = pts.map((q) => q[1]); const xlo = Math.min.apply(null, xs), xhi = Math.max.apply(null, xs), ylo = Math.min.apply(null, ys), yhi = Math.max.apply(null, ys); const X = (v) => pad + (v - xlo) / ((xhi - xlo) || 1) * (W - pad - 12); const Y = (v) => H - 26 - (v - ylo) / ((yhi - ylo) || 1) * (H - 52); ctx.strokeStyle = theme.muted; ctx.globalAlpha = .5; ctx.beginPath(); ctx.moveTo(pad, H - 26); ctx.lineTo(W - 6, H - 26); ctx.stroke(); ctx.globalAlpha = 1; const upto = Math.max(1, Math.floor(pts.length * prog)); ctx.strokeStyle = theme.accent; ctx.lineWidth = 3; ctx.beginPath(); for (let i = 0; i < upto; i++) (i ? ctx.lineTo : ctx.moveTo).call(ctx, X(pts[i][0]), Y(pts[i][1])); ctx.stroke(); const last = pts[upto - 1]; ctx.fillStyle = theme.accent2; ctx.beginPath(); ctx.arc(X(last[0]), Y(last[1]), 5, 0, 7); ctx.fill(); if (spec.title) { ctx.fillStyle = theme.muted; ctx.font = "12px system-ui"; ctx.textAlign = "left"; ctx.fillText(spec.title, pad, 16); } } let ctrl = null, raf = null; if (isLine) { root.appendChild(cv); root.appendChild(log); k.logLine(log, "training curve — " + (spec.y || "value") + " over time", true); line(1); function draw(done) { stop(); const t0 = performance.now(); (function tick(t) { const u = Math.min(1, (t - t0) / 2600); line(u); if (u >= 1) { raf = null; if (done) done(); return; } raf = requestAnimationFrame(tick); })(t0); } function stop() { if (raf) { cancelAnimationFrame(raf); raf = null; } } root.appendChild(k.row([k.button("▶ Draw", () => draw())], "ctrls")); return { play: (d) => draw(d), stop, reset: () => { stop(); line(1); } }; } function render(step) { bars(step, 1); k.clear(log); log.appendChild(k.el("div", { class: "steps-h" }, [(spec.title || "results") + " (" + step + " / " + cats.length + ")"])); for (let i = 0; i < step; i++) { const tag = i === hiIdx ? " ◀ this paper" : ""; k.logLine(log, cats[i] + " = " + k.fmt(vals[i]) + spec.unit + "" + tag, i === step - 1); } if (step >= cats.length && hiIdx >= 0 && best != null) { const d = vals[hiIdx] - best.v; k.logLine(log, "▲ " + (d >= 0 ? "+" : "") + k.fmt(d) + spec.unit + " vs best prior (" + best.name + ")", true); } } ctrl = k.stepper(new Array(cats.length), render); root.appendChild(cv); root.appendChild(k.row([k.button("▸ Step", () => ctrl.next()), k.button("▶ Play", () => ctrl.play(1050)), k.button("↺ Reset", () => ctrl.reset(), "ghost")], "ctrls")); root.appendChild(log); return k.player(ctrl, 950); } // ── metric cards (count up) ─────────────────────────────────────────────── function mountMetric(root, concept) { const spec = norm((concept && concept.params) || {}); const wrap = k.el("div", { class: "metrics" }); const log = k.panel("steps"); root.appendChild(wrap); root.appendChild(log); function render(step) { k.clear(wrap); k.clear(log); log.appendChild(k.el("div", { class: "steps-h" }, ["headline metrics (" + step + " / " + spec.metrics.length + ")"])); spec.metrics.slice(0, step).forEach((mtr, i) => { wrap.appendChild(k.el("div", { class: "metric" }, [ k.el("div", { class: "m-val" }, [k.fmt(Number(mtr.value)) + (mtr.unit || "")]), k.el("div", { class: "m-name" }, [String(mtr.name || "")]), mtr.delta != null ? k.el("div", { class: "m-delta" }, ["▲ " + String(mtr.delta)]) : "", ].filter(Boolean))); k.logLine(log, (mtr.name || "metric") + " = " + k.fmt(Number(mtr.value)) + (mtr.unit || "") + "" + (mtr.delta != null ? " (" + mtr.delta + ")" : ""), i === step - 1); }); } const ctrl = k.stepper(new Array(spec.metrics.length), render); root.appendChild(k.row([k.button("▸ Step", () => ctrl.next()), k.button("▶ Play", () => ctrl.play(1150)), k.button("↺ Reset", () => ctrl.reset(), "ghost")], "ctrls")); return k.player(ctrl, 1080); } function bestOther(vals, hiIdx, cats) { let b = null; vals.forEach((v, i) => { if (i !== hiIdx && (b == null || v > b.v)) b = { v, name: (cats && cats[i]) || ("#" + i) }; }); return b; } function mount(root, concept, theme) { const kind = String(((concept && concept.params) || {}).kind || (concept && concept.primitive) || "bar").toLowerCase(); return kind === "metric" ? mountMetric(root, concept) : mountChart(root, concept, theme); } ["chart", "results", "bar", "line", "metric", "graph"].forEach((key) => { MANIMO.registry[key] = { title: "Results", mount }; }); })();