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%
9.6 KB · 186 lines javascript
Raw Blame History
1// ============================================================================2//  Project   : modelmap3//  File      : site/lib/charts.js4//  Purpose   : Server-rendered SVG charts for real result maps (dataviz spec)5//  Author    : Simon-Pierre Boucher6//  Contact   : contact@spboucher.ai7//  Website   : https://modelmap.io8//  Created   : 2026-08-129//  Modified  : 2026-08-1210//  Platform  : macOS / Apple Silicon (arm64) — Node.js (deployed on MacLustr)11//  License   : All rights reserved (research code)12// ============================================================================13"use strict";1415// Categorical palette validated with the dataviz six-checks script16// (violet/orange PASS all; aqua passes with a contrast WARN → bars using it17// always carry direct value labels). The twin/null is NOT a categorical slot:18// it renders as a dashed neutral reference line, direct-labeled.19const S1 = "#6d4fc4";   // series 1 — violet20const S2 = "#eb6834";   // series 2 — orange21const S3 = "#1baf7a";   // series 3 — aqua (direct labels mandatory)22const REF = "#8b8798";  // reference/null line (muted ink, dashed)2324function esc(s) {25  return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));26}2728const fmt = (v) => (Math.abs(v) >= 100 ? v.toFixed(0) : Math.abs(v) >= 10 ? v.toFixed(1) : v.toFixed(2));2930/**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 */35function layerLineSvg({ title, series, yLabel, yMin = 0, yMax = 1, caption, xTitle = "layer", xTickLabels = null }) {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;4142  let grid = "";43  const defaultTicks = [0, 0.25, 0.5, 0.75, 1].filter((v) => v >= yMin && v <= yMax);44  const ticks = (yMin >= 0 && yMax <= 1 && defaultTicks.length >= 3)45    ? defaultTicks46    : [0, 1, 2, 3, 4].map((i) => yMin + (i / 4) * (yMax - yMin));47  for (const v of ticks) {48    grid += `<line x1="${ML}" y1="${Y(v)}" x2="${ML + iw}" y2="${Y(v)}" class="grid"/>` +49      `<text x="${ML - 8}" y="${Y(v) + 4}" class="tick" text-anchor="end">${fmt(v)}</text>`;50  }51  if (yMin < 0 && yMax > 0) {52    grid += `<line x1="${ML}" y1="${Y(0)}" x2="${ML + iw}" y2="${Y(0)}" class="axis"/>`;53  }54  let xt = "";55  const xlab = (i) => (xTickLabels ? (xTickLabels[i] ?? "") : String(i));56  for (let i = 0; i < nx; i += Math.ceil(nx / 8)) {57    xt += `<text x="${X(i)}" y="${MT + ih + 18}" class="tick" text-anchor="middle">${xlab(i)}</text>`;58  }59  if (!xTickLabels) xt += `<text x="${X(nx - 1)}" y="${MT + ih + 18}" class="tick" text-anchor="middle">${xlab(nx - 1)}</text>`;60  else for (let i = 1; i < nx; i += 1) if (i % Math.ceil(nx / 8) !== 0) xt += `<text x="${X(i)}" y="${MT + ih + 18}" class="tick" text-anchor="middle">${xlab(i)}</text>`;6162  let lines = "", dots = "", labels = "";63  const usedY = [];64  for (const s of series) {65    const d = s.values.map((v, i) => `${i ? "L" : "M"}${X(i).toFixed(1)},${Y(v).toFixed(1)}`).join(" ");66    lines += `<path d="${d}" fill="none" stroke="${s.color}" stroke-width="2" ` +67      `stroke-linejoin="round"${s.dash ? ' stroke-dasharray="6 5"' : ""}/>`;68    s.values.forEach((v, i) => {69      dots += `<circle cx="${X(i).toFixed(1)}" cy="${Y(v).toFixed(1)}" r="7" fill="transparent" ` +70        `class="pt" data-tip="${esc(s.label)} · layer ${i}: ${fmt(v)}"/>`;71    });72    let ly = Y(s.values[s.values.length - 1]);73    while (usedY.some((u) => Math.abs(u - ly) < 13)) ly += 13;   // collision nudge74    usedY.push(ly);75    labels += `<text x="${ML + iw + 8}" y="${ly + 4}" class="series-label" fill="${s.ref ? REF : s.color}">${esc(s.label)}</text>`;76  }77  const legend = series.map((s) =>78    `<span class="lg"><span class="lg-swatch" style="background:${s.ref ? "transparent" : s.color};` +79    `${s.ref ? `border-top:2px dashed ${REF};height:0;` : ""}"></span>${esc(s.label)}</span>`).join("");8081  return `<figure class="chart-fig">82${title ? `<figcaption class="chart-title">${esc(title)}</figcaption>` : ""}83<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(title || yLabel)}">84<line x1="${ML}" y1="${MT + ih}" x2="${ML + iw}" y2="${MT + ih}" class="axis"/>85${grid}${xt}${lines}${dots}${labels}86<text x="${ML + iw / 2}" y="${H - 4}" class="axis-title" text-anchor="middle">${esc(xTitle)}</text>87<text transform="rotate(-90 14 ${MT + ih / 2})" x="14" y="${MT + ih / 2}" class="axis-title" text-anchor="middle">${esc(yLabel)}</text>88</svg>89<div class="chart-legend">${legend}</div>90<div class="chart-tip" hidden></div>91${caption ? `<figcaption>${caption}</figcaption>` : ""}92</figure>`;93}9495/**96 * Small-multiple bar panels sharing one y-scale (identity on the x axis →97 * single hue; every bar direct-labeled; 4px rounded data ends; 2px gaps).98 * panels: [{title, bars: [{label, value}]}]99 */100function barPanelsSvg({ title, panels, yLabel, unit = "", color = S1, caption }) {101  const PW = 340, H = 280, ML = 46, MR = 8, MT = 30, MB = 58;102  const ih = H - MT - MB;103  const vmax = Math.max(...panels.flatMap((p) => p.bars.map((b) => b.value))) * 1.15;104  const W = panels.length * PW;105  let out = "";106  panels.forEach((panel, pi) => {107    const x0 = pi * PW;108    const iw = PW - ML - MR;109    const n = panel.bars.length;110    const bw = Math.min(46, (iw / n) * 0.62);111    out += `<text x="${x0 + ML + iw / 2}" y="${MT - 12}" class="panel-title" text-anchor="middle">${esc(panel.title)}</text>`;112    for (const v of [0.25, 0.5, 0.75, 1]) {113      const y = MT + ih - v * ih;114      out += `<line x1="${x0 + ML}" y1="${y}" x2="${x0 + ML + iw}" y2="${y}" class="grid"/>`;115      if (pi === 0) out += `<text x="${x0 + ML - 8}" y="${y + 4}" class="tick" text-anchor="end">${fmt(vmax * v)}</text>`;116    }117    panel.bars.forEach((b, i) => {118      const cx = x0 + ML + ((i + 0.5) / n) * iw;119      const h = Math.max(2, (b.value / vmax) * ih);120      const y = MT + ih - h;121      out += `<rect x="${(cx - bw / 2).toFixed(1)}" y="${y.toFixed(1)}" width="${bw}" height="${h.toFixed(1)}" ` +122        `rx="4" fill="${color}" class="pt" data-tip="${esc(panel.title)} · ${esc(b.label)}: ${fmt(b.value)} ${esc(unit)}"/>` +123        `<rect x="${(cx - bw / 2).toFixed(1)}" y="${(MT + ih - 2).toFixed(1)}" width="${bw}" height="2" fill="${color}"/>` +124        `<text x="${cx}" y="${y - 5}" class="bar-val" text-anchor="middle">${fmt(b.value)}</text>` +125        `<text x="${cx}" y="${MT + ih + 16}" class="tick" text-anchor="middle">${esc(b.label)}</text>`;126    });127    out += `<line x1="${x0 + ML}" y1="${MT + ih}" x2="${x0 + ML + iw}" y2="${MT + ih}" class="axis"/>`;128  });129  return `<figure class="chart-fig">130${title ? `<figcaption class="chart-title">${esc(title)}</figcaption>` : ""}131<svg viewBox="0 0 ${panels.length * PW} ${H}" role="img" aria-label="${esc(title || yLabel)}">132${out}133<text transform="rotate(-90 12 ${MT + ih / 2})" x="12" y="${MT + ih / 2}" class="axis-title" text-anchor="middle">${esc(yLabel)}</text>134</svg>135<div class="chart-tip" hidden></div>136${caption ? `<figcaption>${caption}</figcaption>` : ""}137</figure>`;138}139140/**141 * Grouped bars: groups on x, one bar per mode, fixed mode→color assignment,142 * every bar direct-labeled (satisfies the aqua contrast WARN relief).143 * groups: [{label, values: {modeKey: value}}], modes: [{key,label,color}]144 */145function groupedBarSvg({ title, groups, modes, yLabel, unit = "", caption }) {146  const W = 720, H = 300, ML = 52, MR = 10, MT = 16, MB = 58;147  const iw = W - ML - MR, ih = H - MT - MB;148  const vmax = Math.max(...groups.flatMap((g) => modes.map((m) => g.values[m.key] || 0))) * 1.18;149  const gw = iw / groups.length;150  const bw = Math.min(34, (gw / modes.length) * 0.6);151  let out = "";152  for (const v of [0.25, 0.5, 0.75, 1]) {153    const y = MT + ih - v * ih;154    out += `<line x1="${ML}" y1="${y}" x2="${ML + iw}" y2="${y}" class="grid"/>` +155      `<text x="${ML - 8}" y="${y + 4}" class="tick" text-anchor="end">${fmt(vmax * v)}</text>`;156  }157  groups.forEach((g, gi) => {158    const gx = ML + gi * gw + gw / 2;159    modes.forEach((m, mi) => {160      const v = g.values[m.key];161      if (v == null) return;162      const cx = gx + (mi - (modes.length - 1) / 2) * (bw + 2);163      const h = Math.max(2, (v / vmax) * ih);164      const y = MT + ih - h;165      out += `<rect x="${(cx - bw / 2).toFixed(1)}" y="${y.toFixed(1)}" width="${bw}" height="${h.toFixed(1)}" ` +166        `rx="4" fill="${m.color}" class="pt" data-tip="${esc(g.label)} · ${esc(m.label)}: ${fmt(v)} ${esc(unit)}"/>` +167        `<text x="${cx}" y="${y - 5}" class="bar-val" text-anchor="middle">${fmt(v)}</text>`;168    });169    out += `<text x="${gx}" y="${MT + ih + 18}" class="tick" text-anchor="middle">${esc(g.label)}</text>`;170  });171  const legend = modes.map((m) =>172    `<span class="lg"><span class="lg-swatch" style="background:${m.color}"></span>${esc(m.label)}</span>`).join("");173  return `<figure class="chart-fig">174${title ? `<figcaption class="chart-title">${esc(title)}</figcaption>` : ""}175<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(title || yLabel)}">176${out}<line x1="${ML}" y1="${MT + ih}" x2="${ML + iw}" y2="${MT + ih}" class="axis"/>177<text transform="rotate(-90 12 ${MT + ih / 2})" x="12" y="${MT + ih / 2}" class="axis-title" text-anchor="middle">${esc(yLabel)}</text>178</svg>179<div class="chart-legend">${legend}</div>180<div class="chart-tip" hidden></div>181${caption ? `<figcaption>${caption}</figcaption>` : ""}182</figure>`;183}184185module.exports = { layerLineSvg, barPanelsSvg, groupedBarSvg, S1, S2, S3, REF };186