| name | distill-figure |
| description | Create Distill-quality interactive, detailed, clear figures for articles/posts in any domain — linked diagrams (graph/matrix/text with synced highlighting), step/scrub/animation, editable-input-driven, and statistical panels with brushing. Use when the user wants a rich interactive visualization, an explorable explanation, a "Distill-style" or publication-quality figure, or to embed an interactive chart/diagram into writing. Ships a zero-build d3 v7 component library plus an automated render→screenshot→self-critique loop so figures come out high-quality reliably. |
distill-figure
Generate detailed, interactive, clear figures in the style of Distill articles
(e.g. https://distill.pub/2021/gnn-intro/) — reusable across any domain's writing.
The hard part of figure-making for an LLM is that you can't see your output. This skill
solves that with three things: (1) a component library that bakes in Distill's
visual + interaction language, (2) reference docs encoding the principles, and
(3) a render→screenshot→critique loop so you visually verify and iterate.
Skill home: ~/.claude/skills/distill-figure/
lib/ — zero-build d3 v7 ESM components (see Toolbox below)
templates/ — figure.html (scaffold), figure.embed.html (paste-into-article snippet)
references/ — design-principles.md, archetypes.md, quality-checklist.md
tools/render.py — the screenshot/critique loop (Python Playwright + Chrome)
examples/markov-chain.html — a complete worked figure (read it as a model)
examples/sir-network.html — a second worked figure (force graph + step player +
linked line chart + tooltips + parameter slider; the "compose everything" model)
examples/repro-momentum.html / examples/repro-gp.html — continuous-math models
(PlotView): contour field + trajectories + click-to-restart; posterior band +
samples + click-to-add-data (real GP math)
Workflow — follow this every time
- Clarify intent. What concept? What's the one takeaway / "aha"? What data backs
it? What should the reader do? (Ask only if genuinely ambiguous.)
- Pick archetype(s). Read
references/archetypes.md and choose 1+ (they compose):
linked diagram · step/animate · editable input · statistical+brush.
- Scaffold. Copy
templates/figure.html to the target location and copy lib/
next to it (delivery wants lib/ adjacent; imports are ./lib/…). For figures inside
this skill, import ../lib/… and serve from the skill root.
- Build using the Toolbox +
references/design-principles.md (tokens, linked hover,
determinism, responsive). Read examples/markov-chain.html for a concrete pattern.
- Run the loop:
python3 tools/render.py <figure.html> --root <web-root>
It screenshots at 380/720/1100px and captures a hover state (mark one element with
data-hover-demo). Output PNGs print at the end.
- Critique + iterate. Read every PNG, score against
references/quality-checklist.md, fix, re-render. Don't stop until sections A–C pass.
If the figure is interactive, also run the interaction probe — a screenshot does NOT
prove interactivity:
python3 tools/probe.py <figure.html> --root <web-root>
It drives real hover/click/brush/control events on actual marks — d3 marks (.node,
rect.cell, .pill, [data-probe]) probed per mark type AND Vega marks — flips every
control to a different value, clicks inactive tabs, and reports whether the DOM reacts.
Fix any ✗. It also fails loudly if it finds no interactive surface at all. (Charts from
charts.js are interactive by default: tooltips, hover emphasis, legend highlight,
optional brush, and onClick/onHover/onBrush callbacks. Hand-rolled d3 marks should get
a data-probe attribute so the probe can drive them.)
- Deliver the self-contained HTML (+
lib/). Offer templates/figure.embed.html
as the snippet to paste into the user's article, and remind them it must be served
over http(s), not file://.
Toolbox (lib/, import on demand)
- figure-kit.js —
installKit(d3) adds .at/.st/.translate/.appendMany; tokens
& color(i) (the palette — use these, never raw hex); makeRng(seed) deterministic
RNG; conventions(d3,{...}) responsive SVG; range/sleep/oneSwap.
- linked-highlight.js —
createHighlightStore() shared hover/select bus; the core
of synced multi-view highlighting. matches() helper.
- graph-view.js —
GraphView node/edge renderer (circular or supplied positions,
directed arrows, hover→store, neighbor emphasis). Options: force:true (d3-force
layout) + draggable (drag nodes), settle:true (compute the force layout
synchronously → deterministic resting frame), charge/linkDistance, labels:false,
edgeColor, onNodeClick(d) (override click), tooltip:(n)=>html (floating detail
card on hover), and per-node n.color/n.stroke.
- connectors.js —
ConnectorLayer draws animated lines/arrows BETWEEN elements that
live in different (even CSS-3D-transformed) sub-views, via a measured overlay SVG. The
"trace across panels/layers" idiom (see examples/repro-layerwise-trace.html).
- matrix-view.js —
MatrixView adjacency/heatmap with row/col labels + linked hover.
diagLabels:true places labels on the diagonal (the permutation-matrix idiom);
tooltip:({r,c,v})=>html shows a detail card on cell hover.
- token-strip.js —
TokenStrip a row of word/token "pills" (sized to text, optional
arrows) with linked hover; the text-as-graph / sequence idiom. Pairs with MatrixView +
editable-input. Hovering a token lights up its row+column cross in a linked matrix.
- controls.js —
StepPlayer (play/step/scrub), slider, button, tabs
(switch between representations of one thing — the graph-to-tensor idiom;
emits [role=tab] so the probe exercises it). CAUTION: StepPlayer fires
onStep(0) during construction — build your data first, or pass initial:null
and call .goto(i) once ready.
- plot-view.js —
PlotView continuous XY plot: axes + clipped overlay layers
(.line(id,pts), .band(id,{x,lo,hi}), .points(id,pts,{tooltip,onClick})), an
optional scalar field f:(x,y)=>v rendered as filled contours, plus onClick(x,y)
and readout:(x,y,v)=>html on the canvas itself (hit-rect ships data-probe).
The workhorse for continuous math: loss landscapes, posteriors/credible bands,
phase portraits, supply-demand curves. See examples/repro-momentum.html (field)
and examples/repro-gp.html (pure XY).
- tooltip.js —
getTip(d3) one shared floating tooltip for d3 views (Vega charts
already have their own). GraphView/MatrixView wire it via their tooltip: option;
use getTip directly for hand-built marks.
- editable-input.js —
editableText, tokenField (live recompute from user text).
- annotate.js —
annotate() curved arrows + italic labels.
- charts.js —
scatter/line/bar/boxplot/spec Vega-Lite wrappers, interactive
by default (tooltips, hover emphasis, legend highlight). scatter supports logX/logY,
xTitle/yTitle, zoom:true (drag-pan + wheel-zoom, with onZoom), brush:true
(drag-select, with onBrush), and onClick/onHover. line (curves/time series/scaling
laws) adds colorDomain/colorRange to pin series colors to a linked d3 view's node
colors, plus onClick (e.g. click a point → jump a StepPlayer). Needs the vega <script>s from
templates/figure.html — use the explicit /build/…min.js URLs (the bare
npm/vega@5 URL 400s). Charts auto-fall-back to a fixed width if their container
collapses to 0.
- theme.css — base card styling + CSS tokens mirroring figure-kit.
Rules
- Use palette tokens, never invent colors. Seed all randomness.
- Every interaction must teach; the resting state must read correctly on its own.
- Verify by screenshot before claiming done — the critique loop is mandatory, not optional.
- Keep it zero-build and self-contained (one HTML +
lib/); don't add frameworks/bundlers.
Notes
- The render/probe tools are Python Playwright + Chrome via
channel="chrome" (no
browser download needed). Any python with playwright installed + a system Chrome
works: pip install playwright. The figures themselves need NOTHING — any static
http server serves them (python3 -m http.server).