File size: 8,821 Bytes
2ce6518
 
 
 
 
 
 
 
 
 
 
 
 
9dde5d4
2ce6518
 
 
 
 
 
 
9dde5d4
2ce6518
 
 
 
 
9dde5d4
2ce6518
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9dde5d4
 
 
 
2ce6518
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cbd6f7b
2ce6518
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cbd6f7b
2ce6518
cbd6f7b
2ce6518
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cbd6f7b
 
2ce6518
 
9dde5d4
2ce6518
9dde5d4
2ce6518
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
/* 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 }; });
})();