// One responsive geometry contract for the six Module 4 dashboards.
m04Tools = ({
columns(hostWidth) {
const w = Math.max(320, Math.min(760, hostWidth));
const narrow = w < 680;
const outer = narrow ? 12 : 16;
const gap = 16;
const railW = narrow ? w - 2 * outer : 232;
const mainW = narrow ? w - 2 * outer : w - 2 * outer - gap - railW;
return {w, narrow, outer, gap, railW, mainW};
},
textHeight(text, maxWidth, {
fontSize = 11,
lineHeight = 15,
fontWeight = null
} = {}) {
const lines = wrapText(text, maxWidth, {
fontSize,
fontWeight,
fontFamily: "var(--pg-mono)"
});
return {
lines,
height: lines.length ? (lines.length - 1) * lineHeight + fontSize * 1.25 : 0
};
},
layout(c, {
top,
plotHeight,
metricsHeight,
analysisHeight,
verdictHeight
}) {
if (c.narrow) {
const plot = {x: c.outer, y: top, w: c.mainW, h: plotHeight};
const metrics = {x: c.outer, y: plot.y + plot.h + c.gap, w: c.mainW, h: metricsHeight};
const analysis = {x: c.outer, y: metrics.y + metrics.h + c.gap, w: c.railW, h: analysisHeight};
const verdict = {x: c.outer, y: analysis.y + analysis.h + c.gap, w: c.railW, h: verdictHeight};
return {
plot,
metrics,
analysis,
verdict,
height: verdict.y + verdict.h + c.outer
};
}
const plot = {x: c.outer, y: top, w: c.mainW, h: plotHeight};
const metrics = {x: c.outer, y: plot.y + plot.h + c.gap, w: c.mainW, h: metricsHeight};
const analysis = {
x: c.outer + c.mainW + c.gap,
y: top,
w: c.railW,
h: analysisHeight
};
const verdict = {
x: analysis.x,
y: analysis.y + analysis.h + c.gap,
w: c.railW,
h: verdictHeight
};
return {
plot,
metrics,
analysis,
verdict,
height: Math.max(metrics.y + metrics.h, verdict.y + verdict.h) + c.outer
};
},
root(label, w, h) {
return d3.create("svg")
.attr("class", "diagram-fig")
.attr("role", "img")
.attr("aria-label", label)
.attr("viewBox", `0 0 ${w} ${h}`)
.attr("width", w)
.attr("height", h)
.style("width", "100%")
.style("height", "auto")
.style("max-width", `${w}px`)
.style("font-family", "var(--pg-mono, ui-monospace, monospace)");
},
card(svg, box, {role = null, title = null, accent = null} = {}) {
const g = svg.append("g")
.attr("transform", `translate(${box.x}, ${box.y})`);
if (role) g.attr("data-role", role);
g.append("rect")
.attr("width", box.w)
.attr("height", box.h)
.attr("rx", 9)
.attr("fill", diagramTheme.bgSecondary)
.attr("stroke", accent || diagramTheme.nodeStroke)
.attr("stroke-width", 1);
if (title) {
drawWrappedText(g, title, {
x: 14,
y: 22,
maxWidth: box.w - 28,
fontFamily: "var(--pg-mono)",
lineHeight: 14,
fontSize: 11,
fontWeight: 700,
fill: accent || diagramTheme.nodeText,
anchor: "start",
className: "m04-card-title"
});
}
return g;
}
})Module 04: Embeddings
DIAGRAM_LIB_REVISION = "2026-09-25-flow-waypoint-routes"
// =============================================================================
// D3 IMPORT
// =============================================================================
d3 = import("../../assets/vendor/d3/d3.esm.js")
// =============================================================================
// THEME DETECTION
// =============================================================================
// Reactive value that tracks Quarto's native dark mode and re-renders diagrams
// when the theme toggles. Generators.observe makes this a live OJS dependency:
// any cell referencing isDarkMode re-runs whenever the body/html class changes.
isDarkMode = Generators.observe(notify => {
const check = () =>
document.body.classList.contains('quarto-dark') ||
document.documentElement.classList.contains('quarto-dark');
notify(check());
const observer = new MutationObserver(() => notify(check()));
observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
return () => observer.disconnect();
})
// =============================================================================
// CSS VARIABLE UTILITIES
// =============================================================================
// Function to read CSS custom property values from the document.
// Reads from <body> because Quarto applies the .quarto-dark class there, so the
// dark-mode variable overrides resolve on the body element, not <html>.
getCSSVar = function(name, fallback = null) {
if (typeof document === 'undefined') return fallback;
const value = getComputedStyle(document.body).getPropertyValue(name).trim();
return value || fallback;
}
// Parse a CSS color string to {r, g, b} bytes. Understands #rgb/#rrggbb/
// #rrggbbaa, rgb()/rgba(), and var(--token) (resolved against <body>, like
// getCSSVar). Returns null for anything it can't parse — callers must fall
// back, never interpolate through NaN.
_parseColor = function(color) {
if (typeof color !== 'string') return null;
let c = color.trim();
const varMatch = c.match(/^var\(\s*(--[\w-]+)/);
if (varMatch) c = (getCSSVar(varMatch[1], '') || '').trim();
let m;
if ((m = c.match(/^#([0-9a-f]{3,8})$/i))) {
const h = m[1];
if (h.length === 3 || h.length === 4) {
return {
r: parseInt(h[0] + h[0], 16),
g: parseInt(h[1] + h[1], 16),
b: parseInt(h[2] + h[2], 16)
};
}
return {
r: parseInt(h.slice(0, 2), 16),
g: parseInt(h.slice(2, 4), 16),
b: parseInt(h.slice(4, 6), 16)
};
}
if ((m = c.match(/^rgba?\(\s*([\d.]+)\s*[, ]\s*([\d.]+)\s*[, ]\s*([\d.]+)/i))) {
return {r: +m[1], g: +m[2], b: +m[3]};
}
return null;
}
// =============================================================================
// THEME OBJECT
// =============================================================================
// Object containing all diagram colors read from CSS variables
// Falls back to hardcoded values if CSS vars not available
diagramTheme = {
// Light-mode fallback values (used if CSS vars are unavailable)
const lightFallbacks = {
nodeFill: '#f5f5f4',
nodeFillHover: '#e7e5e4',
nodeStroke: '#d6d3d1',
nodeText: '#1c1917',
edgeStroke: '#78716c',
highlight: '#f97316',
highlightGlow: 'rgba(249, 115, 22, 0.3)',
highlightBg: 'rgba(249, 115, 22, 0.1)',
accent: '#0ea5e9',
accentGlow: 'rgba(14, 165, 233, 0.3)',
textOnHighlight: '#1c1917',
textOnAccent: '#1c1917',
// Ink for highlight/accent-colored RUNNING text on the diagram surface
// (labels, titles, readouts — not on a saturated fill). Raw highlight
// (#f97316, 2.68:1) / accent (#0ea5e9, 2.65:1) fail AA as light-mode text;
// 70% hue (the §6 idiom's fraction) still misses at 4.2–4.4:1 for these two
// bright hues, so these mix 60% toward the theme ink
// (light: #a15525/#1473a3, 5.0–5.2:1; dark: #f9b984/#84d3f5, ~10.3:1).
// Shapes keep the raw fills — only text reads these keys.
highlightText: '#a15525',
accentText: '#1473a3',
// Scoped ink for in-cell labels on the paper→highlight heatmap on-ramp.
// The light value is a deep bronze that clears AA across the audited
// mid-band; figures use it only for those labels, never general text.
heatmapMidText: '#7c2d12',
// Primary-accent tint for chips/badges sitting behind --ph-emb ink
// (exposed as diagramTheme.primaryBg): ~10% indigo over transparent.
primaryBg: 'rgba(79, 70, 229, 0.10)',
// Text sitting on a fully-saturated *status* fill (break-red / run-green /
// any deep status hue). Unlike textOnHighlight/textOnAccent (dark in both
// themes, because the orange highlight & sky accent stay mid-tone), the
// status hues flip: DEEP in light (#dc2626/#059669 → white reads) but
// BRIGHTENED in dark (#f87171/#34d399 → dark text reads). So this token
// flips too — white in light, near-black in dark — clearing AA on the
// status fills in both themes. One token so figures stop hard-coding "#fff".
textOnFill: '#ffffff',
// Ink for text sitting ON success/error FILLS — same flip logic as
// textOnFill, scoped per status hue. Resolved from real tokens
// (--diagram-text-on-success/-error in _diagram-variables.scss) so chips,
// heatmap cells, and badges on run-green/break-red stop hand-picking ink.
textOnSuccess: '#ffffff',
textOnError: '#ffffff',
bg: '#fafaf9',
bgSecondary: '#f5f5f4',
// Semantic colors for status/feedback — aliased onto the canonical status/
// phase hues (DESIGN_LANGUAGE.md §2/pillar 2): error≡--pg-break (was
// already byte-identical), success≡--pg-run, info≡--ph-attn (2026-07-11,
// was ad-hoc #16a34a/#2563eb — a "seventh blue" and a second green).
error: '#dc2626',
errorBg: 'rgba(220, 38, 38, 0.1)',
success: '#059669',
successBg: 'rgba(5, 150, 105, 0.1)',
info: '#0284c7',
infoBg: 'rgba(2, 132, 199, 0.1)',
// Pipeline phase palette (DESIGN_LANGUAGE §2) — the site's canonical accent
// grammar, mirrored from the playground north star (playground/ui/kit.js).
// Any diagram that colors a pipeline stage reads its hue from here so lesson
// figures and the debugger share one palette. Status hues (run/break) too.
phaseTok: '#7c3aed',
phaseEmb: '#4f46e5',
phaseAttn: '#0284c7',
phaseMlp: '#0d9488',
phaseNorm: '#64748b',
phaseLogits: '#d97706',
phaseSample: '#e11d48',
run: '#059669',
break: '#dc2626'
};
// Dark-mode fallbacks + brighter semantic colors for readability on dark.
const darkFallbacks = {
nodeFill: '#292524',
nodeFillHover: '#3f3a36',
nodeStroke: '#57534e',
nodeText: '#fafaf9',
edgeStroke: '#a8a29e',
highlight: '#fb923c',
highlightGlow: 'rgba(251, 146, 60, 0.4)',
highlightBg: 'rgba(251, 146, 60, 0.18)',
accent: '#38bdf8',
accentGlow: 'rgba(56, 189, 248, 0.4)',
textOnHighlight: '#1c1917',
textOnAccent: '#1c1917',
// Dark twins: brightened hues mixed 60% toward near-white ink (≈10.3:1 on
// the dark surface).
highlightText: '#f9b984',
accentText: '#84d3f5',
// Dark twin of the scoped heatmap/on-ramp label ink. Near-black umber
// clears the high side of the audited paper→highlight ramp.
heatmapMidText: '#030201',
// Dark twin of primaryBg — the brightened indigo at 18%.
primaryBg: 'rgba(129, 140, 248, 0.18)',
// Dark twin: status fills brighten on dark (#f87171/#34d399), so dark text
// reads on them — the flip described on the light textOnFill above.
textOnFill: '#1c1917',
// Dark twin: brightened status fills (#34d399/#f87171) want dark ink.
textOnSuccess: '#1c1917',
textOnError: '#1c1917',
bg: 'transparent',
bgSecondary: '#1c1917',
error: '#f87171',
errorBg: 'rgba(248, 113, 113, 0.18)',
success: '#34d399',
successBg: 'rgba(52, 211, 153, 0.18)',
info: '#38bdf8',
infoBg: 'rgba(56, 189, 248, 0.18)',
// Phase palette — dark twin (brighter for legibility on dark surfaces),
// mirroring the playground's .quarto-dark --ph-*/--pg-* values exactly.
phaseTok: '#a78bfa',
phaseEmb: '#818cf8',
phaseAttn: '#38bdf8',
phaseMlp: '#2dd4bf',
phaseNorm: '#94a3b8',
phaseLogits: '#fbbf24',
phaseSample: '#fb7185',
run: '#34d399',
break: '#f87171'
};
// Referencing isDarkMode here makes this cell reactive: it recomputes (and all
// diagrams that read it re-render) whenever the theme is toggled.
const fallbacks = isDarkMode ? darkFallbacks : lightFallbacks;
// Resolved once so `bgOpaque` can be derived from them (an object literal
// can't reference its own siblings).
const bg = getCSSVar('--diagram-bg', fallbacks.bg);
const bgSecondary = getCSSVar('--diagram-bg-secondary', fallbacks.bgSecondary);
// bgOpaque hoisted to a const so bgRGB can derive from it (an object literal
// can't reference its own siblings — same constraint as bg/bgSecondary).
const bgOpaque = (!bg || bg === 'transparent' || bg === 'none') ? bgSecondary : bg;
return {
nodeFill: getCSSVar('--diagram-node-fill', fallbacks.nodeFill),
nodeFillHover: getCSSVar('--diagram-hover-fill', fallbacks.nodeFillHover),
nodeStroke: getCSSVar('--diagram-node-stroke', fallbacks.nodeStroke),
nodeText: getCSSVar('--diagram-node-text', fallbacks.nodeText),
edgeStroke: getCSSVar('--diagram-edge-stroke', fallbacks.edgeStroke),
highlight: getCSSVar('--diagram-highlight', fallbacks.highlight),
highlightGlow: getCSSVar('--diagram-highlight-glow', fallbacks.highlightGlow),
highlightBg: getCSSVar('--diagram-highlight-bg', fallbacks.highlightBg),
accent: getCSSVar('--diagram-accent', fallbacks.accent),
accentGlow: getCSSVar('--diagram-accent-glow', fallbacks.accentGlow),
textOnHighlight: fallbacks.textOnHighlight,
textOnAccent: fallbacks.textOnAccent,
highlightText: getCSSVar('--diagram-highlight-text', fallbacks.highlightText),
accentText: getCSSVar('--diagram-accent-text', fallbacks.accentText),
heatmapMidText: getCSSVar('--diagram-heatmap-mid-text', fallbacks.heatmapMidText),
textOnFill: fallbacks.textOnFill,
textOnSuccess: getCSSVar('--diagram-text-on-success', fallbacks.textOnSuccess),
textOnError: getCSSVar('--diagram-text-on-error', fallbacks.textOnError),
bg,
bgSecondary,
// `bg`, but guaranteed PAINTABLE. `--diagram-bg` is deliberately
// `transparent` in dark mode so a figure sits *on* the article surface
// instead of stamping a light slab onto it — which is right for a figure's
// own background rect, and wrong everywhere `bg` is used as *ink*:
//
// * a mark's **halo ring** (`<circle stroke=theme.bg stroke-width=2>`),
// drawn so a point stays legible where it crosses its own line — with a
// transparent stroke there is no ring at all;
// * **knockout text** (`<text fill=theme.bg>`), the page-colored label on
// an active/filled node — with a transparent fill the label is *invisible*.
//
// Both silently broke in dark and were fine in light, which is why they
// survived eight page sweeps. This token keeps `bg` when it is paintable and
// falls back to the opaque `bgSecondary` (#f5f5f4 light / #1c1917 dark) when
// it is not, so ink survives both themes. Rule: a figure's own background
// rect fills `bg`; anything that PAINTS (stroke or text fill) uses
// `bgOpaque` — never `theme.bg`, never a bare "#fff".
bgOpaque,
// Precomputed {r,g,b} of bgOpaque, for color ramps/interpolators that
// would otherwise re-parse a CSS string — and BREAK in dark, where bg is
// 'transparent' (parse → NaN → a near-white ramp). Always the OPAQUE,
// paintable bg: interpolate from this, never from theme.bg.
// Usage: d3.interpolateRgb(d3.rgb(theme.bgRGB), d3.rgb(theme.accent))
bgRGB: _parseColor(bgOpaque) || { r: 245, g: 245, b: 244 },
// Semantic colors — now real --diagram-* tokens (2026-07-11), aliased onto
// --pg-break/--pg-run/--ph-attn at the shared layer so they're overridable
// and theme-reactive the same way every other diagram color is.
error: getCSSVar('--diagram-error', fallbacks.error),
errorBg: getCSSVar('--diagram-error-bg', fallbacks.errorBg),
success: getCSSVar('--diagram-success', fallbacks.success),
successBg: getCSSVar('--diagram-success-bg', fallbacks.successBg),
info: getCSSVar('--diagram-info', fallbacks.info),
infoBg: getCSSVar('--diagram-info-bg', fallbacks.infoBg),
// Primary accent — the site's canonical UI accent (embeddings indigo,
// DESIGN_LANGUAGE §2: `--accent-primary ≈ --ph-emb`). Several lessons
// (m01/m04/m06) already read `theme.primary` for their featured series, but
// the token was never defined here, so it resolved to `undefined` and those
// marks fell back to browser-default black in both themes. Defining it fixes
// that everywhere, theme-reactively.
primary: getCSSVar('--ph-emb', fallbacks.phaseEmb),
// Chip/badge background behind the primary accent — the resolved
// --diagram-primary-bg token (see _diagram-variables.scss).
primaryBg: getCSSVar('--diagram-primary-bg', fallbacks.primaryBg),
// Pipeline phase palette + status hues. Read from the canonical --ph-*/--pg-*
// tokens where present (they get seeded into custom.scss as the shared token
// work lands), else the phase fallbacks above — so a figure is on-palette and
// theme-reactive whether or not the site tokens exist yet. Grouped under
// `phase` (matching playground/ui/kit.js) plus flat run/break status hues.
phase: {
tok: getCSSVar('--ph-tok', fallbacks.phaseTok),
emb: getCSSVar('--ph-emb', fallbacks.phaseEmb),
attn: getCSSVar('--ph-attn', fallbacks.phaseAttn),
mlp: getCSSVar('--ph-mlp', fallbacks.phaseMlp),
norm: getCSSVar('--ph-norm', fallbacks.phaseNorm),
logits: getCSSVar('--ph-logits', fallbacks.phaseLogits),
sample: getCSSVar('--ph-sample', fallbacks.phaseSample)
},
run: getCSSVar('--pg-run', fallbacks.run),
break: getCSSVar('--pg-break', fallbacks.break),
isDark: isDarkMode
};
}
// =============================================================================
// TEXT MEASUREMENT & FITTING
// =============================================================================
// Canvas-backed text measurement shared by every fitting helper. The 2d
// context is created lazily and measured widths are memoized per (text|font)
// key. Falls back to a conservative 0.62em-per-glyph estimate when canvas
// measurement is unavailable, so callers always get a usable number.
// Holder object for the measurement machinery: OJS binds top-level assigned
// names as single-definition (const) cells, so the lazy canvas-context init
// and the broken-canvas flag must mutate PROPERTIES of this holder rather
// than reassign top-level bindings (that throws "Assignment to constant
// variable" at compile time).
// IIFE wrapper: a bare `name = {…}` object literal would parse as an OJS
// BLOCK CELL; the parens force expression parsing.
_measure = (() => ({ctx: null, broken: false, cache: new Map()}))()
// Resolve a CSS font-family that may be written as `var(--pg-mono)`: canvas
// font strings can't use custom properties, so the token is read off <body>
// (getCSSVar), exactly like every other theme read in this file.
_resolveFontFamily = function(family) {
if (!family) return null;
let f = String(family).trim();
const varMatch = f.match(/^var\(\s*(--[\w-]+)\s*(?:,\s*([^)]*))?\)$/);
if (varMatch) {
const fallback = varMatch[2] != null ? varMatch[2].replace(/["']/g, '').trim() : null;
f = getCSSVar(varMatch[1], fallback) || '';
}
f = f.replace(/!important/g, '').trim().replace(/^["']|["']$/g, '');
return f || null;
}
// Measured (or estimated) rendered width of a single-line text in px.
// Options: {fontSize = 12, fontWeight = 400, fontFamily = null, italic = false}
// fontFamily may be `var(--token)`; null measures in the body's font.
measureText = function(text, {fontSize = 12, fontWeight = 400, fontFamily = null, italic = false} = {}) {
if (text == null || text === '') return 0;
const len = String(text).length;
const family = _resolveFontFamily(fontFamily);
// Monospace chrome is the site's diagram font (--pg-mono → JetBrains Mono,
// a 0.600em-advance face). Canvas measurement races webfont loading: OJS
// cells evaluate before @font-face fonts finish, and canvas then reports
// FALLBACK-face metrics (much narrower), silently under-sizing boxes,
// gutters and measured legends. For mono families use the face's exact
// design advance — deterministic and load-independent.
if (family && /mono/i.test(family)) return 0.6 * fontSize * len;
// Proportional faces: canvas is exact ONLY if the webfont is actually
// loaded. If it isn't yet, canvas would silently return fallback metrics —
// return a conservative per-glyph estimate instead.
const firstFace = family ? family.split(',')[0].replace(/["']/g, '').trim() : null;
const fontReady = typeof document !== 'undefined' && document.fonts && document.fonts.check &&
(!firstFace || document.fonts.check(`${italic ? 'italic ' : ''}${fontWeight} ${fontSize}px "${firstFace}"`));
if (typeof document !== 'undefined' && !fontReady) return 0.55 * fontSize * len;
const key = `${text}\u0000${fontSize}\u0000${fontWeight}\u0000${fontFamily || ''}\u0000${italic ? 1 : 0}`;
if (_measure.cache.has(key)) return _measure.cache.get(key);
let width = NaN;
if (typeof document !== 'undefined' && !_measure.broken) {
try {
if (!_measure.ctx) _measure.ctx = document.createElement('canvas').getContext('2d');
if (_measure.ctx) {
const family = _resolveFontFamily(fontFamily) ||
(typeof getComputedStyle === 'function'
? _resolveFontFamily(getComputedStyle(document.body).fontFamily)
: null);
_measure.ctx.font = `${italic ? 'italic ' : ''}${fontWeight} ${fontSize}px ${family || 'sans-serif'}`;
width = _measure.ctx.measureText(String(text)).width;
} else {
_measure.broken = true;
}
} catch (e) {
_measure.broken = true;
}
}
if (!isFinite(width) || width <= 0) {
width = 0.62 * fontSize * String(text).length;
}
_measure.cache.set(key, width);
return width;
}
// Convenience wrapper with positional args: estimateTextWidth(text, fontSize,
// mono). mono=true (the default) measures in the site's mono stack — what most
// diagram chrome uses; mono=false measures in the body text face.
estimateTextWidth = function(text, fontSize = 12, mono = true) {
const family = mono
? getCSSVar('--pg-mono', 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace')
: null;
return measureText(text, {fontSize, fontFamily: family});
}
// Largest font size ≤ fontSize whose rendered width fits maxWidth. Never goes
// below minFontSize; if even that would overflow, the caller must wrap
// (wrapText/drawWrappedText) or move the text (placeLabel) instead.
fitFontSize = function(text, {maxWidth, fontSize = 12, minFontSize = 9, fontWeight, fontFamily, italic} = {}) {
const w = measureText(text, {fontSize, fontWeight, fontFamily, italic});
if (w <= 0 || w <= maxWidth) return fontSize;
return Math.max(minFontSize, Math.floor((fontSize * maxWidth / w) * 2) / 2);
}
// Greedy word-wrap into lines that each fit maxWidth (px). A single word wider
// than maxWidth is hard-broken at character level, so every returned line
// fits. Returns an array of strings ([] for empty input).
wrapText = function(text, maxWidth, {fontSize = 12, fontWeight, fontFamily, italic} = {}) {
if (text == null) return [];
const words = String(text).split(/\s+/).filter(Boolean);
if (!words.length) return [];
const lines = [];
let line = '';
const fits = (s) => !s || measureText(s, {fontSize, fontWeight, fontFamily, italic}) <= maxWidth;
for (const word of words) {
const candidate = line ? line + ' ' + word : word;
if (fits(candidate)) { line = candidate; continue; }
if (line) { lines.push(line); line = ''; }
if (fits(word)) { line = word; continue; }
let piece = '';
for (const ch of word) {
if (piece && !fits(piece + ch)) { lines.push(piece); piece = ch; }
else piece += ch;
}
line = piece;
}
if (line) lines.push(line);
return lines;
}
// Character-count variant of wrapText for mono captions sized in characters.
wrapByChars = function(text, maxChars = 60) {
if (text == null) return [];
const words = String(text).split(/\s+/).filter(Boolean);
const lines = [];
let line = '';
for (const word of words) {
const candidate = line ? line + ' ' + word : word;
if (candidate.length <= maxChars) { line = candidate; continue; }
if (line) { lines.push(line); line = ''; }
let rest = word;
while (rest.length > maxChars) {
lines.push(rest.slice(0, maxChars));
rest = rest.slice(maxChars);
}
line = rest;
}
if (line) lines.push(line);
return lines;
}
// Wrap a caption and draw it as tspans on ONE <text> element — the cure for
// long single-line captions that run past the canvas edge (they clip instead
// of wrapping). Wraps by measured width when maxWidth (px) is given, else by
// maxChars; caps at maxLines with a trailing ellipsis when the text doesn't
// fit. Returns {node, lines, width, height} so callers can reserve room.
// Usage: drawWrappedText(svg, 'The token embedding E is (V × d) …', {x: cx,
// y: 250, maxWidth: width - 40, lineHeight: 14, maxLines: 2})
drawWrappedText = function(svg, text, {
x, y,
maxWidth = null, maxChars = null,
lineHeight = 14,
fontSize = 11,
fontWeight = null,
fontFamily = null,
fill = null,
anchor = 'middle',
maxLines = null,
id = null,
className = 'diagram-caption'
} = {}) {
const sel = svg && svg.node ? svg : d3.select(svg);
const str = text == null ? '' : String(text);
let lines = [];
if (str) {
lines = maxWidth != null
? wrapText(str, maxWidth, {fontSize, fontWeight, fontFamily})
: wrapByChars(str, maxChars == null ? 60 : maxChars);
if (maxLines != null && lines.length > maxLines) {
lines = lines.slice(0, maxLines);
const last = lines[lines.length - 1];
lines[lines.length - 1] = last.slice(0, Math.max(0, last.length - 1)).replace(/\s+\S*$/, '') + '…';
}
}
const t = sel.append('text')
.attr('x', x)
.attr('y', y)
.attr('text-anchor', anchor)
.attr('fill', fill != null ? fill : diagramTheme.nodeText)
.style('font-size', `${fontSize}px`)
.attr('class', className);
if (fontWeight != null) t.attr('font-weight', fontWeight);
if (fontFamily != null) t.attr('font-family', fontFamily);
if (id) t.attr('id', id);
lines.forEach((ln, i) => {
t.append('tspan')
.attr('x', x)
.attr('y', y + i * lineHeight)
.text(ln);
});
const width = lines.reduce((m, ln) => Math.max(m, measureText(ln, {fontSize, fontWeight, fontFamily})), 0);
return {node: t.node(), lines, width, height: lines.length ? (lines.length - 1) * lineHeight + fontSize * 1.25 : 0};
}
// Keep an anchored text run inside the canvas gutter. An end-anchored label at
// x=30 extends LEFT of its anchor, a start-anchored one extends RIGHT — long
// runs then start before 0 (or past the viewBox edge) and get clipped.
// Measures the text and returns the anchor x shifted so the whole run stays
// inside [min, max]. Text wider than the entire gutter is pinned to the
// nearest gutter edge (the caller can then shrink the font via fitFontSize).
// anchor: 'start' | 'middle' | 'end'.
fitGutter = function(text, x, {
anchor = 'end',
fontSize = 10,
fontWeight = null,
fontFamily = null,
min = 4,
max = null
} = {}) {
const w = measureText(text, {fontSize, fontWeight, fontFamily});
const lo = min, hi = max == null ? Infinity : max;
if (w > hi - lo) return Math.min(Math.max(x, lo), hi);
const left = anchor === 'end' ? x - w : anchor === 'middle' ? x - w / 2 : x;
const clampedLeft = Math.min(Math.max(left, lo), hi - w);
return anchor === 'end' ? clampedLeft + w
: anchor === 'middle' ? clampedLeft + w / 2
: clampedLeft;
}
// Perceptual ink choice for text sitting ON a filled shape (heatmap cells,
// status chips, badge fills): returns a near-black ink on light/bright fills
// and a white ink on dark fills, so cell values stay legible in BOTH themes.
// The 0.30 luminance threshold reproduces the site's own tokens: orange
// #f97316 and sky #0ea5e9 read as "bright" → dark ink (matching
// textOnHighlight/textOnAccent), while break-red #dc2626 reads as "dark" →
// white ink (matching textOnFill in light). Accepts #rgb/#rrggbb, rgb()/rgba()
// and var(--token) forms.
contrastInk = function(bg, {darkInk = '#1c1917', lightInk = '#ffffff', threshold = 0.30} = {}) {
const rgb = _parseColor(bg);
if (!rgb) return darkInk;
const lin = (v) => {
v /= 255;
return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
};
const L = 0.2126 * lin(rgb.r) + 0.7152 * lin(rgb.g) + 0.0722 * lin(rgb.b);
return L > threshold ? darkInk : lightInk;
}
// =============================================================================
// COLLISION-AWARE LABEL PLACEMENT
// =============================================================================
// Knockout outline behind glyphs: paint-order renders the stroke first (under
// the fill), in the figure-background color, so text stays legible where an
// arrow or another label passes underneath. Color reads --diagram-halo
// (theme-aware, opaque in both modes; see _diagram-variables.scss) with
// theme.bgOpaque as fallback.
applyHalo = function(text, color = null) {
const sel = text && text.node ? text : d3.select(text);
return sel
.style('paint-order', 'stroke')
.style('stroke', color || getCSSVar('--diagram-halo', diagramTheme.bgOpaque))
.style('stroke-width', '3px')
.style('stroke-linejoin', 'round');
}
// Harvest the drawn shapes of a rendered svg as rectangles in USER units, for
// use as collision obstacles. Full-canvas background slabs are dropped
// automatically (they would push every label out of the figure); everything
// else — node boxes, other labels, arrow paths — becomes an obstacle.
// `svg` may be a d3 selection or an <svg> element. Elements can be excluded
// per-element via a `data-no-obstacle` attribute or via the `exclude`
// selector(s).
collectObstacles = function(svg, {exclude = null} = {}) {
const node = svg && svg.node ? svg.node() : svg;
if (!node || typeof node.getScreenCTM !== 'function' || typeof node.querySelectorAll !== 'function') return [];
const ctm = node.getScreenCTM();
if (!ctm) return [];
const inv = ctm.inverse();
const vb = node.viewBox && node.viewBox.baseVal;
const vbW = (vb && vb.width) || 0, vbH = (vb && vb.height) || 0;
const excludes = exclude == null ? [] : (Array.isArray(exclude) ? exclude : [exclude]);
const isExcluded = (el) => {
if (el.closest('[data-no-obstacle]')) return true;
return excludes.some(sel => {
try { return el.matches(sel) || el.closest(sel); } catch (e) { return false; }
});
};
const toUser = (clientX, clientY) => {
const pt = node.createSVGPoint();
pt.x = clientX;
pt.y = clientY;
const p = pt.matrixTransform(inv);
return {x: p.x, y: p.y};
};
const out = [];
for (const sel of ['rect', 'text', 'path']) {
for (const el of node.querySelectorAll(sel)) {
if (isExcluded(el)) continue;
const r = el.getBoundingClientRect();
if (!r.width && !r.height) continue;
const a = toUser(r.left, r.top);
const b = toUser(r.right, r.bottom);
const x = Math.min(a.x, b.x), y = Math.min(a.y, b.y);
const w = Math.abs(b.x - a.x), h = Math.abs(b.y - a.y);
if (vbW && vbH && w >= vbW * 0.9 && h >= vbH * 0.9) continue; // background slab
out.push({x, y, w, h});
}
}
return out;
}
// Box a text run into a top-left rect, given its anchor point.
_anchorBox = function(x, y, w, h, anchor) {
const left = anchor === 'start' ? x : anchor === 'end' ? x - w : x - w / 2;
return {x: left, y: y - h / 2, w, h};
}
_hitsAny = function(box, obstacles, margin = 3) {
for (const o of obstacles) {
if (box.x < o.x + o.w + margin && box.x + box.w + margin > o.x &&
box.y < o.y + o.h + margin && box.y + box.h + margin > o.y) return true;
}
return false;
}
// Approximate an SVG path with small collision boxes in user units. These are
// deliberately sampled more densely than the 10px captions: the resolver's
// own 3px collision margin then treats each arrow stroke as a narrow,
// continuous obstacle without needing a browser hit-test for every candidate.
_pathObstacleRects = function(path, spacing = 4, pad = 2) {
if (!path || typeof path.getTotalLength !== 'function' || typeof path.getPointAtLength !== 'function') return [];
let length;
try { length = path.getTotalLength(); } catch (e) { return []; }
if (!isFinite(length) || length <= 0) return [];
try {
const out = [];
let prev = path.getPointAtLength(0);
for (let d = Math.min(spacing, length); d <= length + spacing; d += spacing) {
const next = path.getPointAtLength(Math.min(d, length));
out.push({
x: Math.min(prev.x, next.x) - pad,
y: Math.min(prev.y, next.y) - pad,
w: Math.abs(next.x - prev.x) + pad * 2,
h: Math.abs(next.y - prev.y) + pad * 2
});
prev = next;
if (d >= length) break;
}
return out;
} catch (e) {
return [];
}
}
_svgBounds = function(node) {
if (!node || !node.viewBox) return null;
const vb = node.viewBox.baseVal;
if (vb && vb.width && vb.height) {
return {x1: vb.x, y1: vb.y, x2: vb.x + vb.width, y2: vb.y + vb.height};
}
const w = parseFloat(node.getAttribute('width')), h = parseFloat(node.getAttribute('height'));
if (isFinite(w) && isFinite(h)) return {x1: 0, y1: 0, x2: w, y2: h};
return null;
}
// Search a small spiral of candidate offsets for a spot where the label's box
// neither hits an obstacle nor leaves the viewBox. Deterministic: candidates
// are tried in a fixed order (base, then sideways, then vertical, outward),
// so identical input always produces identical placement. Falls back to the
// requested position when nothing is clear — the caller's halo, if any, keeps
// it legible there.
_resolveLabelPos = function({x, y, w, h, anchor = 'middle', obstacles, bounds = null, step = 12, reach = 4, margin = 3}) {
if (!obstacles || !obstacles.length) return {x, y};
const hits = (cx, cy) => _hitsAny(_anchorBox(cx, cy, w, h, anchor), obstacles, margin);
const inside = (cx, cy) => {
if (!bounds) return true;
const b = _anchorBox(cx, cy, w, h, anchor);
return b.x >= bounds.x1 - 1 && b.y >= bounds.y1 - 1 &&
b.x + b.w <= bounds.x2 + 1 && b.y + b.h <= bounds.y2 + 1;
};
if (!hits(x, y) && inside(x, y)) return {x, y};
for (let k = 1; k <= reach; k++) {
for (const d of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
const cx = x + d[0] * k * step, cy = y + d[1] * k * step;
if (!hits(cx, cy) && inside(cx, cy)) return {x: cx, y: cy};
}
}
return {x, y};
}
// Create a free-standing text label with measured size, optional collision
// avoidance, and an optional knockout halo. Returns {node, x, y, width,
// height} — the FINAL placed position, so callers can draw connectors to it.
// avoid: array of {x,y,w,h} rects, or an svg (element/selection) whose drawn
// shapes are harvested as obstacles. Usage: placeLabel(svg, {x: 320, y: 210,
// text: 'softmax(dim=-1)', avoid: svg, halo: true})
placeLabel = function(svg, {
x, y, text,
dx = 0, dy = 0,
anchor = 'middle',
fontSize = 10,
fontWeight = null,
fill = null,
opacity = null,
fontFamily = null,
halo = true,
haloColor = null,
avoid = null,
exclude = null,
step = 12, reach = 4,
id = null,
className = 'diagram-label'
} = {}) {
const sel = svg && svg.node ? svg : d3.select(svg);
const node = sel.node();
const w = measureText(text, {fontSize, fontWeight, fontFamily});
const h = fontSize * 1.3;
let obstacles = null;
if (avoid) obstacles = Array.isArray(avoid) ? avoid : collectObstacles(avoid, {exclude});
const baseX = x + dx, baseY = y + dy;
const pos = obstacles
? _resolveLabelPos({x: baseX, y: baseY, w, h, anchor, obstacles, bounds: _svgBounds(node), step, reach})
: {x: baseX, y: baseY};
const t = sel.append('text')
.attr('x', pos.x)
.attr('y', pos.y)
.attr('text-anchor', anchor)
.attr('dominant-baseline', 'central')
.attr('font-size', `${fontSize}px`)
.style('font-size', `${fontSize}px`)
.attr('pointer-events', 'none')
.attr('class', className)
.text(text);
if (fontWeight != null) t.attr('font-weight', fontWeight);
t.attr('fill', fill != null ? fill : diagramTheme.nodeText);
if (opacity != null) t.attr('opacity', opacity);
if (fontFamily != null) t.attr('font-family', fontFamily);
if (id) t.attr('id', id);
if (halo) applyHalo(t, haloColor);
return {node: t.node(), x: pos.x, y: pos.y, width: w, height: h};
}
// =============================================================================
// FIGURE FIT & SCROLL (anti-clipping)
// =============================================================================
// Expand an svg's viewBox just enough to contain everything actually drawn
// (labels, markers, captions included) — the cure for content that overflows
// the authored bounds and gets clipped at the bottom or right edge. Only ever
// GROWS the viewBox (a figure that already fits is left untouched) and grows
// the width/height attributes with the new aspect so the glyph scale (px per
// user unit) is preserved. Returns true when the viewBox changed. Safe on
// detached svgs (no layout yet → returns false) and after the final render
// step of a stepped diagram.
fitContent = function(svg, {pad = 6} = {}) {
const node = svg && svg.node ? svg.node() : svg;
if (!node || typeof node.getBBox !== 'function') return false;
let b;
try { b = node.getBBox(); } catch (e) { return false; }
if (!b || (!b.width && !b.height)) return false;
const vb = node.viewBox && node.viewBox.baseVal;
const hasVB = !!(vb && vb.width && vb.height);
let vx = 0, vy = 0, vw = 0, vh = 0;
if (hasVB) { vx = vb.x; vy = vb.y; vw = vb.width; vh = vb.height; }
else {
const r = node.getBoundingClientRect();
vw = r.width || 0;
vh = r.height || 0;
}
if (!vw || !vh) return false;
const left = Math.min(vx, b.x - pad), top = Math.min(vy, b.y - pad);
const right = Math.max(vx + vw, b.x + b.width + pad);
const bottom = Math.max(vy + vh, b.y + b.height + pad);
const nw = right - left, nh = bottom - top;
if (nw <= vw + 0.5 && nh <= vh + 0.5) return false;
node.setAttribute('viewBox', `${+left.toFixed(1)} ${+top.toFixed(1)} ${+nw.toFixed(1)} ${+nh.toFixed(1)}`);
// Preserve px-per-unit so text does not shrink when the box grows: scale
// the painted size to the new aspect. CSS-sized svgs (e.g. inside
// .diagram-container) track the viewBox ratio automatically via height:auto.
const attrNum = (name) => {
const v = node.getAttribute(name);
if (!v || v.includes('%')) return NaN;
const n = parseFloat(v);
return isFinite(n) ? n : NaN;
};
const wAttr = attrNum('width'), hAttr = attrNum('height');
const scale = hasVB
? (isFinite(wAttr) ? wAttr / vw : (isFinite(hAttr) ? hAttr / vh : null))
: 1;
if (isFinite(scale) && scale > 0) {
node.setAttribute('width', +(nw * scale).toFixed(1));
node.setAttribute('height', +(nh * scale).toFixed(1));
}
return true;
}
// fitContent for every svg under root (a selector string, element, or d3
// selection). Figures can opt out with the class `diagram-no-fit`. Call once
// after a figure's last render step; returns the number of svgs adjusted.
// Usage: fitAll(cellRoot) // after the final step's draw calls
fitAll = function(root, opts = {}) {
let el = root && root.node ? root.node() : root;
if (typeof el === 'string') el = document.querySelector(el);
if (!el || !el.querySelectorAll) return 0;
let n = 0;
el.querySelectorAll('svg').forEach(s => {
if (s.classList.contains('diagram-no-fit')) return;
if (fitContent(s, opts)) n++;
});
return n;
}
// Wrap a figure for display: tags the svg `.diagram-fig` (so the site's
// blanket `svg text { fill: … !important }` resets in custom.scss stop
// flattening the figure's own theme-aware fills) and puts it inside a
// horizontally scrollable box (`.diagram-scroll`) that never exceeds the page
// column — a wide diagram scrolls INSIDE its container instead of widening
// the page or being squeezed illegibly (DESIGN_LANGUAGE.md §5). The svg keeps
// its authored pixel width (inline min-width): narrower column → the
// container scrolls; wider column → the figure sits at its natural size.
// Options: {minWidth = null, maxHeight = null, className = ''}. `maxHeight`
// caps the box height and scrolls vertically instead — for tall figures
// (heatmap stacks) that would otherwise run past the viewport.
// Usage: display(scrollWrap(svg.node())) // wide figure → x-scroll
// display(scrollWrap(svg.node(), {maxHeight: 420})) // tall → y-scroll
scrollWrap = function(svg, {minWidth = null, maxHeight = null, className = ''} = {}) {
const node = svg && svg.node ? svg.node() : svg;
if (node) {
node.classList.add('diagram-fig');
let w = minWidth;
if (w == null) {
const attr = node.getAttribute('width');
if (attr && !attr.includes('%')) w = parseFloat(attr);
else {
const vb = node.viewBox && node.viewBox.baseVal;
w = vb && vb.width ? vb.width : null;
}
}
if (w) node.style.minWidth = `${w}px`;
}
const cls = `diagram-scroll${className ? ' ' + className : ''}`;
let wrapper;
try {
wrapper = htl.html`<div class="${cls}">${node}</div>`;
} catch (e) {
wrapper = document.createElement('div');
wrapper.className = cls;
if (node) wrapper.appendChild(node);
}
// Apply the cap to the actual scroller: htl returns the root element, but
// some environments wrap it — find the .diagram-scroll either way.
const scroller = (wrapper.classList && wrapper.classList.contains('diagram-scroll'))
? wrapper
: ((wrapper.querySelector && wrapper.querySelector('.diagram-scroll')) || wrapper);
if (maxHeight != null) {
scroller.style.maxHeight = `${maxHeight}px`;
scroller.style.overflowY = 'auto';
}
return wrapper;
}
// =============================================================================
// SVG PRIMITIVES
// =============================================================================
// Creates a group with rounded rect and text.
// Options: {x, y, width, height, label, sublabel, id, theme, rx, ry, className,
// fontFamily}
// Text fitting (additive; existing options keep their meaning):
// * width/height omitted → the box auto-fits its text, never narrower/taller
// than the historical 100×50 default, so legacy call sites keep geometry.
// * width given but the label is wider than the box → the label font shrinks
// (floor 9px, sublabel proportionally) to stay INSIDE the box instead of
// spilling over its edges ("Multi-Head Attention" in a 100px node).
// The resolved box is attached as the group's datum: g.datum() → {width,
// height, fontSize, subFontSize} — read it when positioning connectors.
createNode = function(svg, options) {
const {
x = 0,
y = 0,
width = null,
height = null,
label = '',
sublabel = '',
id = null,
theme = diagramTheme,
rx = 6,
ry = 6,
className = 'diagram-node',
fontFamily = null
} = options;
// --- text fitting ---------------------------------------------------------
// Measure in the font the text will actually render in: an explicit
// fontFamily option wins; else the svg's inline font-family (often
// `var(--pg-mono)` on lesson figures — mono is wider, so measuring in the
// body face would under-size the box); else the body font.
const padX = 12;
const family = fontFamily || (svg && svg.style ? svg.style('font-family') : null) || null;
const labelW = label ? measureText(label, {fontSize: 12, fontWeight: 500, fontFamily: family}) : 0;
const subW = sublabel ? measureText(sublabel, {fontSize: 10, fontFamily: family}) : 0;
let w, h, fontSize = 12, subFontSize = 10;
if (width == null) {
// Auto-fit: grow to the text, never below the historical 100px default.
w = Math.max(100, labelW + padX * 2, subW + 16);
} else {
// Authored box: keep its geometry, shrink the type to fit inside.
w = width;
const avail = Math.max(w - padX * 2, 20);
if (labelW > avail) {
fontSize = fitFontSize(label, {maxWidth: avail, fontSize: 12, minFontSize: 9, fontWeight: 500, fontFamily: family});
subFontSize = Math.max(7.5, 10 * fontSize / 12);
}
}
h = height == null ? 50 : height;
// Create group
const g = svg.append('g')
.attr('class', className)
.attr('transform', `translate(${x}, ${y})`);
if (id) g.attr('id', id);
// Add rectangle
g.append('rect')
.attr('x', -w / 2)
.attr('y', -h / 2)
.attr('width', w)
.attr('height', h)
.attr('rx', rx)
.attr('ry', ry)
.attr('fill', theme.nodeFill)
.attr('stroke', theme.nodeStroke)
.attr('stroke-width', 1.5);
// Add main label
if (label) {
const labelY = sublabel ? -6 : 0;
g.append('text')
.attr('x', 0)
.attr('y', labelY)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'central')
.attr('fill', theme.nodeText)
.style('font-size', `${fontSize}px`) // inline style wins over the `.diagram-node text` CSS rule when shrinking
.attr('font-weight', '500')
.attr('pointer-events', 'none')
.text(label);
}
// Add sublabel
if (sublabel) {
g.append('text')
.attr('x', 0)
.attr('y', 10)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'central')
.attr('fill', theme.nodeText)
.style('font-size', `${subFontSize}px`)
.attr('opacity', 0.7)
.attr('pointer-events', 'none')
.text(sublabel);
}
// Publish the resolved box (additive; harmless to ignore).
g.datum({width: w, height: h, fontSize, subFontSize});
return g;
}
// Creates a path with arrowhead marker
// Options: {x1, y1, x2, y2, label, theme, curved, curvature, id, className,
// dashed, labelOffset, labelDx, labelDy, labelAnchor, halo, avoid,
// exclude}
createArrow = function(svg, options) {
const {
x1 = 0,
y1 = 0,
x2 = 100,
y2 = 0,
label = '',
theme = diagramTheme,
curved = false,
curvature = 0.3,
id = null,
className = 'diagram-edge',
dashed = false,
// Additive label options — every default preserves the original geometry:
labelOffset = 12, // perpendicular distance of the label from the line
labelDx = 0, // extra offset AFTER the perpendicular shift
labelDy = 0,
labelAnchor = 'middle',
halo = false, // knockout outline (--diagram-halo) behind the label
avoid = null, // dodge obstacles: rects[] or an svg (element/selection)
exclude = null // when harvesting, skip elements matching these selectors
} = options;
// Create unique marker ID
const markerId = `arrow-${Math.random().toString(36).substr(2, 9)}`;
// Ensure defs exists
let defs = svg.select('defs');
if (defs.empty()) {
defs = svg.append('defs');
}
// Add arrowhead marker
defs.append('marker')
.attr('id', markerId)
.attr('viewBox', '0 -5 10 10')
.attr('refX', 8)
.attr('refY', 0)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', theme.edgeStroke);
// Create group for arrow
const g = svg.append('g')
.attr('class', className);
if (id) g.attr('id', id);
// Calculate path
let pathD;
if (curved) {
// Quadratic Bezier curve
const midX = (x1 + x2) / 2;
const midY = (y1 + y2) / 2;
const dx = x2 - x1;
const dy = y2 - y1;
// Perpendicular offset for curve
const cx = midX - dy * curvature;
const cy = midY + dx * curvature;
pathD = `M${x1},${y1} Q${cx},${cy} ${x2},${y2}`;
} else {
// Straight line
pathD = `M${x1},${y1} L${x2},${y2}`;
}
// Add path
const path = g.append('path')
.attr('d', pathD)
.attr('fill', 'none')
.attr('stroke', theme.edgeStroke)
.attr('stroke-width', 1.5)
.attr('marker-end', `url(#${markerId})`);
if (dashed) {
path.attr('stroke-dasharray', '5,3');
}
// Add label if provided. Geometry when no additive option is given:
// midpoint of the line shifted 12px perpendicular — exactly as before.
if (label) {
const mx = (x1 + x2) / 2;
const my = (y1 + y2) / 2;
// Offset label perpendicular to the line
const angle = Math.atan2(y2 - y1, x2 - x1);
const nx = Math.sin(angle), ny = -Math.cos(angle);
let lx = mx + nx * labelOffset + labelDx;
let ly = my + ny * labelOffset + labelDy;
// Collision avoidance (opt-in): nudge the label until its box clears every
// obstacle, so edge captions stop sitting on nodes, other labels, or other
// arrows. avoid: rects[] or an svg (element/selection) to harvest.
if (avoid) {
const obstacles = Array.isArray(avoid) ? avoid : collectObstacles(avoid, {exclude});
const tw = measureText(label, {fontSize: 10, fontFamily: (svg && svg.style ? svg.style('font-family') : null) || null});
const placed = _resolveLabelPos({
x: lx, y: ly, w: tw, h: 13, anchor: labelAnchor,
obstacles, bounds: _svgBounds(svg.node()), step: 12, reach: 4
});
lx = placed.x;
ly = placed.y;
}
const labelText = g.append('text')
.attr('x', lx)
.attr('y', ly)
.attr('text-anchor', labelAnchor)
.attr('dominant-baseline', 'central')
.attr('fill', theme.nodeText)
.style('font-size', '10px') // inline: wins over the `.diagram-edge text` CSS rule
.text(label);
if (halo) applyHalo(labelText);
}
return g;
}
// =============================================================================
// STEP ANIMATION CONTROLLER
// =============================================================================
// Factory function returning controller for step-through animations
// Options: {total, initialStep, speed, loop, onStepChange}
createStepController = function(options = {}) {
const {
total = 1,
initialStep = 0,
speed = 1000,
loop = true,
onStepChange = null
} = options;
let current = initialStep;
let isPlaying = false;
let intervalId = null;
let currentSpeed = speed;
const notifyChange = () => {
if (onStepChange && typeof onStepChange === 'function') {
onStepChange(current);
}
};
const controller = {
get current() { return current; },
get isPlaying() { return isPlaying; },
get total() { return total; },
get speed() { return currentSpeed; },
setStep(step) {
current = Math.max(0, Math.min(total - 1, step));
notifyChange();
return current;
},
next() {
if (current < total - 1) {
current++;
} else if (loop) {
current = 0;
}
notifyChange();
return current;
},
prev() {
if (current > 0) {
current--;
} else if (loop) {
current = total - 1;
}
notifyChange();
return current;
},
play() {
if (isPlaying) return;
isPlaying = true;
intervalId = setInterval(() => {
controller.next();
}, currentSpeed);
},
stop() {
isPlaying = false;
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
},
toggle() {
if (isPlaying) {
controller.stop();
} else {
controller.play();
}
},
reset() {
controller.stop();
current = initialStep;
notifyChange();
},
setSpeed(newSpeed) {
currentSpeed = newSpeed;
if (isPlaying) {
controller.stop();
controller.play();
}
}
};
return controller;
}
// =============================================================================
// FLOW DIAGRAM COMPONENT
// =============================================================================
// Higher-level component for node/edge diagrams
// Options: {nodes, edges, width, height, activeNodes, activeEdges, theme,
// nodeWidth, nodeHeight, padding}
// Edge options: {id, source, target, label, labelHalo, labelDx, labelDy,
// dashed, curved, curvature}. Labels avoid unrelated nodes, other
// edge paths and sibling captions after the full diagram is drawn.
// Edges are trimmed at the box borders
// along their own direction (see the edge-trimming block below), and
// a curved edge's label rides the curve's midpoint.
// Text fitting (additive): when nodeWidth is OMITTED, each node's box grows to
// fit its label/sublabel (never narrower than the historical 100px default)
// instead of letting long labels spill outside the rect. When nodeWidth IS
// given, the authored geometry is respected and the label font shrinks (floor
// 9px) to stay inside the box.
FlowDiagram = function(options) {
const {
nodes = [],
edges = [],
width = 600,
height = 400,
activeNodes = [],
activeEdges = [],
theme = diagramTheme,
nodeWidth = 100,
nodeHeight = 50,
padding = 20
} = options;
const autoNodeWidth = options.nodeWidth == null;
const nodePadX = 12;
// Instrument chrome (§3): a figure's labels are chrome, not prose — mono,
// like every other figure in the book and the playground's panel. FlowDiagram
// was the one shared component that never set it, so its figures (m02, m29)
// drew their node labels, sublabels and edge captions in the body sans while
// the hand-built figures beside them were mono. The token is passed to the
// measurements too: mono's 0.6em advance is wider than the sans it replaced,
// so the auto-fit/shrink math has to measure the face that will render.
const chromeFont = 'var(--pg-mono)';
// Resolved {w, h, fontSize, subFontSize} per node (index-aligned).
const effSizes = nodes.map(node => {
const base = node.width || nodeWidth;
const labelW = node.label ? measureText(node.label, {fontSize: 12, fontWeight: 500, fontFamily: chromeFont}) : 0;
const subW = node.sublabel ? measureText(node.sublabel, {fontSize: 10, fontFamily: chromeFont}) : 0;
let w = base, fontSize = 12, subFontSize = 10;
if (autoNodeWidth) {
w = Math.max(base, labelW + nodePadX * 2, subW + 16);
} else {
const avail = Math.max(w - nodePadX * 2, 20);
if (labelW > avail) {
fontSize = fitFontSize(node.label, {maxWidth: avail, fontSize: 12, minFontSize: 9, fontWeight: 500, fontFamily: chromeFont});
subFontSize = Math.max(7.5, 10 * fontSize / 12);
}
}
return {w, h: node.height || nodeHeight, fontSize, subFontSize};
});
// Create SVG element
const svg = d3.create('svg')
.attr('width', width)
.attr('height', height)
.attr('viewBox', `0 0 ${width} ${height}`)
.style('font-family', chromeFont)
.attr('class', 'flow-diagram diagram-fig'); // .diagram-fig: managed figure — opts out of the blanket svg-text color resets in custom.scss
// Add background
svg.append('rect')
.attr('width', width)
.attr('height', height)
.attr('fill', theme.bg)
.attr('rx', 8);
// Create defs for markers
const defs = svg.append('defs');
// Standard arrow marker
defs.append('marker')
.attr('id', 'flow-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 8)
.attr('refY', 0)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', theme.edgeStroke);
// Highlighted arrow marker
defs.append('marker')
.attr('id', 'flow-arrow-highlight')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 8)
.attr('refY', 0)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto')
.append('path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', theme.highlight);
// --- Edge trimming --------------------------------------------------------
// An edge is trimmed at the border of the box it leaves and of the box it
// enters, so the arrowhead lands ON the target box instead of inside it.
// The trim distance must be measured along the edge's OWN direction: a
// vertical edge leaves through the box's top/bottom edge (h/2 away), not its
// side (w/2). Trimming every direction by w/2 (the old geometry) detached the
// head from the target — 17-63px of floating arrowhead on m29's platform
// figure — and, whenever the two centres were closer than w/2 + w/2 + 15, it
// pushed the start past the end and INVERTED the path, so the arrow pointed
// backwards (m29's identity→runtime and observability→memory edges).
// Ray/box exit distance: |centre → border| along unit direction (ux, uy).
const boxExit = (size, ux, uy) => {
const ax = Math.abs(ux), ay = Math.abs(uy);
const tx = ax > 1e-6 ? (size.w / 2) / ax : Infinity;
const ty = ay > 1e-6 ? (size.h / 2) / ay : Infinity;
return Math.min(tx, ty);
};
// Unit vector from (x, y), falling back to (fx, fy) for a degenerate span.
const unitDir = (x, y, fx, fy) => {
const l = Math.sqrt(x * x + y * y);
return l > 1e-6 ? [x / l, y / l] : [fx, fy];
};
const EDGE_START_GAP = 4; // breathing room where the line leaves the source
const EDGE_END_GAP = 1; // the head's tip extends ~2px past the path end
// Edges layer (draw first so nodes appear on top)
const edgesLayer = svg.append('g').attr('class', 'edges-layer');
const flowEdges = [];
const edgeLabels = [];
// Nodes layer
const nodesLayer = svg.append('g').attr('class', 'nodes-layer');
// Draw edges
edges.forEach((edge, i) => {
const sourceNode = nodes.find(n => n.id === edge.source);
const targetNode = nodes.find(n => n.id === edge.target);
if (!sourceNode || !targetNode) return;
const isActive = activeEdges.includes(edge.id) || activeEdges.includes(i);
const edgeColor = isActive ? theme.highlight : theme.edgeStroke;
const markerId = isActive ? 'flow-arrow-highlight' : 'flow-arrow';
// Calculate edge path
const x1 = sourceNode.x;
const y1 = sourceNode.y;
const x2 = targetNode.x;
const y2 = targetNode.y;
const dx = x2 - x1;
const dy = y2 - y1;
const len = Math.sqrt(dx * dx + dy * dy) || 1;
const sizeS = effSizes[nodes.indexOf(sourceNode)];
const sizeT = effSizes[nodes.indexOf(targetNode)];
// Control point from the node CENTRES (the authored bulge is a property of
// the node layout, not of where the line happens to be trimmed).
const curvature = edge.curvature || 0.2;
const ctrlX = (x1 + x2) / 2 - dy * curvature;
const ctrlY = (y1 + y2) / 2 + dx * curvature;
// Optional waypoints describe an authored polyline route in diagram
// coordinates. They are useful when a long relationship must route around
// intervening node boxes; existing straight and curved edges keep their
// historical geometry when `via` is omitted.
const via = Array.isArray(edge.via) ? edge.via.filter(p => Number.isFinite(p.x) && Number.isFinite(p.y)) : [];
// A curved edge leaves toward its control point and arrives from it, so the
// trim follows those tangents — the head then meets the box square-on
// instead of sliding along the chord. Straight edges trim along the chord.
const dirStart = via.length
? unitDir(via[0].x - x1, via[0].y - y1, dx / len, dy / len)
: edge.curved
? unitDir(ctrlX - x1, ctrlY - y1, dx / len, dy / len)
: [dx / len, dy / len];
const dirEnd = via.length
? unitDir(x2 - via[via.length - 1].x, y2 - via[via.length - 1].y, dx / len, dy / len)
: edge.curved
? unitDir(x2 - ctrlX, y2 - ctrlY, dx / len, dy / len)
: [dx / len, dy / len];
let dStart = boxExit(sizeS, dirStart[0], dirStart[1]) + EDGE_START_GAP;
let dEnd = boxExit(sizeT, dirEnd[0], dirEnd[1]) + EDGE_END_GAP;
// Boxes that overlap (or nearly touch) leave no room for a line: scale both
// trims so a short stub survives instead of crossing the two anchors over.
const room = Math.max(len - 4, 0);
if (dStart + dEnd > room) {
const k = room / (dStart + dEnd);
dStart *= k;
dEnd *= k;
}
const startX = x1 + dirStart[0] * dStart;
const startY = y1 + dirStart[1] * dStart;
const endX = x2 - dirEnd[0] * dEnd;
const endY = y2 - dirEnd[1] * dEnd;
const edgeGroup = edgesLayer.append('g')
.attr('class', `edge ${isActive ? 'highlighted' : ''}`);
if (edge.id) edgeGroup.attr('id', edge.id);
// Draw path
const pathD = via.length
? [`M${startX},${startY}`, ...via.map(p => `L${p.x},${p.y}`), `L${endX},${endY}`].join(' ')
: edge.curved
? `M${startX},${startY} Q${ctrlX},${ctrlY} ${endX},${endY}`
: `M${startX},${startY} L${endX},${endY}`;
const path = edgeGroup.append('path')
.attr('d', pathD)
.attr('fill', 'none')
.attr('stroke', edgeColor)
.attr('stroke-width', isActive ? 2.5 : 1.5)
.attr('marker-end', `url(#${markerId})`);
flowEdges.push({path: path.node()});
if (edge.dashed) {
path.attr('stroke-dasharray', '5,3');
}
if (isActive) {
path.attr('filter', `drop-shadow(0 0 4px ${theme.highlightGlow})`);
}
// Add label if present
if (edge.label) {
// A curved edge's caption rides the CURVE, not the chord: on a bowed
// edge the chord midpoint can sit inside an unrelated node box (m29's
// "recall" label was painted underneath the Agent runtime box, i.e.
// invisible) while the curve itself is clear. The quadratic's midpoint
// is (P0 + 2C + P2)/4, and its tangent there is parallel to the chord,
// so the perpendicular offset below stays correct for both cases.
const labelX = edge.curved && !via.length
? (startX + 2 * ctrlX + endX) / 4
: (startX + endX) / 2;
const labelY = edge.curved && !via.length
? (startY + 2 * ctrlY + endY) / 4
: (startY + endY) / 2;
const labelPath = path.node();
const labelAt = labelPath.getPointAtLength(labelPath.getTotalLength() / 2);
const labelBefore = labelPath.getPointAtLength(Math.max(0, labelPath.getTotalLength() / 2 - 1));
const labelAfter = labelPath.getPointAtLength(Math.min(labelPath.getTotalLength(), labelPath.getTotalLength() / 2 + 1));
const angle = Math.atan2(labelAfter.y - labelBefore.y, labelAfter.x - labelBefore.x);
// Offset the caption perpendicular to the line, by the caption's OWN
// support along that direction: the text is horizontal, so a fixed 14px
// offset only clears the line for a horizontal edge — on a vertical edge
// the line ran straight through the caption (m29's "authorized" label sat
// on its own arrow). |nx|·w/2 + |ny|·h/2 + gap reproduces the historical
// ~14px for horizontal edges and widens as the edge turns vertical.
const perpX = Math.sin(angle);
const perpY = -Math.cos(angle);
const labelWidth = measureText(edge.label, {
fontSize: 10,
fontFamily: (svg && svg.style ? svg.style('font-family') : null) || null
});
const offset = Math.abs(perpX) * (labelWidth / 2) + Math.abs(perpY) * 6.5 + 8;
const offsetX = perpX * offset;
const offsetY = perpY * offset;
const labelXPlaced = (via.length ? labelAt.x : labelX) + offsetX + (edge.labelDx || 0);
const labelYPlaced = (via.length ? labelAt.y : labelY) + offsetY + (edge.labelDy || 0);
const labelText = edgeGroup.append('text')
.attr('x', labelXPlaced)
.attr('y', labelYPlaced)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'central')
.attr('fill', isActive ? theme.highlightText : theme.nodeText)
.style('font-size', '10px')
.text(edge.label);
edgeLabels.push({
node: labelText.node(),
x: labelXPlaced,
y: labelYPlaced,
width: labelWidth,
sourceIndex: nodes.indexOf(sourceNode),
targetIndex: nodes.indexOf(targetNode),
path: path.node()
});
// Opt-in knockout halo (edge.labelHalo) keeps the caption legible where
// another edge or label passes underneath.
if (edge.labelHalo) applyHalo(labelText);
}
});
// Draw nodes
nodes.forEach((node, i) => {
const isActive = activeNodes.includes(node.id) || activeNodes.includes(i);
const nodeFill = isActive ? theme.highlight : theme.nodeFill;
const nodeStroke = isActive ? theme.highlight : theme.nodeStroke;
const textFill = isActive ? theme.textOnHighlight : theme.nodeText;
const {w, h, fontSize, subFontSize} = effSizes[i];
const nodeGroup = nodesLayer.append('g')
.attr('class', `node ${isActive ? 'highlighted' : ''}`)
.attr('transform', `translate(${node.x}, ${node.y})`);
if (node.id) nodeGroup.attr('id', node.id);
// Node rectangle
const rect = nodeGroup.append('rect')
.attr('x', -w / 2)
.attr('y', -h / 2)
.attr('width', w)
.attr('height', h)
.attr('rx', 6)
.attr('ry', 6)
.attr('fill', nodeFill)
.attr('stroke', nodeStroke)
.attr('stroke-width', isActive ? 2 : 1.5);
if (isActive) {
rect.attr('filter', `drop-shadow(0 0 6px ${theme.highlightGlow})`);
}
// Main label
const labelY = node.sublabel ? -6 : 0;
nodeGroup.append('text')
.attr('x', 0)
.attr('y', labelY)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'central')
.attr('fill', textFill)
.style('font-size', `${fontSize}px`)
.attr('font-weight', '500')
.attr('pointer-events', 'none')
.text(node.label || '');
// Sublabel
if (node.sublabel) {
nodeGroup.append('text')
.attr('x', 0)
.attr('y', 10)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'central')
.attr('fill', textFill)
.style('font-size', `${subFontSize}px`)
.attr('opacity', isActive ? 0.9 : 0.7)
.attr('pointer-events', 'none')
.text(node.sublabel);
}
});
// Resolve captions only after every node and edge exists, so every candidate
// can avoid the complete diagram. The authored perpendicular offset and
// labelDx/labelDy remain the base position; with no collision the resolver
// returns it unchanged. Endpoint boxes are excluded because captions belong
// to those edges, while unrelated nodes, other edge paths and sibling
// captions are obstacles.
if (edgeLabels.length) {
const nodeObstacles = nodes.flatMap((node, i) => {
const {w, h} = effSizes[i];
return [{x: node.x - w / 2, y: node.y - h / 2, w, h, index: i}];
});
const pathObstacles = new Map(flowEdges.map(({path}) => [path, _pathObstacleRects(path)]));
edgeLabels.forEach((label, i) => {
const obstacles = nodeObstacles
.filter(box => box.index !== label.sourceIndex && box.index !== label.targetIndex)
.map(({index, ...box}) => box);
flowEdges.forEach(({path}) => {
if (path !== label.path) obstacles.push(...(pathObstacles.get(path) || []));
});
edgeLabels.forEach((other, j) => {
if (i === j) return;
const pos = other.node;
const x = parseFloat(pos.getAttribute('x'));
const y = parseFloat(pos.getAttribute('y'));
obstacles.push(_anchorBox(x, y, other.width, 13, 'middle'));
});
const placed = _resolveLabelPos({
x: label.x,
y: label.y,
w: label.width,
h: 13,
anchor: 'middle',
obstacles,
bounds: _svgBounds(svg.node()),
step: 12,
reach: 4
});
label.node.setAttribute('x', placed.x);
label.node.setAttribute('y', placed.y);
});
}
return svg.node();
}
// =============================================================================
// EXPORTS
// =============================================================================
// Export everything as a single object for lessons to use
diagramLib = {
// Core dependencies
d3,
// Theme utilities
isDarkMode,
getCSSVar,
diagramTheme,
contrastInk,
// Text measurement & fitting
measureText,
estimateTextWidth,
fitFontSize,
wrapText,
wrapByChars,
drawWrappedText,
fitGutter,
// SVG primitives
createNode,
createArrow,
applyHalo,
placeLabel,
collectObstacles,
// Animation controller
createStepController,
// Components
FlowDiagram,
// Figure fit & scroll (anti-clipping)
fitContent,
fitAll,
scrollWrap
}/**
* Segmented step control for visualization stepping.
* @param {Object} options
* @param {number} options.min - Minimum step value (default 0)
* @param {number} options.max - Maximum step value
* @param {number} options.value - Initial value (default min)
* @param {string} options.label - Optional label text
* @returns {number} Current step value (reactive)
*/
stepControl = function({min = 0, max, value, label = null} = {}) {
const initialValue = value ?? min;
const steps = Array.from({length: max - min + 1}, (_, i) => min + i);
const container = htl.html`<div class="step-control">
${label ? htl.html`<span class="step-control-label">${label}</span>` : ''}
<div class="step-control-segments" role="group" aria-label="${label || 'Step control'}">
${steps.map(step => htl.html`<button
class="step-control-segment ${step === initialValue ? 'active' : ''}"
data-step="${step}"
aria-pressed="${step === initialValue}"
tabindex="${step === initialValue ? 0 : -1}"
>${step}</button>`)}
</div>
</div>`;
const segments = container.querySelectorAll('.step-control-segment');
let currentValue = initialValue;
function updateActive(newValue) {
currentValue = newValue;
segments.forEach(seg => {
const isActive = parseInt(seg.dataset.step) === newValue;
seg.classList.toggle('active', isActive);
seg.setAttribute('aria-pressed', isActive);
seg.tabIndex = isActive ? 0 : -1;
});
container.value = newValue;
container.dispatchEvent(new Event('input', {bubbles: true}));
}
// Click handler
segments.forEach(seg => {
seg.addEventListener('click', () => {
updateActive(parseInt(seg.dataset.step));
});
});
// Keyboard navigation
container.addEventListener('keydown', (e) => {
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
e.preventDefault();
const next = Math.min(currentValue + 1, max);
updateActive(next);
segments[next - min].focus();
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
e.preventDefault();
const prev = Math.max(currentValue - 1, min);
updateActive(prev);
segments[prev - min].focus();
} else if (e.key === 'Home') {
e.preventDefault();
updateActive(min);
segments[0].focus();
} else if (e.key === 'End') {
e.preventDefault();
updateActive(max);
segments[max - min].focus();
}
});
container.value = initialValue;
return container;
}Introduction
An embedding turns a token ID into numbers a network can actually use. A token embedding is the learned vector stored in the table row selected by a token ID.
Why it matters for LLMs:
- Everything downstream reads vectors: attention, feedforward layers, and output heads all operate on embeddings, never on raw IDs.
- Lookup is the whole mechanism: one indexing operation converts a symbolic address into adjustable numeric information.
- Learning lives in the rows: training never moves an ID — it reshapes the vectors the IDs point to, and useful geometry can emerge as a consequence.
What You’ll Learn
After this module, you can:
- Explain why a token ID is a categorical address, not a measurement
- Retrieve an embedding row by direct lookup and state the one-hot equivalence
eₖᵀ E = E[k] - Trace
[B, T]token IDs through one shared[V, D]table to[B, T, D]output vectors, including repeated IDs - Show that an optimizer step changes only the addressed rows while token IDs stay fixed
- Inspect learned geometry with cosine similarity without mistaking it for literal meaning
- Use
nn.Embeddingfor the same lookup in PyTorch - Explain why token identity alone cannot encode sequence order
Prerequisites
This module requires familiarity with:
- Module 01: Tensors — Tensor shapes, indexing, and broadcasting used throughout the lookup story
- Module 02: Autograd — Gradients and optimizer updates behind the selective row change
- Module 03: Tokenization — How text becomes the integer token IDs this lesson looks up
Orientation: From a Symbol to an Address
Module 03 showed how text is split into tokens and how each token receives an integer token ID. We only need that earlier story as a starting point: the ID is a label that lets us find the token again.
This lesson asks what happens next:
How can a symbolic word label become numeric information that a model can use and adjust?
Before any formula, keep four plain-language meanings in mind:
- An address is a stable slot number used to find an item. A token ID is an address, not a measurement: the number’s size and its gap from another ID do not describe the tokens.
- A table is an organized set of slots. Here, each vocabulary token owns one row in the embedding table.
- A parameter is an adjustable number inside the model. Training can change a parameter when that number helps the model make better predictions.
- A vector is an ordered list of numbers. In this lesson, a row’s two parameters form a vector that we can pass to the rest of the model or draw as a point.
That learned row vector is the token’s embedding. The address finds the row; the row supplies adjustable numeric information. We will first watch that lookup happen, then watch feedback change a row, and finally inspect the geometry that can emerge.
The False Number-Line Problem
To see why an address must not be treated like a measurement, consider this deliberately arbitrary assignment:
cat → 42
airplane → 43
dog → 891
On a number line, cat appears much closer to airplane than to dog. That is a trap. The assignment says nothing about animals, vehicles, or similarity; a tokenizer could choose different addresses without changing the tokens.
TipPredict before you look
Which token appears numerically closest to cat? Does the tokenizer give us any reason to expect that pair to behave similarly in a model?
The false number line is the only place where this lesson uses arbitrary large addresses. From here onward, every required explanation and visual uses one small, fixed teaching world: eight tokens, IDs 0 through 7, and two learned values per row.
toyWorld = ({
dimension: 2,
learningRate: 0.25,
// One shared batch sample for the later shape story; ID 0 repeats.
batchIds: [[0, 1, 0], [3, 4, 7]],
counterexample: [
{token: "cat", id: 42, display: 0.10},
{token: "airplane", id: 43, display: 0.22},
{token: "dog", id: 891, display: 0.90}
],
// Each manyBatchEndpoint is a curated teaching endpoint after many batches
// of selective row updates, not the exact one-step value from `gradient`.
tokens: [
{id: 0, token: "cat", kind: "animal", before: [-0.72, 0.18], gradient: [-3.20, -1.60], manyBatchEndpoint: [0.68, 0.62]},
{id: 1, token: "dog", kind: "animal", before: [0.64, -0.62], gradient: [0.24, -3.28], manyBatchEndpoint: [0.58, 0.52]},
{id: 2, token: "kitten", kind: "animal", before: [-0.12, 0.78], gradient: [-1.20, 0.88], manyBatchEndpoint: [0.80, 0.40]},
{id: 3, token: "airplane", kind: "vehicle", before: [0.18, -0.08], gradient: [1.92, 0.88], manyBatchEndpoint: [-0.72, -0.50]},
{id: 4, token: "bicycle", kind: "vehicle", before: [-0.82, -0.32], gradient: [-1.68, 0.32], manyBatchEndpoint: [-0.55, -0.62]},
{id: 5, token: "king", kind: "royal", before: [0.38, 0.70], gradient: [0.72, 0.72], manyBatchEndpoint: [-0.18, 0.82]},
{id: 6, token: "queen", kind: "royal", before: [-0.46, 0.44], gradient: [-0.80, -0.72], manyBatchEndpoint: [-0.32, 0.68]},
{id: 7, token: "code", kind: "other", before: [0.82, 0.12], gradient: [1.20, 1.60], manyBatchEndpoint: [0.05, -0.78]}
]
})tokenIdProblemViz = {
const c = m04Tools.columns(width);
const title = "Token IDs look quantitative — but they are addresses";
const subtitle = "The display spacing is compressed so all three arbitrary IDs remain readable.";
const titleMax = c.w - 2 * c.outer - (c.narrow ? 16 : 8);
const titleSize = m04Tools.textHeight(title, titleMax, {fontSize: 16, lineHeight: 19, fontWeight: 700});
const subtitleSize = m04Tools.textHeight(subtitle, titleMax, {fontSize: 11, lineHeight: 15});
const analysisText = "A tokenizer needs stable identifiers, not meaningful measurements. Subtracting or averaging IDs only manipulates addresses.";
const verdictText = "ID = address. Its numerical size and its distance from another ID carry no meaning.";
const analysisSize = m04Tools.textHeight(analysisText, c.railW - 28, {fontSize: 10.5, lineHeight: 15});
const verdictSize = m04Tools.textHeight(verdictText, c.railW - 28, {fontSize: 10.5, lineHeight: 15, fontWeight: 600});
const top = c.outer + titleSize.height + subtitleSize.height + 24;
const l = m04Tools.layout(c, {
top,
plotHeight: 248,
metricsHeight: 126,
analysisHeight: Math.max(140, 62 + analysisSize.height),
verdictHeight: Math.max(92, 52 + verdictSize.height)
});
const svg = m04Tools.root(
"Arbitrary token IDs on a false number line showing that numerical proximity is not meaning",
c.w,
l.height
).attr("data-m04-figure", "token-id-problem");
drawWrappedText(svg, title, {
x: c.outer, y: c.outer + 15, maxWidth: titleMax,
fontFamily: "var(--pg-mono)", lineHeight: 19, fontSize: 16,
fontWeight: 700, fill: diagramTheme.nodeText, anchor: "start",
className: "m04-dashboard-title"
});
drawWrappedText(svg, subtitle, {
x: c.outer, y: c.outer + titleSize.height + 18, maxWidth: titleMax,
fontFamily: "var(--pg-mono)", lineHeight: 15, fontSize: 11,
fill: diagramTheme.edgeStroke, anchor: "start",
className: "m04-dashboard-subtitle"
});
const plot = m04Tools.card(svg, l.plot, {
role: "plot-panel", title: "False number line", accent: diagramTheme.accent
});
const inner = {x: 18, y: 42, w: l.plot.w - 36, h: l.plot.h - 58};
const axisY = inner.y + 88;
plot.append("line")
.attr("x1", inner.x).attr("x2", inner.x + inner.w)
.attr("y1", axisY).attr("y2", axisY)
.attr("stroke", diagramTheme.edgeStroke).attr("stroke-width", 1.5);
toyWorld.counterexample.forEach((item, i) => {
const x = inner.x + item.display * inner.w;
const color = i === 0 ? diagramTheme.primary : i === 1 ? diagramTheme.highlight : diagramTheme.accent;
const labelY = i === 1 ? axisY + 50 : axisY - 42;
plot.append("line")
.attr("x1", x).attr("x2", x)
.attr("y1", axisY - 7).attr("y2", axisY + 7)
.attr("stroke", color).attr("stroke-width", 2);
plot.append("line")
.attr("x1", x).attr("x2", x)
.attr("y1", axisY + (labelY < axisY ? -7 : 7))
.attr("y2", labelY + (labelY < axisY ? 8 : -15))
.attr("stroke", color).attr("stroke-width", 1);
plot.append("circle")
.attr("cx", x).attr("cy", axisY).attr("r", 4.5)
.attr("fill", color).attr("stroke", diagramTheme.bgOpaque).attr("stroke-width", 1.5);
const anchor = i === 0 ? (c.narrow ? "start" : "end") : i === 1 ? "start" : "middle";
const dx = i === 0 ? (c.narrow ? 4 : -4) : i === 1 ? 4 : 0;
plot.append("text")
.attr("x", x).attr("y", labelY)
.attr("text-anchor", anchor)
.attr("dx", dx)
.attr("fill", color).attr("font-size", 10).attr("font-weight", 700)
.text(`${item.token} · ${item.id}`);
});
drawWrappedText(plot, "numerical gap 1", {
x: inner.x + 0.16 * inner.w, y: axisY + 24,
maxWidth: inner.w * 0.34, fontFamily: "var(--pg-mono)",
fontSize: 9, fontWeight: 700, fill: diagramTheme.highlight, anchor: "middle"
});
drawWrappedText(plot, "cat-to-dog gap 849", {
x: inner.x + 0.57 * inner.w, y: axisY - 22,
maxWidth: inner.w * 0.42, fontFamily: "var(--pg-mono)",
fontSize: 9, fontWeight: 700, fill: diagramTheme.edgeStroke, anchor: "middle"
});
drawWrappedText(plot, "The line knows address gaps. It knows nothing about tokens.", {
x: inner.x, y: inner.y + inner.h - 10, maxWidth: inner.w,
fontFamily: "var(--pg-mono)", lineHeight: 14, fontSize: 9.5,
fill: diagramTheme.nodeText, anchor: "start"
});
const metrics = m04Tools.card(svg, l.metrics, {
role: "metrics-card", title: "What the integers actually tell us"
});
[
["closest IDs", "cat ↔ airplane"],
["address gaps", "1 and 849"],
["semantic evidence", "none"]
].forEach(([label, value], i) => {
const y = 50 + i * 24;
metrics.append("text")
.attr("x", 14).attr("y", y).attr("fill", diagramTheme.edgeStroke)
.attr("font-size", 9).attr("font-weight", 700).text(label.toUpperCase());
drawWrappedText(metrics, value, {
x: Math.min(144, l.metrics.w * 0.48), y,
maxWidth: l.metrics.w - Math.min(144, l.metrics.w * 0.48) - 14,
fontFamily: "var(--pg-mono)", fontSize: 10.5, fontWeight: 600,
fill: diagramTheme.nodeText, anchor: "start"
});
});
const analysis = m04Tools.card(svg, l.analysis, {
role: "analysis-panel", title: "Why the trap works", accent: diagramTheme.primary
});
drawWrappedText(analysis, analysisText, {
x: 14, y: 50, maxWidth: l.analysis.w - 28,
fontFamily: "var(--pg-mono)", lineHeight: 15, fontSize: 10.5,
fill: diagramTheme.nodeText, anchor: "start"
});
const verdict = m04Tools.card(svg, l.verdict, {
role: "verdict-card", title: "First principle", accent: diagramTheme.highlight
});
drawWrappedText(verdict, verdictText, {
x: 14, y: 48, maxWidth: l.verdict.w - 28,
fontFamily: "var(--pg-mono)", lineHeight: 15, fontSize: 10.5,
fontWeight: 600, fill: diagramTheme.nodeText, anchor: "start"
});
return svg.node();
}
ImportantFirst principle: an ID only finds a row
The numerical value of a token ID carries no semantic meaning. Use it as an address for a learnable row instead of treating it as a measurement.
Table Lookup: An Address Chooses a Row
An embedding table is a table whose entries the model is allowed to adjust. In matrix notation, call the table E. It has V = 8 rows—one for each token in the toy vocabulary—and D = 2 numeric values in each row.
The ID does not undergo arithmetic. It selects a row:
token ID = address
table = adjustable rows
E[k] = row selected by address k
The values in a selected row are parameters: numbers that training may change. Follow one token from its label to its ID, highlighted row, and returned vector.
One-Hot as a Categorical Selector
A one-hot vector is another honest way to name one category. It is mostly zeros with one 1 at the chosen address. It does not claim that categories have a meaningful numeric order, and it does not create learned similarity by itself.
For a selected ID k, the one-hot selector is:
\mathbf{e}_k^{\top} := [0, \ldots, 0, \underbrace{1}_{\text{position }k}, 0, \ldots, 0].
Multiplying it by the table keeps exactly the selected row:
\mathbf{e}_k^{\top} E = E[k].
Predict: when the selected token changes, which one-hot position and which table row should change? Turn on Show math in the visual below to compare the sparse selector’s result with direct indexing. Direct indexing is still the mechanism to keep in mind: nn.Embedding retrieves E[k] without materializing a mostly-zero vector.
rowLookupViz = {
const c = m04Tools.columns(width);
const selected = toyWorld.tokens.find(d => d.id === lookupToken);
const fmt = value => `${value >= 0 ? "+" : ""}${value.toFixed(2)}`;
const title = "An ID selects one learnable row";
const subtitle = lookupShowMath
? `one-hot e_kᵀ E = E[k] · k = ${selected.id} · [${selected.before.map(fmt).join(", ")}]`
: `Follow ${selected.token}: token → ID ${selected.id} → highlighted row → vector`;
const titleMax = c.w - 2 * c.outer - (c.narrow ? 16 : 8);
const titleSize = m04Tools.textHeight(title, titleMax, {fontSize: 16, lineHeight: 19, fontWeight: 700});
const subtitleSize = m04Tools.textHeight(subtitle, titleMax, {fontSize: 11, lineHeight: 15});
const analysisText = `The integer ${selected.id} is used only to choose a row. The two values in that row are parameters; they are the embedding for ${selected.token}.`;
const verdictText = "Direct indexing is the primary model: ID = address, row = parameters, vector = embedding.";
const analysisSize = m04Tools.textHeight(analysisText, c.railW - 28, {fontSize: 10.5, lineHeight: 15});
const verdictSize = m04Tools.textHeight(verdictText, c.railW - 28, {fontSize: 10.5, lineHeight: 15, fontWeight: 600});
const top = c.outer + titleSize.height + subtitleSize.height + 24;
const l = m04Tools.layout(c, {
top,
plotHeight: 408,
metricsHeight: 126,
analysisHeight: Math.max(146, 64 + analysisSize.height),
verdictHeight: Math.max(100, 54 + verdictSize.height)
});
const svg = m04Tools.root(
`Embedding table lookup for token ${selected.token}, ID ${selected.id}`,
c.w,
l.height
).attr("data-m04-figure", "row-lookup")
.attr("role", "group");
drawWrappedText(svg, title, {
x: c.outer, y: c.outer + 15, maxWidth: titleMax,
fontFamily: "var(--pg-mono)", lineHeight: 19, fontSize: 16,
fontWeight: 700, fill: diagramTheme.nodeText, anchor: "start",
className: "m04-dashboard-title"
});
drawWrappedText(svg, subtitle, {
x: c.outer, y: c.outer + titleSize.height + 18, maxWidth: titleMax,
fontFamily: "var(--pg-mono)", lineHeight: 15, fontSize: 11,
fontWeight: lookupShowMath ? 700 : 400,
fill: lookupShowMath ? diagramTheme.primary : diagramTheme.edgeStroke,
anchor: "start", className: "m04-dashboard-subtitle"
});
const plot = m04Tools.card(svg, l.plot, {
role: "plot-panel", title: "Embedding table E · shape [8, 2]", accent: diagramTheme.accent
});
const inner = {x: 14, y: 42, w: l.plot.w - 28, h: l.plot.h - 56};
const selectorW = Math.min(44, inner.w * 0.15);
const labelW = Math.min(104, inner.w * 0.34);
const valueGap = 4;
const cellW = (inner.w - selectorW - labelW - valueGap) / 2;
const rowH = 27;
const rowGap = 3;
const rowStart = inner.y + 40;
const inputIds = plot.append("g")
.attr("data-role", "input-id")
.attr("role", "group")
.attr("aria-label", `Input token ${selected.token} with ID ${selected.id}`);
inputIds.append("text")
.attr("x", inner.x + 6).attr("y", inner.y + 10)
.attr("fill", diagramTheme.primary).attr("font-size", 9)
.attr("font-weight", 700)
.text(`INPUT · ${selected.token} → ID ${selected.id}`);
const table = plot.append("g")
.attr("data-role", "embedding-table")
.attr("role", "table")
.attr("aria-label", "Embedding table E with eight rows and two dimensions");
const selector = table.append("g")
.attr("data-role", "one-hot-selector")
.attr("role", "group")
.attr("aria-label", `Sparse one-hot selector for ID ${selected.id}`)
.attr("aria-hidden", lookupShowMath ? null : "true")
.attr("display", lookupShowMath ? null : "none")
.attr("opacity", lookupShowMath ? 1 : 0);
selector.append("text")
.attr("x", inner.x + selectorW / 2).attr("y", inner.y + 26)
.attr("text-anchor", "middle")
.attr("fill", diagramTheme.primary).attr("font-size", 9)
.attr("font-weight", 700).text("eₖᵀ");
table.append("text")
.attr("x", inner.x + selectorW + 6).attr("y", inner.y + 26)
.attr("fill", diagramTheme.edgeStroke).attr("font-size", 9)
.attr("font-weight", 700).text("TOKEN · ID");
["DIM 1", "DIM 2"].forEach((label, j) => {
table.append("text")
.attr("x", inner.x + selectorW + labelW + j * (cellW + valueGap) + cellW / 2)
.attr("y", inner.y + 26).attr("text-anchor", "middle")
.attr("fill", diagramTheme.edgeStroke).attr("font-size", 9)
.attr("font-weight", 700).text(label);
});
const rows = table.append("g")
.attr("data-role", "table-rows")
.attr("role", "rowgroup");
toyWorld.tokens.forEach((item, i) => {
const y = rowStart + i * (rowH + rowGap);
const active = item.id === selected.id;
const row = rows.append("g")
.attr("data-role", active ? "selected-row" : "table-row")
.attr("role", "row")
.attr("data-token-id", item.id)
.attr("aria-label", `${item.token}, ID ${item.id}${active ? ", selected row" : ""}`);
row.append("rect")
.attr("x", inner.x).attr("y", y).attr("width", inner.w).attr("height", rowH)
.attr("rx", 4)
.attr("fill", active ? diagramTheme.accent : diagramTheme.bgOpaque)
.attr("stroke", active ? diagramTheme.accent : diagramTheme.nodeStroke)
.attr("stroke-width", active ? 1.5 : 0.8)
.attr("opacity", active ? 1 : 0.72);
row.append("text")
.attr("x", inner.x + selectorW + 7).attr("y", y + 18)
.attr("fill", active ? diagramTheme.textOnFill : diagramTheme.nodeText)
.attr("font-size", 9.5).attr("font-weight", active ? 700 : 500)
.text(`${item.id} · ${item.token}`);
item.before.forEach((value, j) => {
row.append("text")
.attr("x", inner.x + selectorW + labelW + j * (cellW + valueGap) + cellW / 2)
.attr("y", y + 18).attr("text-anchor", "middle")
.attr("fill", active ? diagramTheme.textOnFill : diagramTheme.nodeText)
.attr("font-size", 9.5).attr("font-weight", active ? 700 : 400)
.text(fmt(value));
});
const position = selector.append("g")
.attr("data-role", "selector-position")
.attr("data-token-id", item.id)
.attr("aria-label", `Selector position ${item.id}: ${active ? "one" : "zero"}`);
position.append("rect")
.attr("x", inner.x + 4).attr("y", y + 3)
.attr("width", selectorW - 8).attr("height", rowH - 6).attr("rx", 3)
.attr("fill", active ? diagramTheme.accent : diagramTheme.bgOpaque)
.attr("stroke", active ? diagramTheme.accent : diagramTheme.nodeStroke)
.attr("stroke-width", active ? 1.2 : 0.7);
position.append("text")
.attr("x", inner.x + selectorW / 2).attr("y", y + 18)
.attr("text-anchor", "middle")
.attr("fill", active ? diagramTheme.textOnFill : diagramTheme.edgeStroke)
.attr("font-size", 10).attr("font-weight", 700)
.text(active ? "1" : "0");
});
const tableBottom = rowStart + toyWorld.tokens.length * (rowH + rowGap) - rowGap;
const outputY = tableBottom + 30;
plot.append("line")
.attr("x1", inner.x + inner.w / 2).attr("x2", inner.x + inner.w / 2)
.attr("y1", tableBottom + 3).attr("y2", outputY - 7)
.attr("stroke", diagramTheme.primary).attr("stroke-width", 1.5);
plot.append("text")
.attr("data-role", "selector-identity")
.attr("x", inner.x + inner.w / 2).attr("y", outputY - 10)
.attr("text-anchor", "middle").attr("fill", diagramTheme.primary)
.attr("font-size", 9).attr("font-weight", 700)
.text(lookupShowMath ? "SELECT ROW · eₖᵀ E = E[k]" : "SELECT ROW");
const output = plot.append("g")
.attr("data-role", "output-vector")
.attr("role", "group")
.attr("aria-label", `Output vector E[${selected.id}] for ${selected.token}`);
selected.before.forEach((value, j) => {
const outW = (inner.w - valueGap) / 2;
output.append("rect")
.attr("x", inner.x + j * (outW + valueGap)).attr("y", outputY)
.attr("width", outW).attr("height", 32).attr("rx", 5)
.attr("fill", diagramTheme.bgOpaque).attr("stroke", diagramTheme.primary)
.attr("stroke-width", 1.4);
output.append("text")
.attr("x", inner.x + j * (outW + valueGap) + outW / 2).attr("y", outputY + 21)
.attr("text-anchor", "middle").attr("fill", diagramTheme.nodeText)
.attr("font-size", 10.5).attr("font-weight", 700).text(fmt(value));
});
const metrics = m04Tools.card(svg, l.metrics, {
role: "metrics-card", title: "Lookup facts"
});
[
["address", `${selected.id} · ${selected.token}`],
["table shape", "[8, 2]"],
["output shape", "[2]"]
].forEach(([label, value], i) => {
const metric = metrics.append("g")
.attr("role", "group")
.attr("data-role", label === "output shape" ? "shape-readout" : "lookup-metric")
.attr("aria-label", `${label}: ${value}`);
const y = 50 + i * 24;
metric.append("text")
.attr("x", 14).attr("y", y).attr("fill", diagramTheme.edgeStroke)
.attr("font-size", 9).attr("font-weight", 700).text(label.toUpperCase());
drawWrappedText(metric, value, {
x: Math.min(144, l.metrics.w * 0.48), y,
maxWidth: l.metrics.w - Math.min(144, l.metrics.w * 0.48) - 14,
fontFamily: "var(--pg-mono)", fontSize: 10.5, fontWeight: 600,
fill: diagramTheme.nodeText, anchor: "start"
});
});
const analysis = m04Tools.card(svg, l.analysis, {
role: "analysis-panel", title: `${selected.token}: address to parameters`, accent: diagramTheme.primary
});
drawWrappedText(analysis, analysisText, {
x: 14, y: 50, maxWidth: l.analysis.w - 28,
fontFamily: "var(--pg-mono)", lineHeight: 15, fontSize: 10.5,
fill: diagramTheme.nodeText, anchor: "start"
});
const verdict = m04Tools.card(svg, l.verdict, {
role: "verdict-card", title: "Mechanism", accent: diagramTheme.highlight
});
drawWrappedText(verdict, verdictText, {
x: 14, y: 48, maxWidth: l.verdict.w - 28,
fontFamily: "var(--pg-mono)", lineHeight: 15, fontSize: 10.5,
fontWeight: 600, fill: diagramTheme.nodeText, anchor: "start"
});
return svg.node();
}The highlighted row is the embedding returned by the lookup: a vector of learned parameters, not a reinterpretation of the ID. The selector explanation and the direct lookup therefore end at the same object, E[id].
Row to Point: The Same Vector, Drawn Geometrically
A vector is an ordered list, so a two-value row can also be drawn as a coordinate pair. For the fixed toy world, cat (ID 0) starts with row [-0.72, +0.18]; those same values locate the point (-0.72, +0.18).
row [-0.72, +0.18] → vector → coordinates (-0.72, +0.18) → point
This is a change of view, not a new representation and not a projection. The two plotted axes are teaching coordinates chosen to make the mechanism visible. They are not literal features such as “animalness,” and the map is not a production model’s measurement.
Predict: before changing the stage, which numbers should appear in the table and at the point? The staged visual below begins with Lookup and Vector to point; the same two numbers should move from the highlighted row onto the axes.
TipStop point: lookup to point only
Use only Lookup → Vector to point for this first pass. The same visual also offers Training update; leave that state unselected until the Learning Feedback section below explains the adjustment.
viewof trainingToken = Inputs.select(
toyWorld.tokens.map(d => d.id),
{
value: 0,
label: "Training token",
format: id => toyWorld.tokens.find(d => d.id === id).token
}
)
viewof trainingStage = Inputs.radio(
["Lookup", "Vector to point", "Training update"],
{value: "Vector to point", label: "Stage"}
)trainingUpdateViz = {
const c = m04Tools.columns(width);
const item = toyWorld.tokens.find(d => d.id === trainingToken);
const lr = toyWorld.learningRate;
const next = item.before.map((value, j) => value - lr * item.gradient[j]);
const fmt = value => `${value >= 0 ? "+" : ""}${value.toFixed(2)}`;
const isLookup = trainingStage === "Lookup";
const isPoint = trainingStage === "Vector to point";
const isUpdate = trainingStage === "Training update";
const title = isLookup
? "An ID selects one parameter row"
: isPoint
? "The selected vector becomes a point"
: "One optimizer step changes the selected row";
const subtitle = isLookup
? `${item.token} keeps ID ${item.id}; the ID is only an address`
: isPoint
? `${item.token}'s two row values become two coordinates`
: `${item.token} keeps ID ${item.id} while its row values change`;
const analysisText = isLookup
? `ID ${item.id} selects ${item.token}'s parameter row. No arithmetic on the address is involved.`
: isPoint
? `The row [${item.before.map(fmt).join(", ")}] is the coordinate pair (${item.before.map(fmt).join(", ")}). The vector and point are two views of one object.`
: `With learning rate ${lr.toFixed(2)}, the selected row follows row ← row − learning rate × gradient. Other rows are de-emphasized because this toy step did not use them.`;
const verdictText = isLookup
? "The ID is only an address. This first stage shows the selected table row and nothing else."
: isPoint
? "Row = parameters. Vector = embedding. A point is a geometric view of that same vector."
: `The ID stayed ${item.id}. Training changed ${item.token}'s row from [${item.before.map(fmt).join(", ")}] to [${next.map(fmt).join(", ")}].`;
const titleMax = c.w - 2 * c.outer - (c.narrow ? 16 : 8);
const titleSize = m04Tools.textHeight(title, titleMax, {fontSize: 16, lineHeight: 19, fontWeight: 700});
const subtitleSize = m04Tools.textHeight(subtitle, titleMax, {fontSize: 11, lineHeight: 15});
const analysisSize = m04Tools.textHeight(analysisText, c.railW - 28, {fontSize: 10.5, lineHeight: 15});
const verdictSize = m04Tools.textHeight(verdictText, c.railW - 28, {fontSize: 10.5, lineHeight: 15, fontWeight: 600});
// Vertical budget inside the plot card, measured before layout so the opaque
// inactive-row panel can never paint over the point/update caption below the
// chart (narrow widths wrap the caption onto more lines). `captionText` is the
// exact string drawn later; `inner` below uses the same pads (top 42, x 14,
// bottom 14) and inner width (c.mainW - 28), so the estimate matches the draw.
const captionText = isPoint
? `row [${item.before.map(fmt).join(", ")}] → coordinates (${item.before.map(fmt).join(", ")}) → point`
: isUpdate
? `gradient [${item.gradient.map(fmt).join(", ")}] moves the selected point`
: "Select row first; the same values will become coordinates next.";
const captionSize = m04Tools.textHeight(captionText, c.mainW - 28, {fontSize: 9, lineHeight: 14});
const idleColumns = c.narrow ? 3 : 4;
const idleRows = Math.ceil((toyWorld.tokens.length - 1) / idleColumns);
const idleRowHeight = 24;
const idleHeight = 30 + idleRows * idleRowHeight;
const captionBottom = 42 + 245 + 20 + captionSize.height;
const idleTop = captionBottom + 12 + 14;
const top = c.outer + titleSize.height + subtitleSize.height + 24;
const l = m04Tools.layout(c, {
top,
plotHeight: Math.ceil(idleTop + idleHeight + 10 + 14),
metricsHeight: 132,
analysisHeight: Math.max(158, 66 + analysisSize.height),
verdictHeight: Math.max(112, 56 + verdictSize.height)
});
const svg = m04Tools.root(
`${trainingStage} for token ${item.token}, ID ${item.id}`,
c.w,
l.height
).attr("data-m04-figure", "training-update")
.attr("role", "group");
drawWrappedText(svg, title, {
x: c.outer, y: c.outer + 15, maxWidth: titleMax,
fontFamily: "var(--pg-mono)", lineHeight: 19, fontSize: 16,
fontWeight: 700, fill: diagramTheme.nodeText, anchor: "start",
className: "m04-dashboard-title"
});
drawWrappedText(svg, subtitle, {
x: c.outer, y: c.outer + titleSize.height + 18, maxWidth: titleMax,
fontFamily: "var(--pg-mono)", lineHeight: 15, fontSize: 11,
fill: diagramTheme.edgeStroke, anchor: "start",
className: "m04-dashboard-subtitle"
});
const plot = m04Tools.card(svg, l.plot, {
role: "plot-panel", title: "Selected parameter row", accent: diagramTheme.accent
});
const inner = {x: 14, y: 42, w: l.plot.w - 28, h: l.plot.h - 56};
const rowGap = 48;
const rowW = (inner.w - rowGap) / 2;
const rowY = inner.y;
const rowH = 56;
const beforeG = plot.append("g")
.attr("data-role", "active-row-before")
.attr("role", "group")
.attr("data-token-id", item.id)
.attr("data-values", item.before.join(","))
.attr("aria-label", `${item.token} row before training, ID ${item.id}, values [${item.before.map(fmt).join(", ")}]`)
.attr("transform", `translate(${inner.x}, ${rowY})`);
const afterG = plot.append("g")
.attr("data-role", "active-row-after")
.attr("role", "group")
.attr("data-token-id", item.id)
.attr("data-values", next.join(","))
.attr("aria-label", `${item.token} row after training update, ID ${item.id}, values [${next.map(fmt).join(", ")}]`)
.attr("aria-hidden", isUpdate ? null : "true")
.attr("transform", `translate(${inner.x + rowW + rowGap}, ${rowY})`)
.attr("opacity", isUpdate ? 1 : 0);
const drawRow = (g, heading, values, active) => {
g.append("rect")
.attr("width", rowW).attr("height", rowH).attr("rx", 7)
.attr("fill", active ? diagramTheme.bgSecondary : diagramTheme.bgOpaque)
.attr("stroke", active ? diagramTheme.primary : diagramTheme.nodeStroke)
.attr("stroke-width", active ? 1.5 : 1);
g.append("text")
.attr("x", 8).attr("y", 16).attr("fill", active ? diagramTheme.primary : diagramTheme.edgeStroke)
.attr("font-size", 9).attr("font-weight", 700).text(heading);
g.append("text")
.attr("x", 8).attr("y", 32).attr("fill", diagramTheme.nodeText)
.attr("font-size", 9).attr("font-weight", 700).text(`${item.id} · ${item.token}`);
g.append("text")
.attr("x", 8).attr("y", 48).attr("fill", diagramTheme.nodeText)
.attr("font-size", 9).attr("font-weight", active ? 700 : 500)
.text(`[${values.map(fmt).join(", ")}]`);
};
drawRow(beforeG, "ROW BEFORE", item.before, true);
drawRow(afterG, "ROW AFTER STEP", next, isUpdate);
const defs = svg.append("defs");
defs.append("marker")
.attr("id", "m04-gradient-head")
.attr("viewBox", "0 0 10 10").attr("refX", 8).attr("refY", 5)
.attr("markerWidth", 7).attr("markerHeight", 7).attr("orient", "auto")
.append("path").attr("d", "M0,0 L10,5 L0,10 Z").attr("fill", diagramTheme.highlight);
const gradientG = plot.append("g")
.attr("data-role", "gradient-arrow")
.attr("role", "group")
.attr("aria-label", "Training adjustment movement")
.attr("aria-hidden", isUpdate ? null : "true")
.attr("opacity", isUpdate ? 1 : 0);
gradientG.append("line")
.attr("x1", inner.x + rowW + 6).attr("x2", inner.x + rowW + rowGap - 7)
.attr("y1", rowY + rowH / 2).attr("y2", rowY + rowH / 2)
.attr("stroke", diagramTheme.highlight).attr("stroke-width", 2)
.attr("marker-end", "url(#m04-gradient-head)");
gradientG.append("text")
.attr("x", inner.x + rowW + rowGap / 2).attr("y", rowY + 16)
.attr("text-anchor", "middle").attr("fill", diagramTheme.highlight)
.attr("font-size", 9).attr("font-weight", 700).text("SGD");
const chart = plot.append("g")
.attr("data-role", "vector-point-view")
.attr("role", "group")
.attr("aria-label", isLookup
? "Vector-to-point view hidden until the next stage"
: isUpdate
? `${item.token} point before and after the training update`
: `${item.token} row values shown as coordinates`)
.attr("aria-hidden", isLookup ? "true" : null)
.attr("display", isLookup ? "none" : null)
.attr("opacity", isLookup ? 0 : 1);
const chartTop = inner.y + 100;
const chartBottom = inner.y + 245;
const xScale = d3.scaleLinear().domain([-1, 1]).range([inner.x + 22, inner.x + inner.w - 22]);
const yScale = d3.scaleLinear().domain([-1, 1]).range([chartBottom, chartTop]);
const beforeAnchorEnd = item.before[0] > 0.45;
const nextAnchorEnd = next[0] > 0.45;
chart.append("line")
.attr("x1", xScale(-1)).attr("x2", xScale(1))
.attr("y1", yScale(0)).attr("y2", yScale(0))
.attr("stroke", diagramTheme.nodeStroke).attr("stroke-dasharray", "3,4");
chart.append("line")
.attr("x1", xScale(0)).attr("x2", xScale(0))
.attr("y1", yScale(-1)).attr("y2", yScale(1))
.attr("stroke", diagramTheme.nodeStroke).attr("stroke-dasharray", "3,4");
chart.append("text")
.attr("x", xScale(1)).attr("y", yScale(0) - 6).attr("text-anchor", "end")
.attr("fill", diagramTheme.edgeStroke).attr("font-size", 9).text("dimension 1");
chart.append("text")
.attr("x", xScale(0) + 6).attr("y", yScale(1) + 10)
.attr("fill", diagramTheme.edgeStroke).attr("font-size", 9).text("dimension 2");
chart.append("line")
.attr("x1", xScale(item.before[0])).attr("y1", yScale(item.before[1]))
.attr("x2", xScale(next[0])).attr("y2", yScale(next[1]))
.attr("stroke", diagramTheme.highlight).attr("stroke-width", isUpdate ? 2 : 0)
.attr("stroke-dasharray", "4,3").attr("marker-end", isUpdate ? "url(#m04-gradient-head)" : null);
chart.append("circle")
.attr("cx", xScale(item.before[0])).attr("cy", yScale(item.before[1]))
.attr("r", 6).attr("fill", diagramTheme.primary)
.attr("stroke", diagramTheme.bgOpaque).attr("stroke-width", 1.5);
chart.append("text")
.attr("x", xScale(item.before[0]) + (beforeAnchorEnd ? -8 : 8)).attr("y", yScale(item.before[1]) - 8)
.attr("text-anchor", beforeAnchorEnd ? "end" : "start")
.attr("fill", diagramTheme.primary).attr("font-size", 9).attr("font-weight", 700)
.text(isUpdate ? "before" : `${item.token} = point`);
chart.append("circle")
.attr("cx", xScale(next[0])).attr("cy", yScale(next[1]))
.attr("r", 6).attr("fill", diagramTheme.highlight)
.attr("stroke", diagramTheme.bgOpaque).attr("stroke-width", 1.5)
.attr("opacity", isUpdate ? 1 : 0);
chart.append("text")
.attr("x", xScale(next[0]) + (nextAnchorEnd ? -8 : 8)).attr("y", yScale(next[1]) - 8)
.attr("text-anchor", nextAnchorEnd ? "end" : "start")
.attr("fill", diagramTheme.highlight).attr("font-size", 9).attr("font-weight", 700)
.attr("opacity", isUpdate ? 1 : 0).text("after step");
drawWrappedText(chart, captionText, {
x: inner.x, y: chartBottom + 20, maxWidth: inner.w,
fontFamily: "var(--pg-mono)", lineHeight: 14, fontSize: 9,
fontWeight: 600, fill: diagramTheme.nodeText, anchor: "start"
});
const idleItems = toyWorld.tokens.filter(d => d.id !== item.id);
const idleG = plot.append("g")
.attr("data-role", "inactive-rows")
.attr("role", "group")
.attr("aria-label", "Other embedding rows")
.attr("opacity", isUpdate ? 0.34 : 0.56);
// idleTop is the measured budget from above: the panel starts below the
// wrapped caption, and plotHeight already reserves idleHeight + bottom pad.
idleG.append("rect")
.attr("x", inner.x).attr("y", idleTop)
.attr("width", inner.w).attr("height", idleHeight).attr("rx", 5)
.attr("fill", diagramTheme.bgOpaque).attr("stroke", diagramTheme.nodeStroke);
idleG.append("text")
.attr("x", inner.x + 8).attr("y", idleTop + 15)
.attr("fill", diagramTheme.edgeStroke).attr("font-size", 9).attr("font-weight", 700)
.text("OTHER ROWS · unchanged in this step");
const idleGap = 5;
const idleCellW = (inner.w - idleGap * (idleColumns - 1)) / idleColumns;
idleItems.forEach((other, index) => {
const column = index % idleColumns;
const rowIndex = Math.floor(index / idleColumns);
const x = inner.x + column * (idleCellW + idleGap) + 4;
const y = idleTop + 29 + rowIndex * idleRowHeight;
const inactive = idleG.append("g")
.attr("data-role", "inactive-row")
.attr("role", "group")
.attr("data-token-id", other.id)
.attr("data-values", other.before.join(","))
.attr("aria-label", `${other.token}, ID ${other.id}, unchanged values [${other.before.map(fmt).join(", ")}]`);
inactive.append("text")
.attr("x", x).attr("y", y)
.attr("fill", diagramTheme.nodeText).attr("font-size", 9).attr("font-weight", 600)
.text(`${other.id} ${other.token}`);
inactive.append("text")
.attr("x", x).attr("y", y + 11)
.attr("fill", diagramTheme.edgeStroke).attr("font-size", 9)
.text(`[${other.before.map(fmt).join(",")}]`);
});
const metricsRows = isLookup
? [
["address", `${item.id} · ${item.token}`],
["selected row", `[${item.before.map(fmt).join(", ")}]`],
["operation", "direct indexing"]
]
: isPoint
? [
["vector", `[${item.before.map(fmt).join(", ")}]`],
["coordinates", `(${item.before.map(fmt).join(", ")})`],
["new values", "none"]
]
: [
["before", `[${item.before.map(fmt).join(", ")}]`],
["gradient", `[${item.gradient.map(fmt).join(", ")}]`],
["after step", `[${next.map(fmt).join(", ")}]`]
];
const metrics = m04Tools.card(svg, l.metrics, {
role: "metrics-card", title: "Selected-row values"
});
metricsRows.forEach(([label, value], i) => {
const metric = metrics.append("g")
.attr("role", "group")
.attr("data-role", label === "vector" || label === "coordinates" ? "vector-readout" : "training-metric")
.attr("aria-label", `${label}: ${value}`);
const y = 50 + i * 25;
metric.append("text")
.attr("x", 14).attr("y", y).attr("fill", diagramTheme.edgeStroke)
.attr("font-size", 9).attr("font-weight", 700).text(label.toUpperCase());
drawWrappedText(metric, value, {
x: Math.min(144, l.metrics.w * 0.48), y,
maxWidth: l.metrics.w - Math.min(144, l.metrics.w * 0.48) - 14,
fontFamily: "var(--pg-mono)", fontSize: 10.5, fontWeight: 600,
fill: diagramTheme.nodeText, anchor: "start"
});
});
const analysis = m04Tools.card(svg, l.analysis, {
role: "analysis-panel", title: trainingStage, accent: diagramTheme.primary
});
drawWrappedText(analysis, analysisText, {
x: 14, y: 50, maxWidth: l.analysis.w - 28,
fontFamily: "var(--pg-mono)", lineHeight: 15, fontSize: 10.5,
fill: diagramTheme.nodeText, anchor: "start"
});
const verdict = m04Tools.card(svg, l.verdict, {
role: "verdict-card", title: isUpdate ? "What changed?" : "What stays fixed?", accent: diagramTheme.highlight
});
drawWrappedText(verdict, verdictText, {
x: 14, y: 48, maxWidth: l.verdict.w - 28,
fontFamily: "var(--pg-mono)", lineHeight: 15, fontSize: 10.5,
fontWeight: 600, fill: diagramTheme.nodeText, anchor: "start"
});
return svg.node();
}Batch Lookup: One Table, Many Positions
The same table is reused when a model handles several sequences at once. A batch is a group of sequences; B counts the sequences and T counts the token positions in each sequence. If the table has V rows and D values per row, lookup changes the shape but not the table:
token IDs [B, T] → shared table E [V, D] → vectors [B, T, D]
The fixed toy batch is [[0, 1, 0], [3, 4, 7]]: two sequences of three IDs use the same [8, 2] table and produce [2, 3, 2] vectors. ID 0 appears twice in the first sequence, so both positions retrieve the identical cat row. Reusing a row is not copying a new parameter; it is asking the same table for the same address twice.
Predict: which two output positions should contain identical vectors, and which table row supplies them? Keep the answer in view as the batch shape visual introduces repeated-row identity.
batchLookupViz = {
const c = m04Tools.columns(width);
const batch = toyWorld.batchIds;
const tokens = toyWorld.tokens;
const fmt = value => `${value >= 0 ? "+" : ""}${value.toFixed(2)}`;
const inputShape = `[${batch.length}, ${batch[0].length}]`;
const tableShape = `[${tokens.length}, ${toyWorld.dimension}]`;
const outputShape = `[${batch.length}, ${batch[0].length}, ${toyWorld.dimension}]`;
const title = "Many positions reuse one embedding table";
const subtitle = `IDs ${inputShape} → shared table E ${tableShape} → vectors ${outputShape}`;
const repeatedId = batch[0][0];
const repeatedPositions = batch
.flatMap((sequence, batchIndex) => sequence
.map((id, position) => ({id, batchIndex, position})))
.filter(({id}) => id === repeatedId);
const repeatedText = `ID ${repeatedId} repeats at ${repeatedPositions.map(({batchIndex, position}) => `B${batchIndex}, T${position}`).join(" and ")} → identical vectors`;
const analysisText = `Every position asks the same table for its ID. The repeated ID ${repeatedId} retrieves the same row twice, so both output positions receive the same vector.`;
const verdictText = `The ID tensor changes shape from ${inputShape} to ${outputShape} after lookup. The table stays ${tableShape}; repeating an address never creates a new row.`;
const titleMax = c.w - 2 * c.outer - (c.narrow ? 16 : 8);
const titleSize = m04Tools.textHeight(title, titleMax, {fontSize: 16, lineHeight: 19, fontWeight: 700});
const subtitleSize = m04Tools.textHeight(subtitle, titleMax, {fontSize: 11, lineHeight: 15});
const analysisSize = m04Tools.textHeight(analysisText, c.railW - 28, {fontSize: 10.5, lineHeight: 15});
const verdictSize = m04Tools.textHeight(verdictText, c.railW - 28, {fontSize: 10.5, lineHeight: 15, fontWeight: 600});
const top = c.outer + titleSize.height + subtitleSize.height + 24;
const l = m04Tools.layout(c, {
top,
plotHeight: 486,
metricsHeight: 140,
analysisHeight: Math.max(166, 68 + analysisSize.height),
verdictHeight: Math.max(126, 58 + verdictSize.height)
});
const svg = m04Tools.root(
`Batch embedding lookup: IDs ${inputShape} become vectors ${outputShape} through shared table ${tableShape}`,
c.w,
l.height
).attr("data-m04-figure", "batch-lookup")
.attr("role", "group");
drawWrappedText(svg, title, {
x: c.outer, y: c.outer + 15, maxWidth: titleMax,
fontFamily: "var(--pg-mono)", lineHeight: 19, fontSize: 16,
fontWeight: 700, fill: diagramTheme.nodeText, anchor: "start",
className: "m04-dashboard-title"
});
drawWrappedText(svg, subtitle, {
x: c.outer, y: c.outer + titleSize.height + 18, maxWidth: titleMax,
fontFamily: "var(--pg-mono)", lineHeight: 15, fontSize: 11,
fill: diagramTheme.edgeStroke, anchor: "start",
className: "m04-dashboard-subtitle"
});
const plot = m04Tools.card(svg, l.plot, {
role: "plot-panel", title: "Batch lookup flow", accent: diagramTheme.accent
});
const inner = {x: 14, y: 42, w: l.plot.w - 28, h: l.plot.h - 56};
const occurrenceCount = new Map();
batch.flat().forEach(id => occurrenceCount.set(id, (occurrenceCount.get(id) || 0) + 1));
const inputLabelW = 32;
const inputGap = 5;
const inputCellW = (inner.w - inputLabelW - 2 * inputGap) / 3;
const inputRowH = 21;
const inputRowGap = 8;
const inputTop = inner.y + 20;
const inputIds = plot.append("g")
.attr("data-role", "input-ids")
.attr("role", "list")
.attr("aria-label", `Input token IDs with shape ${inputShape}`);
inputIds.append("text")
.attr("x", inner.x).attr("y", inner.y + 10)
.attr("fill", diagramTheme.primary).attr("font-size", 9)
.attr("font-weight", 700).text(`INPUT IDS · shape ${inputShape}`);
batch.forEach((sequence, batchIndex) => {
const row = inputIds.append("g")
.attr("role", "group")
.attr("data-batch-index", batchIndex)
.attr("aria-label", `Sequence ${batchIndex} of ${batch.length}`);
const y = inputTop + batchIndex * (inputRowH + inputRowGap);
row.append("text")
.attr("x", inner.x).attr("y", y + 15)
.attr("fill", diagramTheme.edgeStroke).attr("font-size", 9)
.attr("font-weight", 700).text(`B${batchIndex}`);
sequence.forEach((id, position) => {
const item = tokens.find(token => token.id === id);
const repeated = occurrenceCount.get(id) > 1;
const cell = row.append("g")
.attr("data-role", "input-id")
.attr("role", "listitem")
.attr("data-token-id", id)
.attr("data-position", `B${batchIndex},T${position}`)
.attr("aria-label", `B${batchIndex}, position ${position}: ${item.token}, ID ${id}${repeated ? ", repeated address" : ""}`);
const x = inner.x + inputLabelW + position * (inputCellW + inputGap);
cell.append("rect")
.attr("x", x).attr("y", y).attr("width", inputCellW).attr("height", inputRowH)
.attr("rx", 4)
.attr("fill", repeated ? diagramTheme.highlightBg : diagramTheme.bgOpaque)
.attr("stroke", repeated ? diagramTheme.highlight : diagramTheme.accent)
.attr("stroke-width", repeated ? 1.5 : 1);
cell.append("text")
.attr("x", x + inputCellW / 2).attr("y", y + 14)
.attr("text-anchor", "middle")
.attr("fill", repeated ? diagramTheme.highlightText : diagramTheme.nodeText)
.attr("font-size", 9).attr("font-weight", repeated ? 700 : 500)
.text(`${id} · ${item.token}`);
});
});
const tableTop = inputTop + batch.length * (inputRowH + inputRowGap) + 24;
const table = plot.append("g")
.attr("data-role", "embedding-table")
.attr("role", "table")
.attr("aria-label", `Shared embedding table E with shape ${tableShape}`);
table.append("text")
.attr("x", inner.x).attr("y", tableTop)
.attr("fill", diagramTheme.primary).attr("font-size", 9)
.attr("font-weight", 700)
.text(`SHARED TABLE E · shape ${tableShape} · ID ${repeatedId} reused ×${occurrenceCount.get(repeatedId)}`);
const tableLabelW = Math.min(96, inner.w * 0.32);
const tableValueGap = 4;
const tableCellW = (inner.w - tableLabelW - tableValueGap) / 2;
const tableRowH = 18;
const tableRowGap = 2;
const tableHeaderY = tableTop + 14;
const tableRowStart = tableTop + 22;
table.append("text")
.attr("x", inner.x + 6).attr("y", tableHeaderY)
.attr("fill", diagramTheme.edgeStroke).attr("font-size", 9)
.attr("font-weight", 700).text("TOKEN · ID");
["DIM 1", "DIM 2"].forEach((label, j) => {
table.append("text")
.attr("x", inner.x + tableLabelW + j * (tableCellW + tableValueGap) + tableCellW / 2)
.attr("y", tableHeaderY).attr("text-anchor", "middle")
.attr("fill", diagramTheme.edgeStroke).attr("font-size", 9)
.attr("font-weight", 700).text(label);
});
const tableRows = table.append("g")
.attr("data-role", "selected-rows")
.attr("role", "rowgroup")
.attr("aria-label", "All embedding rows in ID order; selected rows marked by the input IDs");
const selectedIds = new Set(batch.flat());
tokens.forEach((item, index) => {
const selected = selectedIds.has(item.id);
const repeated = occurrenceCount.get(item.id) > 1;
const row = tableRows.append("g")
.attr("data-role", selected ? "selected-row" : "table-row")
.attr("role", "row")
.attr("aria-selected", selected ? "true" : "false")
.attr("data-token-id", item.id)
.attr("aria-label", `${item.token}, ID ${item.id}${selected ? ", selected by batch" : ""}${repeated ? `, retrieved ${occurrenceCount.get(item.id)} times` : ""}`);
const y = tableRowStart + index * (tableRowH + tableRowGap);
row.append("rect")
.attr("x", inner.x).attr("y", y).attr("width", inner.w).attr("height", tableRowH)
.attr("rx", 3)
.attr("fill", selected ? (repeated ? diagramTheme.highlightBg : diagramTheme.accentGlow) : diagramTheme.bgOpaque)
.attr("stroke", selected ? (repeated ? diagramTheme.highlight : diagramTheme.accent) : diagramTheme.nodeStroke)
.attr("stroke-width", selected ? 1.4 : 0.8)
.attr("opacity", selected ? 1 : 0.64);
row.append("text")
.attr("x", inner.x + 6).attr("y", y + 13)
.attr("fill", selected && repeated ? diagramTheme.highlightText : diagramTheme.nodeText)
.attr("font-size", 9).attr("font-weight", selected ? 700 : 500)
.text(`${item.id} · ${item.token}`);
item.before.forEach((value, j) => {
row.append("text")
.attr("x", inner.x + tableLabelW + j * (tableCellW + tableValueGap) + tableCellW / 2)
.attr("y", y + 13).attr("text-anchor", "middle")
.attr("fill", selected && repeated ? diagramTheme.highlightText : diagramTheme.nodeText)
.attr("font-size", 9).attr("font-weight", selected ? 700 : 400)
.text(fmt(value));
});
});
const tableBottom = tableRowStart + tokens.length * (tableRowH + tableRowGap) - tableRowGap;
const outputTop = tableBottom + 25;
const flow = plot.append("g")
.attr("data-role", "batch-flow")
.attr("role", "group")
.attr("aria-label", "Input IDs flow through the shared table to output vectors");
flow.append("line")
.attr("x1", inner.x + inner.w / 2).attr("x2", inner.x + inner.w / 2)
.attr("y1", inputTop + batch.length * (inputRowH + inputRowGap) + 2)
.attr("y2", tableTop - 5)
.attr("stroke", diagramTheme.primary).attr("stroke-width", 1.4);
flow.append("line")
.attr("x1", inner.x + inner.w / 2).attr("x2", inner.x + inner.w / 2)
.attr("y1", tableBottom + 3).attr("y2", outputTop - 8)
.attr("stroke", diagramTheme.primary).attr("stroke-width", 1.4);
const output = plot.append("g")
.attr("data-role", "output-vectors")
.attr("role", "list")
.attr("aria-label", `Output vectors with shape ${outputShape}`);
output.append("text")
.attr("x", inner.x).attr("y", outputTop - 10)
.attr("fill", diagramTheme.primary).attr("font-size", 9)
.attr("font-weight", 700).text(`OUTPUT VECTORS · shape ${outputShape}`);
const outputLabelW = 34;
const outputGap = 4;
const vectorW = (inner.w - outputLabelW - 2 * outputGap) / 3;
const vectorH = 28;
const vectorRowGap = 7;
batch.forEach((sequence, batchIndex) => {
const y = outputTop + batchIndex * (vectorH + vectorRowGap);
output.append("text")
.attr("x", inner.x).attr("y", y + 17)
.attr("fill", diagramTheme.edgeStroke).attr("font-size", 9)
.attr("font-weight", 700).text(`B${batchIndex}`);
sequence.forEach((id, position) => {
const item = tokens.find(token => token.id === id);
const values = item.before.map(fmt).join(",");
const repeated = occurrenceCount.get(id) > 1;
const vector = output.append("g")
.attr("data-role", "output-vector")
.attr("role", "listitem")
.attr("data-token-id", id)
.attr("data-position", `B${batchIndex},T${position}`)
.attr("aria-label", `B${batchIndex}, position ${position}: ${item.token} vector [${values}]${repeated ? ", identical repeated lookup" : ""}`);
const x = inner.x + outputLabelW + position * (vectorW + outputGap);
vector.append("rect")
.attr("x", x).attr("y", y).attr("width", vectorW).attr("height", vectorH)
.attr("rx", 4)
.attr("fill", repeated ? diagramTheme.highlightBg : diagramTheme.bgOpaque)
.attr("stroke", repeated ? diagramTheme.highlight : diagramTheme.primary)
.attr("stroke-width", repeated ? 1.5 : 1);
vector.append("text")
.attr("x", x + vectorW / 2).attr("y", y + 11)
.attr("text-anchor", "middle")
.attr("fill", repeated ? diagramTheme.highlightText : diagramTheme.edgeStroke)
.attr("font-size", 9).attr("font-weight", 700)
.text(`T${position} · ID ${id}`);
vector.append("text")
.attr("x", x + vectorW / 2).attr("y", y + 23)
.attr("text-anchor", "middle")
.attr("fill", diagramTheme.nodeText)
.attr("font-size", 9).attr("font-weight", 600)
.text(`[${values}]`);
});
});
drawWrappedText(output, repeatedText, {
x: inner.x, y: outputTop + batch.length * (vectorH + vectorRowGap) + 5,
maxWidth: inner.w, fontFamily: "var(--pg-mono)", lineHeight: 13,
fontSize: 9, fontWeight: 700, fill: diagramTheme.highlightText, anchor: "start"
});
const metrics = m04Tools.card(svg, l.metrics, {
role: "metrics-card", title: "Shape readout"
});
[
["input IDs", inputShape],
["table E", tableShape],
["output vectors", outputShape]
].forEach(([label, value], i) => {
const metric = metrics.append("g")
.attr("data-role", "shape-readout")
.attr("role", "group")
.attr("aria-label", `${label}: ${value}`);
const y = 50 + i * 28;
metric.append("text")
.attr("x", 14).attr("y", y).attr("fill", diagramTheme.edgeStroke)
.attr("font-size", 9).attr("font-weight", 700).text(label.toUpperCase());
drawWrappedText(metric, value, {
x: Math.min(144, l.metrics.w * 0.48), y,
maxWidth: l.metrics.w - Math.min(144, l.metrics.w * 0.48) - 14,
fontFamily: "var(--pg-mono)", fontSize: 10.5, fontWeight: 600,
fill: diagramTheme.nodeText, anchor: "start"
});
});
const analysis = m04Tools.card(svg, l.analysis, {
role: "analysis-panel", title: "Reuse, not copies", accent: diagramTheme.primary
});
drawWrappedText(analysis, analysisText, {
x: 14, y: 50, maxWidth: l.analysis.w - 28,
fontFamily: "var(--pg-mono)", lineHeight: 15, fontSize: 10.5,
fill: diagramTheme.nodeText, anchor: "start"
});
const verdict = m04Tools.card(svg, l.verdict, {
role: "verdict-card", title: "What changed?", accent: diagramTheme.highlight
});
drawWrappedText(verdict, verdictText, {
x: 14, y: 48, maxWidth: l.verdict.w - 28,
fontFamily: "var(--pg-mono)", lineHeight: 15, fontSize: 10.5,
fontWeight: 600, fill: diagramTheme.nodeText, anchor: "start"
});
return svg.node();
}Learning Feedback: Why a Row Changes
Lookup only retrieves values; learning gives those values a reason to move. During a training example, the model uses the selected vector to make a prediction. A loss is a numerical score for how far that prediction is from the desired result. The score sends feedback toward the parameters that participated in the prediction. Feedback loop at a glance:
input → prediction → feedback → small adjustment
Read that chain left to right: the input ID selects a row, the row helps the model make a prediction, comparing that prediction with a target produces feedback, and the feedback nudges the participating row values.
Two Module 02 ideas are enough here; this is a bridge back to autograd, not a second autograd lesson:
- A gradient is a suggested direction and size of adjustment: it summarizes how the loss would change if a parameter moved. Gradient descent commonly steps opposite the gradient to reduce the loss.
- An optimizer is the rule that applies that suggestion, often using a learning rate and other safeguards.
The causal story is therefore:
token → row lookup → prediction → loss/feedback → gradient → optimizer → row changes
Predict: if this example looked up ID 0, should the ID change, should every row change, or should the selected row’s values change? Return to the visual above and select Training update. It shows one small optimizer step: the selected row moves from before to after step, while the other seven rows stay visible but dimmed.
ImportantToken IDs do not learn. Embedding values do.
The ID remains an address. Training changes the parameters stored in table rows, which changes their vectors and therefore moves their points.
A real batch can touch several rows, and a repeated token can contribute more than once. The shared table is still one set of parameters; only rows reached by the example receive a nonzero update for that step.
Learned Geometry: Inspect, Don’t Read Meaning
One update moves one selected row and therefore one point. Across many batches, different rows can be selected on different steps, so their cumulative histories can reorganize the table. Useful neighborhoods or directions may emerge because they help reduce prediction loss; the model is not handed labels such as “animal” or “vehicle.”
Predict: before seeing the next visual, what should happen in each state? Before training shows the initial rows, During training shows one selective step for the current focus token, and After training shows a separately curated endpoint after many batches. The Compare token menu automatically excludes the current focus token.
Use this plot as inspection evidence, not literal meaning. A nearby point, direction, or analogy-like offset can be a hypothesis-generating probe to test on a trained model; it is not a human-labeled axis, a dictionary definition, or a universal rule. This 2D map is a teaching view of the fixed toy rows, not a production embedding visualization.
viewof meaningFocus = Inputs.select(
toyWorld.tokens.map(d => d.id),
{
value: 0,
label: "Focus token",
format: id => toyWorld.tokens.find(d => d.id === id).token
}
)
viewof meaningCompare = Inputs.select(
toyWorld.tokens.filter(d => d.id !== meaningFocus).map(d => d.id),
{
value: meaningFocus === 1 ? 0 : 1,
label: "Compare token",
format: id => toyWorld.tokens.find(d => d.id === id).token
}
)
viewof meaningState = Inputs.radio(
["Before training", "During training", "After training"],
{value: "Before training", label: "Training state"}
)meaningSpaceViz = {
const c = m04Tools.columns(width);
const lr = toyWorld.learningRate;
const stateKey = meaningState === "Before training" ? "before" : meaningState === "During training" ? "during" : "manyBatch";
const focus = toyWorld.tokens.find(d => d.id === meaningFocus);
const compare = toyWorld.tokens.find(d => d.id === meaningCompare && d.id !== focus.id)
|| toyWorld.tokens.find(d => d.id !== focus.id);
const oneStep = item => item.before.map((value, j) => value - lr * item.gradient[j]);
const coords = item => stateKey === "before"
? item.before
: stateKey === "during"
? (item.id === focus.id ? oneStep(item) : item.before)
: item.manyBatchEndpoint;
const focusPoint = coords(focus);
const comparePoint = coords(compare);
const fmt = value => `${value >= 0 ? "+" : ""}${value.toFixed(2)}`;
const relation = stateKey === "before"
? "initial rows scattered"
: stateKey === "during"
? "1 selected row moved; 7 unchanged"
: "curated many-batch endpoint";
const title = "Geometry emerges as rows are trained";
const subtitle = stateKey === "before"
? `Before training · compare ${focus.token} with ${compare.token} at their initial coordinates`
: stateKey === "during"
? `During training · one selective step moves only ${focus.token}; the other seven rows stay at their before coordinates`
: `After training · compare ${focus.token} with ${compare.token} at a curated endpoint after many selective batches`;
const analysisText = stateKey === "before"
? "The initial coordinates are scattered. An ID address supplies no reason for related tokens to begin near one another."
: stateKey === "during"
? `Only ${focus.token}'s row was selected for this illustrated optimizer step. It moves once; the other seven rows remain exactly at their before coordinates.`
: "This separately curated endpoint follows many batches. Different token rows were selectively updated on different steps, so each trail summarizes a cumulative history—not one shared update.";
const verdictText = "Geometry = consequence of learning. Treat a neighborhood as a property to inspect, not a literal dictionary of meaning.";
const titleMax = c.w - 2 * c.outer - (c.narrow ? 16 : 8);
const titleSize = m04Tools.textHeight(title, titleMax, {fontSize: 16, lineHeight: 19, fontWeight: 700});
const subtitleSize = m04Tools.textHeight(subtitle, titleMax, {fontSize: 11, lineHeight: 15});
const analysisSize = m04Tools.textHeight(analysisText, c.railW - 28, {fontSize: 10.5, lineHeight: 15});
const verdictSize = m04Tools.textHeight(verdictText, c.railW - 28, {fontSize: 10.5, lineHeight: 15, fontWeight: 600});
const top = c.outer + titleSize.height + subtitleSize.height + 24;
const l = m04Tools.layout(c, {
top,
plotHeight: 340,
metricsHeight: 132,
analysisHeight: Math.max(158, 66 + analysisSize.height),
verdictHeight: Math.max(116, 58 + verdictSize.height)
});
const svg = m04Tools.root(
`${meaningState} embedding geometry comparing ${focus.token} and ${compare.token}`,
c.w,
l.height
).attr("data-m04-figure", "meaning-space")
.attr("role", "group");
drawWrappedText(svg, title, {
x: c.outer, y: c.outer + 15, maxWidth: titleMax,
fontFamily: "var(--pg-mono)", lineHeight: 19, fontSize: 16,
fontWeight: 700, fill: diagramTheme.nodeText, anchor: "start",
className: "m04-dashboard-title"
});
drawWrappedText(svg, subtitle, {
x: c.outer, y: c.outer + titleSize.height + 18, maxWidth: titleMax,
fontFamily: "var(--pg-mono)", lineHeight: 15, fontSize: 11,
fill: diagramTheme.edgeStroke, anchor: "start",
className: "m04-dashboard-subtitle"
});
const plot = m04Tools.card(svg, l.plot, {
role: "plot-panel", title: "Embedding space · axes are learned coordinates", accent: diagramTheme.accent
});
const inner = {x: 14, y: 42, w: l.plot.w - 28, h: l.plot.h - 56};
const xScale = d3.scaleLinear().domain([-1, 1]).range([inner.x + 34, inner.x + inner.w - 34]);
const yScale = d3.scaleLinear().domain([-1, 1]).range([inner.y + inner.h - 38, inner.y + 24]);
plot.append("line")
.attr("x1", xScale(-1)).attr("x2", xScale(1))
.attr("y1", yScale(0)).attr("y2", yScale(0))
.attr("stroke", diagramTheme.nodeStroke).attr("stroke-dasharray", "3,4");
plot.append("line")
.attr("x1", xScale(0)).attr("x2", xScale(0))
.attr("y1", yScale(-1)).attr("y2", yScale(1))
.attr("stroke", diagramTheme.nodeStroke).attr("stroke-dasharray", "3,4");
plot.append("text")
.attr("x", xScale(1)).attr("y", yScale(0) - 7).attr("text-anchor", "end")
.attr("fill", diagramTheme.edgeStroke).attr("font-size", 9).text("dimension 1");
plot.append("text")
.attr("x", xScale(0) + 7).attr("y", yScale(1) + 10)
.attr("fill", diagramTheme.edgeStroke).attr("font-size", 9).text("dimension 2");
const kindColor = kind => kind === "animal"
? diagramTheme.accent
: kind === "vehicle"
? diagramTheme.highlight
: kind === "royal"
? diagramTheme.primary
: diagramTheme.edgeStroke;
if (stateKey === "during") {
const current = coords(focus);
const trail = plot.append("g").attr("data-role", "selected-step-trail");
trail.append("line")
.attr("x1", xScale(focus.before[0])).attr("y1", yScale(focus.before[1]))
.attr("x2", xScale(current[0])).attr("y2", yScale(current[1]))
.attr("stroke", diagramTheme.primary).attr("stroke-width", 2.2)
.attr("stroke-dasharray", "4,3");
trail.append("circle")
.attr("cx", xScale(focus.before[0])).attr("cy", yScale(focus.before[1]))
.attr("r", 3).attr("fill", diagramTheme.bgOpaque)
.attr("stroke", diagramTheme.primary).attr("stroke-width", 1.4);
trail.append("text")
.attr("x", xScale(focus.before[0]) + 6).attr("y", yScale(focus.before[1]) + 12)
.attr("fill", diagramTheme.primary).attr("font-size", 9).attr("font-weight", 700)
.text("before");
} else if (stateKey === "manyBatch") {
const trails = plot.append("g").attr("data-role", "cumulative-history-trails");
toyWorld.tokens.forEach(item => {
const current = coords(item);
trails.append("line")
.attr("data-role", "cumulative-history-trail")
.attr("x1", xScale(item.before[0])).attr("y1", yScale(item.before[1]))
.attr("x2", xScale(current[0])).attr("y2", yScale(current[1]))
.attr("stroke", kindColor(item.kind)).attr("stroke-width", 1.2)
.attr("stroke-dasharray", "3,3").attr("opacity", 0.42);
trails.append("circle")
.attr("data-role", "history-start")
.attr("cx", xScale(item.before[0])).attr("cy", yScale(item.before[1]))
.attr("r", 2.4).attr("fill", diagramTheme.bgOpaque)
.attr("stroke", kindColor(item.kind)).attr("stroke-width", 1)
.attr("opacity", 0.62);
});
}
plot.append("line")
.attr("x1", xScale(focusPoint[0])).attr("y1", yScale(focusPoint[1]))
.attr("x2", xScale(comparePoint[0])).attr("y2", yScale(comparePoint[1]))
.attr("stroke", diagramTheme.primary).attr("stroke-width", 1.8)
.attr("stroke-dasharray", "5,4");
toyWorld.tokens.forEach(item => {
const point = coords(item);
const isFocus = item.id === focus.id;
const isCompare = item.id === compare.id;
const color = stateKey === "before" || (stateKey === "during" && !isFocus)
? diagramTheme.edgeStroke
: kindColor(item.kind);
if (isFocus || isCompare) {
plot.append("circle")
.attr("cx", xScale(point[0])).attr("cy", yScale(point[1]))
.attr("r", isFocus ? 11 : 9)
.attr("fill", "none")
.attr("stroke", isFocus ? diagramTheme.primary : diagramTheme.highlight)
.attr("stroke-width", 2).attr("stroke-dasharray", "4,3");
}
plot.append("circle")
.attr("cx", xScale(point[0])).attr("cy", yScale(point[1]))
.attr("r", isFocus ? 6.5 : isCompare ? 5.5 : 4.5)
.attr("fill", color).attr("stroke", diagramTheme.bgOpaque).attr("stroke-width", 1.5);
const anchorEnd = point[0] > 0.42;
plot.append("text")
.attr("x", xScale(point[0]) + (anchorEnd ? -8 : 8))
.attr("y", yScale(point[1]) - 7)
.attr("text-anchor", anchorEnd ? "end" : "start")
.attr("fill", isFocus ? diagramTheme.primary : isCompare ? diagramTheme.highlight : diagramTheme.nodeText)
.attr("font-size", isFocus || isCompare ? 9.5 : 9)
.attr("font-weight", isFocus || isCompare ? 700 : 500)
.text(item.token);
});
const plotCue = stateKey === "before"
? "INITIAL COORDINATES · no row has moved"
: stateKey === "during"
? `ONE SELECTIVE STEP · ${focus.token} moves; 7 rows stay fixed`
: "CUMULATIVE HISTORIES · many selective batches";
drawWrappedText(plot, plotCue, {
x: inner.x, y: inner.y + inner.h - 23, maxWidth: inner.w,
fontFamily: "var(--pg-mono)", lineHeight: 13, fontSize: 9,
fontWeight: 700,
fill: stateKey === "during" ? diagramTheme.primary : stateKey === "manyBatch" ? diagramTheme.highlight : diagramTheme.edgeStroke,
anchor: "start"
});
const metrics = m04Tools.card(svg, l.metrics, {
role: "metrics-card", title: "State and selected pair"
});
[
["focus", `${focus.token} (${focusPoint.map(fmt).join(", ")})`, "coordinate-readout"],
["compare", `${compare.token} (${comparePoint.map(fmt).join(", ")})`, "coordinate-readout"],
["structure", relation, "meaning-metric"]
].forEach(([label, value, role], i) => {
const metric = metrics.append("g")
.attr("role", "group")
.attr("data-role", role)
.attr("aria-label", `${label}: ${value}`);
const y = 50 + i * 25;
metric.append("text")
.attr("x", 14).attr("y", y).attr("fill", diagramTheme.edgeStroke)
.attr("font-size", 9).attr("font-weight", 700).text(label.toUpperCase());
drawWrappedText(metric, value, {
x: Math.min(118, l.metrics.w * 0.40), y,
maxWidth: l.metrics.w - Math.min(118, l.metrics.w * 0.40) - 14,
fontFamily: "var(--pg-mono)", fontSize: 10, fontWeight: 600,
fill: diagramTheme.nodeText, anchor: "start"
});
});
const analysis = m04Tools.card(svg, l.analysis, {
role: "analysis-panel", title: meaningState, accent: diagramTheme.primary
});
drawWrappedText(analysis, analysisText, {
x: 14, y: 50, maxWidth: l.analysis.w - 28,
fontFamily: "var(--pg-mono)", lineHeight: 15, fontSize: 10.5,
fill: diagramTheme.nodeText, anchor: "start"
});
const verdict = m04Tools.card(svg, l.verdict, {
role: "verdict-card", title: "Interpret carefully", accent: diagramTheme.highlight
});
drawWrappedText(verdict, verdictText, {
x: 14, y: 48, maxWidth: l.verdict.w - 28,
fontFamily: "var(--pg-mono)", lineHeight: 15, fontSize: 10.5,
fontWeight: 600, fill: diagramTheme.nodeText, anchor: "start"
});
return svg.node();
}The Before training state is scattered. In During training, only the current focus token takes one illustrated gradient step; all seven other rows stay at their before coordinates. After training is a separate, curated endpoint after many batches in which different token rows were selectively updated on different steps. Its trails summarize cumulative histories rather than one shared update. The axes still have no simple labels such as “animalness”: real learned features are usually distributed across many coordinates.
Measuring a Relationship: Cosine Similarity
Now that vectors are visible as points and directions, we can ask a purposeful question: how aligned are the two selected vectors? Cosine similarity measures the angle between them while ignoring overall length:
\operatorname{cos}(\mathbf{u}, \mathbf{v}) = \frac{\mathbf{u}^{\top}\mathbf{v}}{\lVert\mathbf{u}\rVert\,\lVert\mathbf{v}\rVert}.
The current Focus token, Compare token, and Training state controls drive this measurement:
cosineMeasurement = {
const lr = toyWorld.learningRate;
const stateKey = meaningState === "Before training" ? "before" : meaningState === "During training" ? "during" : "manyBatch";
const focus = toyWorld.tokens.find(d => d.id === meaningFocus);
const compare = toyWorld.tokens.find(d => d.id === meaningCompare && d.id !== focus.id)
|| toyWorld.tokens.find(d => d.id !== focus.id);
const oneStep = item => item.before.map((value, j) => value - lr * item.gradient[j]);
const coords = item => stateKey === "before"
? item.before
: stateKey === "during"
? (item.id === focus.id ? oneStep(item) : item.before)
: item.manyBatchEndpoint;
const u = coords(focus);
const v = coords(compare);
const dot = u[0] * v[0] + u[1] * v[1];
const normU = Math.hypot(u[0], u[1]);
const normV = Math.hypot(v[0], v[1]);
const score = dot / (normU * normV);
return html`<p><strong>${focus.token} ↔ ${compare.token}</strong> in <strong>${meaningState.toLowerCase()}</strong>: cosine similarity <strong>${score.toFixed(3)}</strong>.</p>`;
}Cosine is a measurement of one learned geometric relationship—not a definition of meaning, an antonym detector, or proof that every model must organize these tokens the same way.
Implementation Bridge: nn.Embedding Does the Same Lookup
The visual mechanism has a direct PyTorch name: nn.Embedding(vocab_size, embedding_dim). This module stores the table as a trainable parameter. Calling it with integer IDs asks for rows by address; it does not first build one-hot vectors.
For B sequences of T IDs, the shape contract is:
token IDs [B, T]
embedding table [V, D]
output vectors [B, T, D]
The executable example uses the same eight-token, two-dimensional table and the same shared batch ([[0, 1, 0], [3, 4, 7]]) as the Batch Lookup visual, plus a real optimizer step on ID 0 that mirrors the Learning Feedback visual. It inspects the output shape, the repeated-row identity, and exactly which row changes. The default padding_idx=None leaves row 0 trainable, so it remains the cat row in this teaching world.
import torch
import torch.nn as nn
torch.manual_seed(4)
# Same toy world as the diagrams: V = 8 tokens and D = 2 values per row.
initial_rows = torch.tensor([
[-0.72, 0.18], # 0 cat
[ 0.64, -0.62], # 1 dog
[-0.12, 0.78], # 2 kitten
[ 0.18, -0.08], # 3 airplane
[-0.82, -0.32], # 4 bicycle
[ 0.38, 0.70], # 5 king
[-0.46, 0.44], # 6 queen
[ 0.82, 0.12], # 7 code
], dtype=torch.float32)
embedding = nn.Embedding(num_embeddings=8, embedding_dim=2)
with torch.no_grad():
embedding.weight.copy_(initial_rows)
ids = torch.tensor([
[0, 1, 0], # same shared batch as the Batch Lookup visual; ID 0 repeats
[3, 4, 7],
])
vectors = embedding(ids)
print("token ID shape:", list(ids.shape))
print("output shape: ", list(vectors.shape))
# Both positions of ID 0 retrieve the same cat row, as the batch visual shows.
print("repeated ID 0 identical:", bool((vectors[0, 0] == vectors[0, 2]).all()))
# A tiny downstream objective touches only row 0, mirroring the
# Learning Feedback visual: loss sends feedback to the participating row.
optimizer = torch.optim.SGD(embedding.parameters(), lr=0.25)
target = torch.tensor([0.88, 0.98])
before = embedding.weight.detach().clone()
loss = (embedding(torch.tensor([0]))[0] - target).pow(2).sum()
optimizer.zero_grad()
loss.backward()
optimizer.step()
after = embedding.weight.detach().clone()
changed_rows = torch.where((after - before).abs().sum(dim=1) > 0)[0].tolist()
print("row 0 before:", [round(value, 2) for value in before[0].tolist()])
print("row 0 after: ", [round(value, 2) for value in after[0].tolist()])
print("changed rows:", changed_rows)token ID shape: [2, 3]
output shape: [2, 3, 2]
repeated ID 0 identical: True
row 0 before: [-0.72, 0.18]
row 0 after: [0.08, 0.58]
changed rows: [0]
The input IDs remain integers. embedding.weight is the learnable matrix. Both [0, 0] and [0, 2] positions returned the same row because they share ID 0. The optimizer changed only row 0 because that was the row used by the loss; the other seven rows stayed fixed, exactly as the feedback visual’s selective update shows.
One Problem Remains: Order
Token lookup depends on token identity, not sequence position. The same address retrieves the same row wherever the token appears:
cat at position 2 → E[0]
cat at position 9 → E[0]
same token
same token embedding
different sequence position
Predict: if both occurrences get the same vector, what additional signal could tell the model where each occurrence sits? Token embeddings alone do not resolve order. Position needs separate machinery, which we will preview only as optional context after the core mental model; Module 05 supplies the attention mechanics later.
Mental Model
Keep this one-screen pipeline:
mmSteps = [
{title: "Symbol becomes an address", caption: "The tokenizer turns a symbol such as cat into token ID 0 — a slot number, not a measurement."},
{title: "Address selects a row", caption: "ID 0 picks row 0 of the shared [8, 2] table. The row holds adjustable parameters."},
{title: "Row returns a vector", caption: "Row 0's two values leave the table as the vector E[0] — the token's embedding."},
{title: "Vectors enter the transformer", caption: "The embedding flows into the model, which combines it with other positions to predict."},
{title: "Feedback updates the row", caption: "Prediction loss sends a gradient back; the optimizer changes the participating row. The ID stays fixed."},
{title: "Geometry emerges", caption: "Repeated updates nudge related rows near each other — neighborhoods to inspect, not literal meanings."}
]mentalModelViz = {
const c = m04Tools.columns(width);
const step = mmStep;
const active = mmSteps[step];
const verdicts = [
"IDs are addresses: size and gaps mean nothing.",
"ID = address; row = parameters.",
"Vector = embedding: the row's values, ready for the model.",
"Embeddings feed the transformer; prediction comes next.",
"Training changes rows; token IDs never move.",
"Geometry is a consequence of learning — inspect it, don't read it."
];
const title = "From a symbol to usable geometry";
const subtitle = "Step through the one-screen pipeline; the highlighted stage is active.";
const titleMax = c.w - 2 * c.outer - (c.narrow ? 16 : 8);
const titleSize = m04Tools.textHeight(title, titleMax, {fontSize: 16, lineHeight: 19, fontWeight: 700});
const subtitleSize = m04Tools.textHeight(subtitle, titleMax, {fontSize: 11, lineHeight: 15});
const analysisSize = m04Tools.textHeight(active.caption, c.railW - 28, {fontSize: 10.5, lineHeight: 15});
const verdictSize = m04Tools.textHeight(verdicts[step], c.railW - 28, {fontSize: 10.5, lineHeight: 15, fontWeight: 600});
const top = c.outer + titleSize.height + subtitleSize.height + 24;
const l = m04Tools.layout(c, {
top,
plotHeight: 320,
metricsHeight: 126,
analysisHeight: Math.max(140, 62 + analysisSize.height),
verdictHeight: Math.max(92, 52 + verdictSize.height)
});
const svg = m04Tools.root(
"Six-stage embedding pipeline from token ID to learned geometry",
c.w,
l.height
).attr("data-m04-figure", "mental-model")
.attr("role", "group");
drawWrappedText(svg, title, {
x: c.outer, y: c.outer + 15, maxWidth: titleMax,
fontFamily: "var(--pg-mono)", lineHeight: 19, fontSize: 16,
fontWeight: 700, fill: diagramTheme.nodeText, anchor: "start",
className: "m04-dashboard-title"
});
drawWrappedText(svg, subtitle, {
x: c.outer, y: c.outer + titleSize.height + 18, maxWidth: titleMax,
fontFamily: "var(--pg-mono)", lineHeight: 15, fontSize: 11,
fill: diagramTheme.edgeStroke, anchor: "start",
className: "m04-dashboard-subtitle"
});
const plot = m04Tools.card(svg, l.plot, {
role: "plot-panel", title: "Lookup pipeline", accent: diagramTheme.accent
});
const inner = {x: 14, y: 42, w: l.plot.w - 28, h: l.plot.h - 56};
const laneW = 64;
const rowW = inner.w - laneW;
const rowH = 30;
const rowGap = 16;
const rowY = (i) => inner.y + i * (rowH + rowGap);
const rowCX = inner.x + rowW / 2;
const defs = svg.append("defs");
defs.append("marker")
.attr("id", "mm-arrow")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 8).attr("refY", 0)
.attr("markerWidth", 6).attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", diagramTheme.edgeStroke);
defs.append("marker")
.attr("id", "mm-arrow-hi")
.attr("viewBox", "0 -5 10 10")
.attr("refX", 8).attr("refY", 0)
.attr("markerWidth", 6).attr("markerHeight", 6)
.attr("orient", "auto")
.append("path")
.attr("d", "M0,-5L10,0L0,5")
.attr("fill", diagramTheme.highlight);
mmSteps.forEach((s, i) => {
const y = rowY(i);
const isActive = i === step;
const ink = isActive ? diagramTheme.textOnHighlight : diagramTheme.nodeText;
const g = plot.append("g")
.attr("data-role", "pipeline-stage")
.attr("role", "group")
.attr("aria-label", `Stage ${i + 1} of 6: ${s.title}`);
const rect = g.append("rect")
.attr("x", inner.x).attr("y", y)
.attr("width", rowW).attr("height", rowH)
.attr("rx", 8)
.attr("fill", isActive ? diagramTheme.highlight : diagramTheme.nodeFill)
.attr("stroke", isActive ? diagramTheme.highlight : diagramTheme.nodeStroke)
.attr("stroke-width", isActive ? 2 : 1.5);
if (isActive) {
rect.attr("filter", `drop-shadow(0 0 6px ${diagramTheme.highlightGlow})`);
}
g.append("circle")
.attr("cx", inner.x + 18).attr("cy", y + rowH / 2).attr("r", 9)
.attr("fill", "none")
.attr("stroke", ink).attr("stroke-width", 1.5);
g.append("text")
.attr("x", inner.x + 18).attr("y", y + rowH / 2)
.attr("text-anchor", "middle").attr("dominant-baseline", "central")
.attr("fill", ink).attr("font-size", 10).attr("font-weight", 700)
.text(`${i + 1}`);
g.append("text")
.attr("x", inner.x + 34).attr("y", y + rowH / 2)
.attr("dominant-baseline", "central")
.attr("fill", ink).attr("font-size", 10.5).attr("font-weight", isActive ? 700 : 500)
.text(s.title);
if (i < mmSteps.length - 1) {
const edgeActive = (i + 1) === step;
plot.append("line")
.attr("data-role", "pipeline-edge")
.attr("x1", rowCX).attr("y1", y + rowH)
.attr("x2", rowCX).attr("y2", y + rowH + rowGap - 2)
.attr("stroke", edgeActive ? diagramTheme.highlight : diagramTheme.edgeStroke)
.attr("stroke-width", edgeActive ? 2.5 : 1.5)
.attr("marker-end", edgeActive ? "url(#mm-arrow-hi)" : "url(#mm-arrow)");
}
});
const fbY0 = rowY(4) + rowH / 2;
const fbY1 = rowY(1) + rowH / 2;
const fbX0 = inner.x + rowW;
const fbX1 = inner.x + inner.w - 6;
const fbActive = step === 4;
plot.append("path")
.attr("data-role", "feedback-edge")
.attr("d", `M${fbX0},${fbY0} C${fbX1},${fbY0} ${fbX1},${fbY1} ${fbX0 + 4},${fbY1}`)
.attr("fill", "none")
.attr("stroke", fbActive ? diagramTheme.highlight : diagramTheme.edgeStroke)
.attr("stroke-width", fbActive ? 2.5 : 1.5)
.attr("stroke-dasharray", "5,3")
.attr("marker-end", fbActive ? "url(#mm-arrow-hi)" : "url(#mm-arrow)");
const fbLabel = plot.append("text")
.attr("x", (fbX0 + fbX1) / 2).attr("y", (fbY0 + fbY1) / 2)
.attr("text-anchor", "middle").attr("dominant-baseline", "central")
.attr("fill", fbActive ? diagramTheme.highlightText : diagramTheme.nodeText)
.attr("font-size", 9).attr("font-weight", 700)
.text("feedback");
applyHalo(fbLabel);
const metrics = m04Tools.card(svg, l.metrics, {
role: "metrics-card", title: "Current stage"
});
metrics.append("text")
.attr("x", 14).attr("y", 50)
.attr("fill", diagramTheme.edgeStroke)
.attr("font-size", 9).attr("font-weight", 700)
.text("STEP");
drawWrappedText(metrics, `Step ${step + 1} of 6 · ${active.title}`, {
x: 14, y: 68, maxWidth: l.metrics.w - 28,
fontFamily: "var(--pg-mono)", lineHeight: 16, fontSize: 12,
fontWeight: 700, fill: diagramTheme.nodeText, anchor: "start"
});
const analysis = m04Tools.card(svg, l.analysis, {
role: "analysis-panel", title: "What happens here", accent: diagramTheme.primary
});
drawWrappedText(analysis, active.caption, {
x: 14, y: 50, maxWidth: l.analysis.w - 28,
fontFamily: "var(--pg-mono)", lineHeight: 15, fontSize: 10.5,
fill: diagramTheme.nodeText, anchor: "start"
});
const verdict = m04Tools.card(svg, l.verdict, {
role: "verdict-card", title: "Remember", accent: diagramTheme.highlight
});
drawWrappedText(verdict, verdicts[step], {
x: 14, y: 48, maxWidth: l.verdict.w - 28,
fontFamily: "var(--pg-mono)", lineHeight: 15, fontSize: 10.5,
fontWeight: 600, fill: diagramTheme.nodeText, anchor: "start"
});
return svg.node();
}- ID = address
- row = parameters
- vector = embedding
- training = changes rows
- geometry = consequence of learning
Check Your Understanding
Answer these without formulas.
- What does a token ID represent?
Answer
An address used to select a vocabulary entry and an embedding-table row. - What is an embedding?
Answer
The learned vector stored in the selected row. - What changes during training?
Answer
The values in embedding rows and other model parameters change; token IDs do not. - Why can related tokens develop useful geometric relationships?
Answer
Prediction loss sends gradients that move their vectors into relationships that help the model perform its task. - What does
nn.Embeddingdo when it receives integer IDs?Answer
It retrieves the addressed rows directly as vectors, without building one-hot selectors; a training step then changes only the rows its loss used. - Why does position require additional machinery?
Answer
The same token retrieves the same token embedding wherever it appears in a sequence.
Exercises
Exercise 1: Batch Shapes and Repeated Rows
Rebuild the toy [8, 2] table and run the shared batch through it. Confirm the [B, T] to [B, T, D] shape contract and that both positions of the repeated ID retrieve the same row.
import torch
import torch.nn as nn
torch.manual_seed(4)
# Same toy world as the diagrams: V = 8 tokens and D = 2 values per row.
initial_rows = torch.tensor([
[-0.72, 0.18], # 0 cat
[ 0.64, -0.62], # 1 dog
[-0.12, 0.78], # 2 kitten
[ 0.18, -0.08], # 3 airplane
[-0.82, -0.32], # 4 bicycle
[ 0.38, 0.70], # 5 king
[-0.46, 0.44], # 6 queen
[ 0.82, 0.12], # 7 code
], dtype=torch.float32)
embedding = nn.Embedding(num_embeddings=8, embedding_dim=2)
with torch.no_grad():
embedding.weight.copy_(initial_rows)
ids = torch.tensor([
[0, 1, 0], # same shared batch as the Batch Lookup visual; ID 0 repeats
[3, 4, 7],
])
vectors = embedding(ids)
print("token ID shape:", list(ids.shape))
print("output shape: ", list(vectors.shape))
print("repeated ID 0 identical:", bool((vectors[0, 0] == vectors[0, 2]).all()))token ID shape: [2, 3]
output shape: [2, 3, 2]
repeated ID 0 identical: True
Exercise 2: Only the Addressed Row Changes
Run a tiny optimizer step against one row and check exactly which rows moved. The loss below uses only the kitten row; change the target or the addressed ID and re-run to confirm the update stays selective.
# Fresh table from Exercise 1's initial_rows, so earlier cells cannot leak in.
embedding = nn.Embedding(num_embeddings=8, embedding_dim=2)
with torch.no_grad():
embedding.weight.copy_(initial_rows)
# A tiny downstream objective touches only row 2 (kitten), mirroring the
# Learning Feedback visual: loss sends feedback to the participating row.
optimizer = torch.optim.SGD(embedding.parameters(), lr=0.25)
target = torch.tensor([0.50, -0.50])
before = embedding.weight.detach().clone()
# Your implementation here: write a loss that uses only the kitten row,
# e.g. loss = (embedding(torch.tensor([2]))[0] - target).pow(2).sum()
loss = (embedding(torch.tensor([2]))[0] - target).pow(2).sum()
optimizer.zero_grad()
loss.backward()
optimizer.step()
after = embedding.weight.detach().clone()
changed_rows = torch.where((after - before).abs().sum(dim=1) > 0)[0].tolist()
print("changed rows:", changed_rows)
assert changed_rows == [2], "only the addressed row should change"
print("Only the addressed row changed; the other seven rows stayed fixed.")changed rows: [2]
Only the addressed row changed; the other seven rows stayed fixed.
Exercise 3: What Cosine Similarity Proves
Compute the cosine similarity between the starting cat and dog rows, then read the number for exactly what it is.
cosine(cat, dog) = -0.866
This number measures the angle between two curated teaching rows: it tells you how aligned they start out, which is all a similarity readout ever claims. It does not prove cat and dog mean similar things — these rows are hand-picked starting points, and only training feedback plus task behavior could turn a neighborhood into evidence.
Optional Chapter: Positional Representations and Production Scale
The required lesson ends with the mental model above and the unresolved order problem. Everything in this chapter is optional: it previews ways to add position and a few engineering choices without making the token-lookup story depend on attention.
Four ways to add position
All four methods answer the same question—how can two occurrences of one token carry different positional information? They contribute that information in different places:
- Learned absolute positions store one trainable vector per position.
LearnedPositionalEmbeddinglooks up the row for positiontand can add it to the token vector. - Fixed sinusoidal positions compute sine and cosine waves at several rates.
SinusoidalPositionalEmbeddingadds a repeatable, non-trainable pattern so each absolute position has a distinct signal. - RoPE (rotary position embedding) rotates paired features by an angle tied to position.
RotaryPositionalEmbeddingandapply_rotary_pos_embapply that rotation later to the query/key representations, so relative offsets can affect their relationship. - ALiBi (Attention with Linear Biases) leaves token vectors alone and contributes a distance-dependent penalty where attention scores are compared.
alibi_slopessupplies head-specific slopes andalibi_biasbuilds the corresponding biases.
These are four different homes for position, not four required implementations to memorize now. The optional position visual asks one focused question at a time; defer score calculations and the rest of attention vocabulary to Module 05.
Predict: if cat at positions 2 and 9 retrieves the same E[0], which part of each method below supplies the difference? Recall the order problem above — cat at position 2 → E[0] and cat at position 9 → E[0] — and use the Position method control to ask one focused question at a time. This preview is optional: the required mental model stands without it, and score details wait for Module 05.
positionMethodsViz = {
const c = m04Tools.columns(width);
const fmt = value => `${value >= 0 ? "+" : ""}${value.toFixed(2)}`;
const cat = toyWorld.tokens[0];
const method = positionMethod;
const isLearned = method === "Learned absolute";
const isSinusoidal = method === "Sinusoidal";
const isRope = method === "RoPE rotation";
const learnedRows = [
[0.11, -0.24], [-0.31, 0.08], [0.42, 0.19], [-0.07, -0.44], [0.28, -0.12],
[-0.19, 0.33], [0.05, 0.41], [-0.38, -0.05], [0.23, 0.27], [-0.14, -0.36]
];
const sumAt = t => [cat.before[0] + learnedRows[t][0], cat.before[1] + learnedRows[t][1]];
const ropeTheta = 0.3;
const ropePair = [0.8, 0.6];
const ropeRot = m => {
const a = m * ropeTheta;
return [
ropePair[0] * Math.cos(a) - ropePair[1] * Math.sin(a),
ropePair[0] * Math.sin(a) + ropePair[1] * Math.cos(a)
];
};
const alibiSlope = 0.25;
const alibiQuery = 9;
const alibiKeys = [4, 5, 6, 7, 8, 9];
const title = "One token, two positions: four ways to differ";
const subtitle = isLearned
? "Learned absolute · trainable row per position · cat E[0] at positions 2 and 9"
: isSinusoidal
? "Sinusoidal · fixed sine and cosine waves · pattern values at positions 2 and 9"
: isRope
? "RoPE rotation · paired features rotate by position · same pair at positions 2 and 9"
: "ALiBi bias · distance penalty on scores · query 9 over keys 4 to 9";
const plotTitle = isLearned
? "Learned table P plus token row"
: isSinusoidal
? "Fixed waves across positions"
: isRope
? "One pair rotated by position"
: "Penalties from query 9";
const metricRows = isLearned
? [["API", "LearnedPositionalEmbedding"], ["row shape", "(T, D): (10, 2) here"], ["combine", "input = E[id] + P[t]"]]
: isSinusoidal
? [["API", "SinusoidalPositionalEmbedding"], ["pattern", "PE(pos, 2i) = sin · PE(pos, 2i+1) = cos"], ["fixed", "non-trainable · D must be even"]]
: isRope
? [["API", "RotaryPositionalEmbedding"], ["operation", "apply_rotary_pos_emb rotates pairs"], ["shapes", "same (..., seq, dim) in and out"]]
: [["API", "alibi_slopes + alibi_bias"], ["bias rule", "-slope x distance · nearest = 0"], ["bias shape", "(heads, q_len, k_len)"]];
const analysisText = isLearned
? "Where can a position vector live? In its own table. P[2] and P[9] are different trainable rows, so adding either to the same E[0] gives a different input. Positions past max_seq_len own no row."
: isSinusoidal
? "What does a fixed position signal look like? Waves at several rates. Positions 2 and 9 read different sine and cosine values with no training, while slower dims vary over longer spans."
: isRope
? "How can position act without adding a vector? Rotate. The same feature pair turns by m times theta, so queries at positions 2 and 9 meet keys at different angles. Only the offset 9 - 2 = 7 shapes their relationship."
: "How can distance alone shape attention? Penalize it. Query 9 scores keys 4 to 9 with minus slope times distance at slope 1/4, so the nearest key keeps its full score and token vectors do not change.";
const verdictText = isLearned
? "Adds at the input · trainable · fixed length. Next state: fixed waves."
: isSinusoidal
? "Adds at the input · fixed · extrapolates. Next state: rotation on queries and keys."
: isRope
? "Acts on queries and keys · relative by construction. Next state: penalties on scores."
: "Acts on scores · no parameters · extrapolates. Score details wait for Module 05.";
const titleMax = c.w - 2 * c.outer - (c.narrow ? 16 : 8);
const titleSize = m04Tools.textHeight(title, titleMax, {fontSize: 16, lineHeight: 19, fontWeight: 700});
const subtitleSize = m04Tools.textHeight(subtitle, titleMax, {fontSize: 11, lineHeight: 15});
const analysisSize = m04Tools.textHeight(analysisText, c.railW - 28, {fontSize: 10.5, lineHeight: 15});
const verdictSize = m04Tools.textHeight(verdictText, c.railW - 28, {fontSize: 10.5, lineHeight: 15, fontWeight: 600});
const top = c.outer + titleSize.height + subtitleSize.height + 24;
const l = m04Tools.layout(c, {
top,
plotHeight: 380,
metricsHeight: 150,
analysisHeight: Math.max(150, 64 + analysisSize.height),
verdictHeight: Math.max(104, 54 + verdictSize.height)
});
const svg = m04Tools.root(
`Position methods, optional: ${subtitle}`,
c.w,
l.height
).attr("data-m04-figure", "position-methods")
.attr("role", "group");
drawWrappedText(svg, title, {
x: c.outer, y: c.outer + 15, maxWidth: titleMax,
fontFamily: "var(--pg-mono)", lineHeight: 19, fontSize: 16,
fontWeight: 700, fill: diagramTheme.nodeText, anchor: "start",
className: "m04-dashboard-title"
});
drawWrappedText(svg, subtitle, {
x: c.outer, y: c.outer + titleSize.height + 18, maxWidth: titleMax,
fontFamily: "var(--pg-mono)", lineHeight: 15, fontSize: 11,
fill: diagramTheme.edgeStroke, anchor: "start",
className: "m04-dashboard-subtitle"
});
const plot = m04Tools.card(svg, l.plot, {
role: "plot-panel", title: plotTitle, accent: diagramTheme.accent
});
const inner = {x: 14, y: 42, w: l.plot.w - 28, h: l.plot.h - 56};
if (isLearned) {
const learned = plot.append("g")
.attr("data-role", "position-learned")
.attr("role", "group")
.attr("aria-label", `Learned absolute positions: trainable rows P2 [${learnedRows[2].map(fmt).join(", ")}] and P9 [${learnedRows[9].map(fmt).join(", ")}] added to token row E0 [${cat.before.map(fmt).join(", ")}]`);
learned.append("text")
.attr("x", inner.x).attr("y", inner.y + 12)
.attr("fill", diagramTheme.primary).attr("font-size", 9)
.attr("font-weight", 700)
.text("POSITION TABLE P · [10, 2] · trainable rows");
const posLabelW = Math.min(56, inner.w * 0.22);
const posGap = 4;
const posCellW = (inner.w - posLabelW - posGap) / 2;
const rowH = 16;
const rowGap = 2;
const rowStart = inner.y + 32;
learned.append("text")
.attr("x", inner.x + 6).attr("y", inner.y + 26)
.attr("fill", diagramTheme.edgeStroke).attr("font-size", 9)
.attr("font-weight", 700).text("POS");
["DIM 1", "DIM 2"].forEach((label, j) => {
learned.append("text")
.attr("x", inner.x + posLabelW + j * (posCellW + posGap) + posCellW / 2)
.attr("y", inner.y + 26).attr("text-anchor", "middle")
.attr("fill", diagramTheme.edgeStroke).attr("font-size", 9)
.attr("font-weight", 700).text(label);
});
const tableRows = learned.append("g")
.attr("data-role", "position-table")
.attr("role", "table")
.attr("aria-label", "Learned position table P with ten rows and two dimensions");
learnedRows.forEach((pair, t) => {
const hot = t === 2 || t === 9;
const row = tableRows.append("g")
.attr("data-role", "position-row")
.attr("role", "row")
.attr("aria-selected", hot ? "true" : "false")
.attr("aria-label", `Position ${t} row [${pair.map(fmt).join(", ")}]${hot ? ", combined with cat below" : ""}`);
const y = rowStart + t * (rowH + rowGap);
row.append("rect")
.attr("x", inner.x).attr("y", y).attr("width", inner.w).attr("height", rowH)
.attr("rx", 3)
.attr("fill", hot ? diagramTheme.highlightBg : diagramTheme.bgOpaque)
.attr("stroke", hot ? diagramTheme.highlight : diagramTheme.nodeStroke)
.attr("stroke-width", hot ? 1.4 : 0.8)
.attr("opacity", hot ? 1 : 0.64);
row.append("text")
.attr("x", inner.x + 6).attr("y", y + 12)
.attr("fill", hot ? diagramTheme.highlightText : diagramTheme.nodeText)
.attr("font-size", 9).attr("font-weight", hot ? 700 : 500)
.text(`t = ${t}`);
pair.forEach((value, j) => {
row.append("text")
.attr("x", inner.x + posLabelW + j * (posCellW + posGap) + posCellW / 2)
.attr("y", y + 12).attr("text-anchor", "middle")
.attr("fill", hot ? diagramTheme.highlightText : diagramTheme.nodeText)
.attr("font-size", 9).attr("font-weight", hot ? 700 : 400)
.text(fmt(value));
});
});
const tableBottom = rowStart + learnedRows.length * (rowH + rowGap) - rowGap;
[2, 9].forEach((t, k) => {
learned.append("text")
.attr("x", inner.x).attr("y", tableBottom + 20 + k * 18)
.attr("fill", diagramTheme.highlightText).attr("font-size", 9.5)
.attr("font-weight", 700)
.text(`cat@${t} = E[0] + P[${t}] = [${sumAt(t).map(fmt).join(", ")}]`);
});
drawWrappedText(learned, "SAME E[0], DIFFERENT P ROW: positions 2 and 9 give different inputs", {
x: inner.x, y: inner.y + inner.h - 23, maxWidth: inner.w,
fontFamily: "var(--pg-mono)", lineHeight: 13, fontSize: 9,
fontWeight: 700, fill: diagramTheme.highlightText, anchor: "start"
});
}
if (isSinusoidal) {
const sino = plot.append("g")
.attr("data-role", "position-sinusoidal")
.attr("role", "group")
.attr("aria-label", "Sinusoidal positions: fixed sine and cosine waves across positions 0 to 11; positions 2 and 9 read different values");
sino.append("text")
.attr("x", inner.x).attr("y", inner.y + 12)
.attr("fill", diagramTheme.primary).attr("font-size", 9)
.attr("font-weight", 700)
.text("FIXED WAVES · fast pair · no training");
const chartTop = inner.y + 30;
const chartBottom = inner.y + 206;
const xScale = d3.scaleLinear().domain([0, 11]).range([inner.x + 32, inner.x + inner.w - 12]);
const yScale = d3.scaleLinear().domain([-1.15, 1.15]).range([chartBottom, chartTop]);
sino.append("line")
.attr("x1", xScale(0)).attr("x2", xScale(11))
.attr("y1", yScale(0)).attr("y2", yScale(0))
.attr("stroke", diagramTheme.nodeStroke).attr("stroke-dasharray", "3,4");
[[-1, "-1.0"], [0, "0"], [1, "+1.0"]].forEach(([v, label]) => {
sino.append("text")
.attr("x", inner.x + 28).attr("y", yScale(v) + 3)
.attr("text-anchor", "end")
.attr("fill", diagramTheme.edgeStroke).attr("font-size", 9)
.text(label);
});
[0, 2, 4, 6, 8, 10].forEach(p => {
sino.append("text")
.attr("x", xScale(p)).attr("y", chartBottom + 14)
.attr("text-anchor", "middle")
.attr("fill", diagramTheme.edgeStroke).attr("font-size", 9)
.text(`${p}`);
});
const wavePath = fn => {
const pts = [];
for (let p = 0; p <= 11.001; p += 0.25) pts.push([xScale(p), yScale(fn(p))]);
return d3.line()(pts);
};
sino.append("path")
.attr("d", wavePath(p => Math.sin(p)))
.attr("fill", "none")
.attr("stroke", diagramTheme.accent).attr("stroke-width", 2);
sino.append("path")
.attr("d", wavePath(p => Math.cos(p)))
.attr("fill", "none")
.attr("stroke", diagramTheme.primary).attr("stroke-width", 1.6)
.attr("stroke-dasharray", "5,3");
[2, 9].forEach(p => {
const v = Math.sin(p);
sino.append("line")
.attr("x1", xScale(p)).attr("x2", xScale(p))
.attr("y1", chartTop).attr("y2", chartBottom)
.attr("stroke", diagramTheme.highlight).attr("stroke-width", 1)
.attr("stroke-dasharray", "3,3");
sino.append("circle")
.attr("cx", xScale(p)).attr("cy", yScale(v)).attr("r", 4)
.attr("fill", diagramTheme.accent)
.attr("stroke", diagramTheme.bgOpaque).attr("stroke-width", 1.5);
sino.append("text")
.attr("x", xScale(p)).attr("y", v > 0.6 ? yScale(v) + 16 : yScale(v) - 8)
.attr("text-anchor", "middle")
.attr("fill", diagramTheme.accent).attr("font-size", 9).attr("font-weight", 700)
.text(`sin(${p}) = ${fmt(v)}`);
});
const legendY = chartBottom + 32;
[["dim 0 · sin(pos)", diagramTheme.accent, null], ["dim 1 · cos(pos)", diagramTheme.primary, "5,3"]].forEach(([label, color, dash], i) => {
const lx = inner.x + i * (inner.w / 2);
const swatch = sino.append("line")
.attr("x1", lx).attr("x2", lx + 22)
.attr("y1", legendY - 3).attr("y2", legendY - 3)
.attr("stroke", color).attr("stroke-width", 2);
if (dash) swatch.attr("stroke-dasharray", dash);
sino.append("text")
.attr("x", lx + 27).attr("y", legendY)
.attr("fill", diagramTheme.nodeText).attr("font-size", 9)
.text(label);
});
drawWrappedText(sino, "SLOWER DIMS VARY OVER LONGER SPANS", {
x: inner.x, y: legendY + 18, maxWidth: inner.w,
fontFamily: "var(--pg-mono)", lineHeight: 13, fontSize: 9,
fontWeight: 700, fill: diagramTheme.edgeStroke, anchor: "start"
});
drawWrappedText(sino, "FIXED WAVES · positions 2 and 9 read different values", {
x: inner.x, y: inner.y + inner.h - 23, maxWidth: inner.w,
fontFamily: "var(--pg-mono)", lineHeight: 13, fontSize: 9,
fontWeight: 700, fill: diagramTheme.highlightText, anchor: "start"
});
}
if (isRope) {
const rope = plot.append("g")
.attr("data-role", "position-rope")
.attr("role", "group")
.attr("aria-label", `RoPE rotation: feature pair [${ropePair.map(fmt).join(", ")}] rotated by position 2 to [${ropeRot(2).map(fmt).join(", ")}] and by position 9 to [${ropeRot(9).map(fmt).join(", ")}]`);
rope.append("text")
.attr("x", inner.x).attr("y", inner.y + 12)
.attr("fill", diagramTheme.primary).attr("font-size", 9)
.attr("font-weight", 700)
.text("ONE FEATURE PAIR · 0.30 rad per position");
const cx = inner.x + inner.w / 2;
const cy = inner.y + 140;
const R = Math.min(72, inner.w * 0.28);
const toXY = v => [cx + R * v[0], cy - R * v[1]];
rope.append("circle")
.attr("cx", cx).attr("cy", cy).attr("r", R)
.attr("fill", "none")
.attr("stroke", diagramTheme.nodeStroke).attr("stroke-width", 1);
rope.append("circle")
.attr("cx", cx).attr("cy", cy).attr("r", 2.5)
.attr("fill", diagramTheme.edgeStroke);
const base = toXY(ropePair);
rope.append("line")
.attr("x1", cx).attr("y1", cy).attr("x2", base[0]).attr("y2", base[1])
.attr("stroke", diagramTheme.edgeStroke).attr("stroke-width", 1.4)
.attr("stroke-dasharray", "4,3");
rope.append("circle")
.attr("cx", base[0]).attr("cy", base[1]).attr("r", 3)
.attr("fill", diagramTheme.bgOpaque)
.attr("stroke", diagramTheme.edgeStroke).attr("stroke-width", 1.2);
rope.append("text")
.attr("x", base[0] + 6).attr("y", base[1] - 6)
.attr("fill", diagramTheme.edgeStroke).attr("font-size", 9)
.text("x");
[[2, diagramTheme.primary, 1], [9, diagramTheme.highlight, -1]].forEach(([m, color, side]) => {
const pt = toXY(ropeRot(m));
rope.append("line")
.attr("x1", cx).attr("y1", cy).attr("x2", pt[0]).attr("y2", pt[1])
.attr("stroke", color).attr("stroke-width", 2.2);
rope.append("circle")
.attr("cx", pt[0]).attr("cy", pt[1]).attr("r", 4.5)
.attr("fill", color)
.attr("stroke", diagramTheme.bgOpaque).attr("stroke-width", 1.5);
rope.append("text")
.attr("x", pt[0] + side * 7).attr("y", m === 2 ? pt[1] + 14 : pt[1] - 6)
.attr("text-anchor", side > 0 ? "start" : "end")
.attr("fill", color).attr("font-size", 9).attr("font-weight", 700)
.text(`m = ${m}`);
});
const arcR = R + 12;
const arcPt = a => [cx + arcR * Math.cos(a), cy - arcR * Math.sin(a)];
const arcStart = arcPt(2 * ropeTheta);
const arcEnd = arcPt(9 * ropeTheta);
rope.append("path")
.attr("d", `M ${arcStart[0]} ${arcStart[1]} A ${arcR} ${arcR} 0 0 0 ${arcEnd[0]} ${arcEnd[1]}`)
.attr("fill", "none")
.attr("stroke", diagramTheme.edgeStroke).attr("stroke-width", 1)
.attr("stroke-dasharray", "3,3");
const arcMid = arcPt(5.5 * ropeTheta);
rope.append("text")
.attr("x", arcMid[0]).attr("y", arcMid[1] - 6)
.attr("text-anchor", "middle")
.attr("fill", diagramTheme.edgeStroke).attr("font-size", 9)
.attr("font-weight", 700)
.text("offset 7 x theta");
[[2, diagramTheme.primary], [9, diagramTheme.highlight]].forEach(([m, color], k) => {
rope.append("text")
.attr("x", inner.x).attr("y", cy + R + 24 + k * 18)
.attr("fill", color).attr("font-size", 9.5).attr("font-weight", 700)
.text(`m = ${m} → [${ropeRot(m).map(fmt).join(", ")}] · angle ${(m * ropeTheta).toFixed(2)}`);
});
drawWrappedText(rope, "ROTATION, NOT ADDITION · offset 7 decides the relationship", {
x: inner.x, y: inner.y + inner.h - 23, maxWidth: inner.w,
fontFamily: "var(--pg-mono)", lineHeight: 13, fontSize: 9,
fontWeight: 700, fill: diagramTheme.highlightText, anchor: "start"
});
}
if (!isLearned && !isSinusoidal && !isRope) {
const alibi = plot.append("g")
.attr("data-role", "position-alibi")
.attr("role", "group")
.attr("aria-label", "ALiBi bias: query at position 9 over keys 4 to 9 with slope 1/4; penalties grow with distance and the nearest key keeps penalty 0");
alibi.append("text")
.attr("x", inner.x).attr("y", inner.y + 12)
.attr("fill", diagramTheme.primary).attr("font-size", 9)
.attr("font-weight", 700)
.text("QUERY 9 · SLOPE 1/4 · KEYS 4 TO 9");
const barLabelW = Math.min(52, inner.w * 0.2);
const barValueW = 118;
const barGap = 6;
const barMaxW = inner.w - barLabelW - barValueW - 2 * barGap;
const barStart = inner.y + 34;
const barStep = 22;
const barRows = alibi.append("g")
.attr("data-role", "alibi-penalties")
.attr("role", "group")
.attr("aria-label", "ALiBi distance penalties for query 9");
alibiKeys.forEach((k, i) => {
const dist = alibiQuery - k;
const pen = alibiSlope * dist;
const nearest = dist === 0;
const y = barStart + i * barStep;
const row = barRows.append("g")
.attr("data-role", "alibi-penalty")
.attr("role", "group")
.attr("aria-label", nearest ? `Key ${k}: ALiBi penalty 0, no penalty` : `Key ${k}: ALiBi penalty -${pen.toFixed(2)}`);
row.append("text")
.attr("x", inner.x).attr("y", y + 12)
.attr("fill", diagramTheme.nodeText).attr("font-size", 9)
.attr("font-weight", nearest ? 700 : 500)
.text(`k = ${k}`);
if (!nearest) {
row.append("rect")
.attr("x", inner.x + barLabelW + barGap).attr("y", y)
.attr("width", Math.max(2, (pen / 1.25) * barMaxW)).attr("height", 14)
.attr("rx", 3)
.attr("fill", diagramTheme.accent);
}
row.append("text")
.attr("x", inner.x + barLabelW + barGap + barMaxW + barGap).attr("y", y + 12)
.attr("fill", nearest ? diagramTheme.highlightText : diagramTheme.nodeText)
.attr("font-size", 9).attr("font-weight", nearest ? 700 : 500)
.text(nearest ? "0.00 · full score" : `-${pen.toFixed(2)}`);
});
drawWrappedText(alibi, "SAME SLOPE RULE AT ANY DISTANCE · EXTRAPOLATES", {
x: inner.x, y: barStart + alibiKeys.length * barStep + 4, maxWidth: inner.w,
fontFamily: "var(--pg-mono)", lineHeight: 13, fontSize: 9,
fontWeight: 700, fill: diagramTheme.edgeStroke, anchor: "start"
});
drawWrappedText(alibi, "DISTANCE PENALTY · nearest key keeps its full score", {
x: inner.x, y: inner.y + inner.h - 23, maxWidth: inner.w,
fontFamily: "var(--pg-mono)", lineHeight: 13, fontSize: 9,
fontWeight: 700, fill: diagramTheme.highlightText, anchor: "start"
});
}
const metrics = m04Tools.card(svg, l.metrics, {
role: "metrics-card", title: "Method readout"
});
metricRows.forEach(([label, value], i) => {
const metric = metrics.append("g")
.attr("data-role", "method-readout")
.attr("role", "group")
.attr("aria-label", `${label}: ${value}`);
const y = 48 + i * 30;
metric.append("text")
.attr("x", 14).attr("y", y).attr("fill", diagramTheme.edgeStroke)
.attr("font-size", 9).attr("font-weight", 700).text(label.toUpperCase());
drawWrappedText(metric, value, {
x: Math.min(144, l.metrics.w * 0.48), y,
maxWidth: l.metrics.w - Math.min(144, l.metrics.w * 0.48) - 14,
fontFamily: "var(--pg-mono)", fontSize: 10.5, fontWeight: 600,
fill: diagramTheme.nodeText, anchor: "start"
});
});
const analysis = m04Tools.card(svg, l.analysis, {
role: "analysis-panel", title: method, accent: diagramTheme.primary
});
drawWrappedText(analysis, analysisText, {
x: 14, y: 50, maxWidth: l.analysis.w - 28,
fontFamily: "var(--pg-mono)", lineHeight: 15, fontSize: 10.5,
fill: diagramTheme.nodeText, anchor: "start"
});
const verdict = m04Tools.card(svg, l.verdict, {
role: "verdict-card", title: "Where this leads", accent: diagramTheme.highlight
});
drawWrappedText(verdict, verdictText, {
x: 14, y: 48, maxWidth: l.verdict.w - 28,
fontFamily: "var(--pg-mono)", lineHeight: 15, fontSize: 10.5,
fontWeight: 600, fill: diagramTheme.nodeText, anchor: "start"
});
return svg.node();
}The visual keeps each method’s embeddings.py API name and shape contract next to its mechanism. TransformerEmbedding combines only the learned or sinusoidal input variant with dropout; RoPE and ALiBi act later, where Module 05 compares queries, keys, and scores.
Optional production context
A token table contains V × D parameters, so vocabulary size and vector width affect memory. A larger vocabulary can shorten token sequences but enlarges the table. Some models tie the input table to output-vocabulary weights; padding_idx and sparse-gradient settings are implementation choices rather than changes to the direct-lookup idea. In a production model, D is high-dimensional and inspection usually uses projections or similarity tools; it is not a list of D human-labeled semantic axes.
Common Pitfalls
When working with embeddings, watch out for these issues:
- Treating ID size or ID gaps as meaningful quantities: the numbers are addresses; arithmetic on them manipulates slots, not tokens.
- Calling the ID itself an embedding: the embedding is the selected row vector, not the integer that found it.
- Assuming every table row changes on every optimizer step: only rows the loss used receive gradients; the rest stay fixed.
- Reading a toy two-dimensional map as a literal or universal map of meaning: the 2D coordinates are teaching aids, and similarity readouts are inspection evidence, not proofs.
Going Deeper
Core Papers:
- Efficient Estimation of Word Representations in Vector Space — The early demonstration that prediction training can arrange word vectors into useful geometric relationships.
- Attention Is All You Need — The original Transformer, and the source of the sinusoidal position signals previewed above.
- RoFormer and ALiBi — The rotation and bias position methods to revisit once attention mechanics are in place.
Practical Resources:
- PyTorch
nn.Embedding— The direct-lookup API used in the bridge above, with its padding and sparsity options.
Summary
Key takeaways:
- ID = address — A token ID is a categorical slot number; its size and its gaps from other IDs carry no meaning.
- Row = parameters — Each ID selects one row of the shared
[V, D]table, whose values training may adjust. - Vector = embedding — The selected row’s values leave the table as a vector the rest of the model can use.
eₖᵀ E = E[k]states the same lookup as a one-hot equivalence, whilenn.Embeddingindexes the row directly. - Training = changes rows — Loss sends gradients to participating rows only, so an optimizer step moves the addressed rows while token IDs stay fixed;
[B, T]IDs return[B, T, D]vectors with repeated IDs retrieving identical rows. - Geometry = consequence of learning — Cosine neighborhoods are inspection evidence and hypothesis-generating probes, not literal or universal meanings. Token identity alone also leaves order unresolved, which is why position needs separate machinery.
What’s Next
Module 05: Attention starts with these embedding vectors and shows how tokens compare and combine information. It also supplies the machinery needed to understand why modern position methods act on attention rather than on token IDs.