jacooob521's picture
fix 14 medium review findings (correctness/robustness/a11y)
9dde5d4 verified
Raw
History Blame Contribute Delete
8.82 kB
/* 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] + " = <b>" + k.fmt(vals[i]) + spec.unit + "</b>" + tag, i === step - 1);
}
if (step >= cats.length && hiIdx >= 0 && best != null) {
const d = vals[hiIdx] - best.v;
k.logLine(log, "β–² <b>" + (d >= 0 ? "+" : "") + k.fmt(d) + spec.unit + "</b> 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") + " = <b>" + k.fmt(Number(mtr.value)) + (mtr.unit || "") + "</b>" + (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 }; });
})();