SPB Git

spb/modelmap Public License

Internal cartography of local LLMs on Apple Silicon — registered, gated, negative-first. Public atlas at modelmap.io.

Python 66.3% JavaScript 24.5% CSS 8.1% Shell 0.7%

Site: server-rendered SVG result maps (dataviz-validated palette)

- lib/charts.js: layer-profile lines, small-multiple bar panels, grouped
  bars — palette validated with the six-checks script (violet/orange PASS;
  aqua carries mandatory direct labels; twin = dashed neutral reference)
- expA probe map (trained A/B vs random-init twin) on home, experiment
  page and atlas entry; expH warm/cold storage panels + capture-overhead
  bars on home and experiment page; per-mark hover tooltips, legends

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 3 h ago (Aug 12, 2026) parent bb88c64

Showing 5 changed files with +320 and −16

added site/lib/charts.js +176 −0
@@ -0,0 +1,176 @@
1 +// ============================================================================
2 +// Project : modelmap
3 +// File : site/lib/charts.js
4 +// Purpose : Server-rendered SVG charts for real result maps (dataviz spec)
5 +// Author : Simon-Pierre Boucher
6 +// Contact : contact@spboucher.ai
7 +// Website : https://modelmap.io
8 +// Created : 2026-08-12
9 +// Modified : 2026-08-12
10 +// Platform : macOS / Apple Silicon (arm64) — Node.js (deployed on MacLustr)
11 +// License : All rights reserved (research code)
12 +// ============================================================================
13 +"use strict";
14 +
15 +// Categorical palette validated with the dataviz six-checks script
16 +// (violet/orange PASS all; aqua passes with a contrast WARN → bars using it
17 +// always carry direct value labels). The twin/null is NOT a categorical slot:
18 +// it renders as a dashed neutral reference line, direct-labeled.
19 +const S1 = "#6d4fc4"; // series 1 — violet
20 +const S2 = "#eb6834"; // series 2 — orange
21 +const S3 = "#1baf7a"; // series 3 — aqua (direct labels mandatory)
22 +const REF = "#8b8798"; // reference/null line (muted ink, dashed)
23 +
24 +function esc(s) {
25 + return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
26 +}
27 +
28 +const fmt = (v) => (Math.abs(v) >= 100 ? v.toFixed(0) : Math.abs(v) >= 10 ? v.toFixed(1) : v.toFixed(2));
29 +
30 +/**
31 + * Layer-profile line chart: x = layer index, y in [yMin,yMax].
32 + * series: [{label, color, dash?, ref?, values:[y per layer]}]
33 + * Direct labels at line ends + legend; per-point hover targets.
34 + */
35 +function layerLineSvg({ title, series, yLabel, yMin = 0, yMax = 1, caption }) {
36 + const W = 720, H = 300, ML = 52, MR = 118, MT = 16, MB = 40;
37 + const iw = W - ML - MR, ih = H - MT - MB;
38 + const nx = Math.max(...series.map((s) => s.values.length));
39 + const X = (i) => ML + (i / (nx - 1)) * iw;
40 + const Y = (v) => MT + ih - ((v - yMin) / (yMax - yMin)) * ih;
41 +
42 + let grid = "";
43 + for (const v of [0, 0.25, 0.5, 0.75, 1].filter((v) => v >= yMin && v <= yMax)) {
44 + grid += `<line x1="${ML}" y1="${Y(v)}" x2="${ML + iw}" y2="${Y(v)}" class="grid"/>` +
45 + `<text x="${ML - 8}" y="${Y(v) + 4}" class="tick" text-anchor="end">${v}</text>`;
46 + }
47 + let xt = "";
48 + for (let i = 0; i < nx; i += Math.ceil(nx / 8)) {
49 + xt += `<text x="${X(i)}" y="${MT + ih + 18}" class="tick" text-anchor="middle">${i}</text>`;
50 + }
51 + xt += `<text x="${X(nx - 1)}" y="${MT + ih + 18}" class="tick" text-anchor="middle">${nx - 1}</text>`;
52 +
53 + let lines = "", dots = "", labels = "";
54 + const usedY = [];
55 + for (const s of series) {
56 + const d = s.values.map((v, i) => `${i ? "L" : "M"}${X(i).toFixed(1)},${Y(v).toFixed(1)}`).join(" ");
57 + lines += `<path d="${d}" fill="none" stroke="${s.color}" stroke-width="2" ` +
58 + `stroke-linejoin="round"${s.dash ? ' stroke-dasharray="6 5"' : ""}/>`;
59 + s.values.forEach((v, i) => {
60 + dots += `<circle cx="${X(i).toFixed(1)}" cy="${Y(v).toFixed(1)}" r="7" fill="transparent" ` +
61 + `class="pt" data-tip="${esc(s.label)} · layer ${i}: ${fmt(v)}"/>`;
62 + });
63 + let ly = Y(s.values[s.values.length - 1]);
64 + while (usedY.some((u) => Math.abs(u - ly) < 13)) ly += 13; // collision nudge
65 + usedY.push(ly);
66 + labels += `<text x="${ML + iw + 8}" y="${ly + 4}" class="series-label" fill="${s.ref ? REF : s.color}">${esc(s.label)}</text>`;
67 + }
68 + const legend = series.map((s) =>
69 + `<span class="lg"><span class="lg-swatch" style="background:${s.ref ? "transparent" : s.color};` +
70 + `${s.ref ? `border-top:2px dashed ${REF};height:0;` : ""}"></span>${esc(s.label)}</span>`).join("");
71 +
72 + return `<figure class="chart-fig">
73 +${title ? `<figcaption class="chart-title">${esc(title)}</figcaption>` : ""}
74 +<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(title || yLabel)}">
75 +<line x1="${ML}" y1="${MT + ih}" x2="${ML + iw}" y2="${MT + ih}" class="axis"/>
76 +${grid}${xt}${lines}${dots}${labels}
77 +<text x="${ML + iw / 2}" y="${H - 4}" class="axis-title" text-anchor="middle">layer</text>
78 +<text transform="rotate(-90 14 ${MT + ih / 2})" x="14" y="${MT + ih / 2}" class="axis-title" text-anchor="middle">${esc(yLabel)}</text>
79 +</svg>
80 +<div class="chart-legend">${legend}</div>
81 +<div class="chart-tip" hidden></div>
82 +${caption ? `<figcaption>${caption}</figcaption>` : ""}
83 +</figure>`;
84 +}
85 +
86 +/**
87 + * Small-multiple bar panels sharing one y-scale (identity on the x axis →
88 + * single hue; every bar direct-labeled; 4px rounded data ends; 2px gaps).
89 + * panels: [{title, bars: [{label, value}]}]
90 + */
91 +function barPanelsSvg({ title, panels, yLabel, unit = "", color = S1, caption }) {
92 + const PW = 340, H = 280, ML = 46, MR = 8, MT = 30, MB = 58;
93 + const ih = H - MT - MB;
94 + const vmax = Math.max(...panels.flatMap((p) => p.bars.map((b) => b.value))) * 1.15;
95 + const W = panels.length * PW;
96 + let out = "";
97 + panels.forEach((panel, pi) => {
98 + const x0 = pi * PW;
99 + const iw = PW - ML - MR;
100 + const n = panel.bars.length;
101 + const bw = Math.min(46, (iw / n) * 0.62);
102 + out += `<text x="${x0 + ML + iw / 2}" y="${MT - 12}" class="panel-title" text-anchor="middle">${esc(panel.title)}</text>`;
103 + for (const v of [0.25, 0.5, 0.75, 1]) {
104 + const y = MT + ih - v * ih;
105 + out += `<line x1="${x0 + ML}" y1="${y}" x2="${x0 + ML + iw}" y2="${y}" class="grid"/>`;
106 + if (pi === 0) out += `<text x="${x0 + ML - 8}" y="${y + 4}" class="tick" text-anchor="end">${fmt(vmax * v)}</text>`;
107 + }
108 + panel.bars.forEach((b, i) => {
109 + const cx = x0 + ML + ((i + 0.5) / n) * iw;
110 + const h = Math.max(2, (b.value / vmax) * ih);
111 + const y = MT + ih - h;
112 + out += `<rect x="${(cx - bw / 2).toFixed(1)}" y="${y.toFixed(1)}" width="${bw}" height="${h.toFixed(1)}" ` +
113 + `rx="4" fill="${color}" class="pt" data-tip="${esc(panel.title)} · ${esc(b.label)}: ${fmt(b.value)} ${esc(unit)}"/>` +
114 + `<rect x="${(cx - bw / 2).toFixed(1)}" y="${(MT + ih - 2).toFixed(1)}" width="${bw}" height="2" fill="${color}"/>` +
115 + `<text x="${cx}" y="${y - 5}" class="bar-val" text-anchor="middle">${fmt(b.value)}</text>` +
116 + `<text x="${cx}" y="${MT + ih + 16}" class="tick" text-anchor="middle">${esc(b.label)}</text>`;
117 + });
118 + out += `<line x1="${x0 + ML}" y1="${MT + ih}" x2="${x0 + ML + iw}" y2="${MT + ih}" class="axis"/>`;
119 + });
120 + return `<figure class="chart-fig">
121 +${title ? `<figcaption class="chart-title">${esc(title)}</figcaption>` : ""}
122 +<svg viewBox="0 0 ${panels.length * PW} ${H}" role="img" aria-label="${esc(title || yLabel)}">
123 +${out}
124 +<text transform="rotate(-90 12 ${MT + ih / 2})" x="12" y="${MT + ih / 2}" class="axis-title" text-anchor="middle">${esc(yLabel)}</text>
125 +</svg>
126 +<div class="chart-tip" hidden></div>
127 +${caption ? `<figcaption>${caption}</figcaption>` : ""}
128 +</figure>`;
129 +}
130 +
131 +/**
132 + * Grouped bars: groups on x, one bar per mode, fixed mode→color assignment,
133 + * every bar direct-labeled (satisfies the aqua contrast WARN relief).
134 + * groups: [{label, values: {modeKey: value}}], modes: [{key,label,color}]
135 + */
136 +function groupedBarSvg({ title, groups, modes, yLabel, unit = "", caption }) {
137 + const W = 720, H = 300, ML = 52, MR = 10, MT = 16, MB = 58;
138 + const iw = W - ML - MR, ih = H - MT - MB;
139 + const vmax = Math.max(...groups.flatMap((g) => modes.map((m) => g.values[m.key] || 0))) * 1.18;
140 + const gw = iw / groups.length;
141 + const bw = Math.min(34, (gw / modes.length) * 0.6);
142 + let out = "";
143 + for (const v of [0.25, 0.5, 0.75, 1]) {
144 + const y = MT + ih - v * ih;
145 + out += `<line x1="${ML}" y1="${y}" x2="${ML + iw}" y2="${y}" class="grid"/>` +
146 + `<text x="${ML - 8}" y="${y + 4}" class="tick" text-anchor="end">${fmt(vmax * v)}</text>`;
147 + }
148 + groups.forEach((g, gi) => {
149 + const gx = ML + gi * gw + gw / 2;
150 + modes.forEach((m, mi) => {
151 + const v = g.values[m.key];
152 + if (v == null) return;
153 + const cx = gx + (mi - (modes.length - 1) / 2) * (bw + 2);
154 + const h = Math.max(2, (v / vmax) * ih);
155 + const y = MT + ih - h;
156 + out += `<rect x="${(cx - bw / 2).toFixed(1)}" y="${y.toFixed(1)}" width="${bw}" height="${h.toFixed(1)}" ` +
157 + `rx="4" fill="${m.color}" class="pt" data-tip="${esc(g.label)} · ${esc(m.label)}: ${fmt(v)} ${esc(unit)}"/>` +
158 + `<text x="${cx}" y="${y - 5}" class="bar-val" text-anchor="middle">${fmt(v)}</text>`;
159 + });
160 + out += `<text x="${gx}" y="${MT + ih + 18}" class="tick" text-anchor="middle">${esc(g.label)}</text>`;
161 + });
162 + const legend = modes.map((m) =>
163 + `<span class="lg"><span class="lg-swatch" style="background:${m.color}"></span>${esc(m.label)}</span>`).join("");
164 + return `<figure class="chart-fig">
165 +${title ? `<figcaption class="chart-title">${esc(title)}</figcaption>` : ""}
166 +<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(title || yLabel)}">
167 +${out}<line x1="${ML}" y1="${MT + ih}" x2="${ML + iw}" y2="${MT + ih}" class="axis"/>
168 +<text transform="rotate(-90 12 ${MT + ih / 2})" x="12" y="${MT + ih / 2}" class="axis-title" text-anchor="middle">${esc(yLabel)}</text>
169 +</svg>
170 +<div class="chart-legend">${legend}</div>
171 +<div class="chart-tip" hidden></div>
172 +${caption ? `<figcaption>${caption}</figcaption>` : ""}
173 +</figure>`;
174 +}
175 +
176 +module.exports = { layerLineSvg, barPanelsSvg, groupedBarSvg, S1, S2, S3, REF };
modified site/lib/content.js +25 −0
@@ -185,6 +185,29 @@ function buildInfo() {
185 185 return readJson("build-info.json") || {};
186 186 }
187 187
188 +/** expA probe map from the first atlas entry (null before it exists). */
189 +function probeMapV1() {
190 + return readJson("atlas/qwen3-0.6b-4bit/probes/v1/map.json");
191 +}
192 +
193 +/** Parsed expH runs for charts: warm/cold storage rows + compute rows. */
194 +function expHData() {
195 + const out = { warm: null, cold: null, compute: [] };
196 + for (const r of listResultRuns().filter((x) => x.experiment === "expH_capture_cost_frontier")) {
197 + const d = readJson(path.posix.join(r.rel, "results.json"));
198 + if (!d) continue;
199 + if (d.run === 1) {
200 + out.warm = d.storage;
201 + out.compute.push(...(d.compute || []).map((c) => ({ ...c, source: "synthetic" })));
202 + } else if (d.run === 2) {
203 + out.cold = d.storage;
204 + } else if (d.run === 3) {
205 + out.compute.push(...(d.compute || []).map((c) => ({ ...c, source: "qwen3-0.6b-4bit" })));
206 + }
207 + }
208 + return out;
209 +}
210 +
188 211 /** Site-wide stats for the home page. */
189 212 function stats() {
190 213 const bib = readText("research/bibliography.md") || "";
@@ -222,4 +245,6 @@ module.exports = {
222 245 listAtlasEntries,
223 246 buildInfo,
224 247 stats,
248 + probeMapV1,
249 + expHData,
225 250 };
modified site/public/app.js +14 −14
@@ -39,20 +39,20 @@
39 39
40 40 // ---------------------------------------------------------- chart tooltips
41 41 (function () {
42 const fig = document.querySelector(".chart-fig");
43 if (!fig) return;
44 const tip = fig.querySelector(".chart-tip");
45 if (!tip) return;
46 fig.querySelectorAll(".pt").forEach((pt) => {
47 pt.addEventListener("mouseenter", () => {
48 tip.textContent = pt.getAttribute("data-tip");
49 tip.hidden = false;
42 + document.querySelectorAll(".chart-fig").forEach((fig) => {
43 + const tip = fig.querySelector(".chart-tip");
44 + if (!tip) return;
45 + fig.querySelectorAll(".pt").forEach((pt) => {
46 + pt.addEventListener("mouseenter", () => {
47 + tip.textContent = pt.getAttribute("data-tip");
48 + tip.hidden = false;
49 + });
50 + pt.addEventListener("mousemove", (ev) => {
51 + const r = fig.getBoundingClientRect();
52 + tip.style.left = Math.min(ev.clientX - r.left + 14, r.width - tip.offsetWidth - 4) + "px";
53 + tip.style.top = ev.clientY - r.top - 34 + "px";
54 + });
55 + pt.addEventListener("mouseleave", () => { tip.hidden = true; });
50 56 });
51 pt.addEventListener("mousemove", (ev) => {
52 const r = fig.getBoundingClientRect();
53 tip.style.left = Math.min(ev.clientX - r.left + 14, r.width - tip.offsetWidth - 4) + "px";
54 tip.style.top = ev.clientY - r.top - 34 + "px";
55 });
56 pt.addEventListener("mouseleave", () => { tip.hidden = true; });
57 57 });
58 58 })();
modified site/public/style.css +11 −1
@@ -296,7 +296,17 @@ h1, h2, h3, .page-title, .section-title { font-family: var(--font-display); font
296 296 .comment-body { font-size: 14.5px; color: var(--ink-2); white-space: pre-wrap; overflow-wrap: break-word; }
297 297
298 298 /* ---------------------------------------------------------------- charts */
299 .chart-fig { margin: 18px 0 8px; position: relative; }
299 +.chart-fig { margin: 22px 0 10px; position: relative; }
300 +.chart-fig + .chart-fig { margin-top: 34px; padding-top: 26px; border-top: 1px solid var(--grid); }
301 +.chart-title {
302 + font-family: var(--font-body); font-size: 14.5px; font-weight: 650;
303 + color: var(--ink); margin-bottom: 6px;
304 +}
305 +.panel-title { fill: var(--ink-2); font-size: 12px; font-weight: 600; font-family: var(--font-body); }
306 +.bar-val { fill: var(--ink-2); font-size: 10.5px; font-family: var(--font-mono); }
307 +.chart-legend { display: flex; flex-wrap: wrap; gap: 14px; margin: 6px 0 2px; font-size: 12.5px; color: var(--ink-2); }
308 +.lg { display: inline-flex; align-items: center; gap: 6px; }
309 +.lg-swatch { width: 14px; height: 14px; border-radius: 4px; display: inline-block; flex: 0 0 auto; }
300 310 .chart-fig svg { width: 100%; height: auto; }
301 311 .chart-fig .grid { stroke: var(--grid); stroke-width: 1; }
302 312 .chart-fig .axis { stroke: var(--baseline); stroke-width: 1; }
modified site/server.js +94 −1
@@ -17,6 +17,81 @@ const express = require("express");
17 17 const C = require("./lib/content");
18 18 const R = require("./lib/render");
19 19 const Comments = require("./lib/comments");
20 +const Charts = require("./lib/charts");
21 +
22 +// ------------------------------------------------------------ chart builders
23 +/** Layer-profile charts for the expA probe map (real A/B + twin null). */
24 +function probeMapCharts(onlyProp) {
25 + const m = C.probeMapV1();
26 + if (!m || !m.properties) return "";
27 + const props = onlyProp ? [onlyProp] : Object.keys(m.properties);
28 + return props.map((prop) => {
29 + const p = m.properties[prop];
30 + if (!p) return "";
31 + const sel = (rows) => rows.map((r) => r.selectivity_mean);
32 + return Charts.layerLineSvg({
33 + title: `${prop} — probe selectivity by layer (trained vs random-init twin)`,
34 + yLabel: "selectivity",
35 + yMin: 0, yMax: 1,
36 + series: [
37 + { label: "trained · set A", color: Charts.S1, values: sel(p.per_layer.A) },
38 + { label: "trained · set B", color: Charts.S2, values: sel(p.per_layer.B) },
39 + { label: "random-init twin", color: Charts.REF, dash: true, ref: true,
40 + values: p.twin_null_per_layer_A.map((r) => r.selectivity_mean) },
41 + ],
42 + caption: "The twin (dashed) matches the trained model — the registered validity gate " +
43 + "failed and this map is published as a NEGATIVE result: on these promptsets, probes " +
44 + "read the tokenizer + architecture prior, not learned computation. Level 1, 5 seeds, " +
45 + "shuffled-label controls inside every probe.",
46 + });
47 + }).join("");
48 +}
49 +
50 +/** expH charts: warm/cold storage panels + capture-overhead grouped bars. */
51 +function expHCharts() {
52 + const d = C.expHData();
53 + let html = "";
54 + if (d.warm && d.cold) {
55 + const rnd = (rows) => rows.filter((r) => r.op === "random-batch")
56 + .map((r) => ({ label: r.format.replace("zarr-", "zarr‑"), value: r.gb_per_s_mean }));
57 + html += Charts.barPanelsSvg({
58 + title: "Activation-store random-batch read throughput — the ordering inverts cold",
59 + yLabel: "GB/s", unit: "GB/s",
60 + panels: [
61 + { title: "warm cache · M5 Max (run #1)", bars: rnd(d.warm) },
62 + { title: "cold cache · M3 Ultra (run #2)", bars: rnd(d.cold) },
63 + ],
64 + caption: "Run #2 falsified the registered hypothesis: raw mmap wins warm (memory speed) " +
65 + "but collapses cold to page-fault IO (~8–16 KiB, QD 1) while chunked zarr reads 32 MiB " +
66 + "blocks. The rule is IO granularity, not the container.",
67 + });
68 + }
69 + if (d.compute.length) {
70 + const key = (c) => `${c.backend}·${c.source}`;
71 + const groupsMap = new Map();
72 + for (const c of d.compute) {
73 + const ms = 1000 * c.s_per_forward.reduce((a, b) => a + b, 0) / c.s_per_forward.length;
74 + if (!groupsMap.has(key(c))) groupsMap.set(key(c), { backend: c.backend, source: c.source, values: {} });
75 + groupsMap.get(key(c)).values[c.mode] = ms;
76 + }
77 + const label = (g) => g.backend === "torch-mps" ? "torch-MPS (synthetic)" :
78 + g.source === "synthetic" ? "MLX (synthetic)" : "MLX (Qwen3-0.6B-4bit)";
79 + html += Charts.groupedBarSvg({
80 + title: "Capture overhead per forward pass — retention is nearly free under MLX",
81 + yLabel: "ms / forward", unit: "ms",
82 + groups: [...groupsMap.values()].map((g) => ({ label: label(g), values: g.values })),
83 + modes: [
84 + { key: "plain", label: "plain", color: Charts.S1 },
85 + { key: "retain", label: "retain", color: Charts.S2 },
86 + { key: "retain+copy+write", label: "retain+copy+write", color: Charts.S3 },
87 + ],
88 + caption: "Runs #1 and #3: per-layer retention costs 1.02× (MLX synthetic) and 1.004× " +
89 + "(real 4-bit checkpoint) vs 1.22× on torch-MPS; copy+write stays ≤1.5× everywhere. " +
90 + "3 repeats per cell; manifests embedded in the raw JSON.",
91 + });
92 + }
93 + return html;
94 +}
20 95
21 96 const app = express();
22 97 const PORT = process.env.PORT || 8140;
@@ -105,6 +180,17 @@ app.get("/", (req, res) => {
105 180 </div>
106 181 </section>
107 182
183 +${(() => {
184 + const probe = probeMapCharts("lang_id");
185 + const exph = expHCharts();
186 + return probe || exph ? `<section class="card">
187 + <h2>First measured maps</h2>
188 + <p class="lede-small">Every figure below is regenerated from committed code + versioned results —
189 + raw JSON with hardware manifests under <a href="/results">Results</a>.</p>
190 + ${probe}${exph}
191 +</section>` : "";
192 + })()}
193 +
108 194 <section class="card">
109 195 <h2>Latest from the research log</h2>
110 196 <div class="log-grid">${logHtml || "<p>No log entries yet.</p>"}</div>
@@ -201,9 +287,13 @@ app.get("/experiments/:id", (req, res) => {
201 287 const codeLink = C.exists(path.posix.join(exp.rel, "benchmark.py"))
202 288 ? `<p><a class="more" href="/file/${exp.rel}/benchmark.py">View benchmark implementation (benchmark.py) →</a></p>`
203 289 : "";
290 + let charts = "";
291 + if (exp.id === "expH_capture_cost_frontier") charts = expHCharts();
292 + if (exp.id === "expA_probe_reliability") charts = probeMapCharts();
293 + if (charts) charts = `<section class="card"><h2>Result maps</h2>${charts}</section>`;
204 294 const body = `<p class="crumb"><a href="/experiments">Experiments</a> / ${R.esc(exp.id)}</p>
205 295 <h1 class="page-title">${R.esc(exp.id)}</h1><p class="lede-small">${R.esc(exp.purpose)}</p>
206 ${codeLink}${sections.join("")}${runsHtml}`;
296 +${codeLink}${charts}${sections.join("")}${runsHtml}`;
207 297 page(res, { title: exp.id, active: "Experiments", body });
208 298 });
209 299
@@ -251,9 +341,12 @@ app.get("/atlas/:model/:mapType/:version", (req, res) => {
251 341 const files = entry.files
252 342 .map((f) => `<li><a href="/results/${f.rel}">${f.name}</a> <span class="mono-small">${(f.size / 1024).toFixed(1)} KiB</span></li>`)
253 343 .join("");
344 + const mapCharts = (model === "qwen3-0.6b-4bit" && mapType === "probes" && version === "v1")
345 + ? probeMapCharts() : "";
254 346 const body = `<p class="crumb"><a href="/atlas">Atlas</a> / ${R.esc(entry.rel)}</p>
255 347 <h1 class="page-title">${R.esc(model)} — ${R.esc(mapType)} <span class="mono-small">${R.esc(version)}</span></h1>
256 348 <p>${R.levelBadge(entry.level)}</p>
349 +${mapCharts ? `<section class="card"><h2>The map</h2>${mapCharts}</section>` : ""}
257 350 ${confidence ? `<section class="card"><h2>Confidence</h2><div class="md">${R.markdownToHtml(confidence.content, entry.rel + "/confidence.md")}</div></section>` : ""}
258 351 <section class="card"><h2>Provenance</h2>${prov}</section>
259 352 <section class="card"><h2>Files</h2><ul class="file-list">${files}</ul></section>`;
260 353