Interactive Visualization Framing
When a Canvas-based visualization (phase portrait, waveform, harmonic spectrum, resolution comparison) appears in a page or blog post, it needs visible chrome that signals “this is a designed element you can interact with” — not a raw dark rectangle dropped into the page.
Why this spec exists. On feat/visual-signature-components (www repo, Z2O-1182), the first placement of Lissajous figures and ResolutionComparison on /platform/signals was rejected: “jarring in the middle of the page… no bordering or signaling visually to illustrate that it’s something that should be interacted with.” Every subsequent visualization author then re-derived the same framing decisions (border radius, padding, caption placement, interaction affordance). This spec codifies those decisions so the next author doesn’t re-derive them and so review effort moves to content, not container chrome.
Maturity: convention — the pattern passed one adversarial review round on Signals. Graduate to rule after it lands on a second real surface without substantive revision.
Container chrome
border: 1px solid var(--viz-grid) /* color.viz.grid — neutral-800 */
border-radius: 0.75rem
padding: 1.5rem /* desktop */
1rem /* mobile (<768px) */
background: var(--viz-bg-canvas-dark) /* color.viz.bg.canvas.dark by default */
var(--viz-bg-canvas-light) /* alternative for light-surface pages */
The frame’s background is ONE of the two Canvas surfaces — never an ad-hoc gray. If the visualization is rendering on a light page, the whole frame goes light. Mixing a dark Canvas inside a light frame, or vice versa, is rejected (see “Light vs. dark placement” below).
Minimum Canvas size
| Use case | Minimum edge | Notes |
|---|---|---|
| Grouped specimens (e.g., 3 × Lissajous) | 180 px square | 140 px was tried and rejected — too small for trace detail |
| Single hero visualization | 320 px wide | Width-bounded by container, height by viz type |
| Inline blog asset | 240 px wide | Reading-surface scale; the picture still has to argue at this size |
These are minimums, not targets. When space allows, render larger.
Width patterns
Body text follows the 65-68ch measure (see foundations/typography.md § Body Measure). Visualizations don’t have to honor the reading column — in fact, most shouldn’t. Canvas data is dense, needs horizontal room to breathe, and gets diminished when clamped to 520-580px.
Three canonical patterns let figures escape the prose column without abandoning its center-line. Each viz declares which pattern it uses; consumers don’t invent new widths.
Pattern 1: Inline
Matches the prose measure exactly. The figure flows with the text.
When to use:
- Small figures or thumbnails that should read as part of the paragraph
- Inline formulas or equations
- Code blocks
- Sparklines smaller than ~240px
CSS:
.viz-inline { width: 100%; max-width: 100%; }
/* The viz inherits the prose column's 65-68ch */
Pattern 2: Breakout
Widens to the content column (~896px, matching categories/web-components/page-sections.md standard), centered on the viewport. Common editorial pattern — text narrow, figures wider, explicit visual signal of “this is evidence, not body text.”
When to use:
- Canvas visualizations with internal structure (LissajousFigure grid, HarmonicSpectrum bars)
- Interactive widgets (ResolutionComparison slider)
- Tables with 4+ columns
- Comparison diagrams
- Screenshots and product imagery
CSS:
.viz-breakout {
width: 100%;
max-width: min(896px, calc(100vw - 2rem));
/* Break out of the narrow parent without destroying the center line */
position: relative;
left: 50%;
transform: translateX(-50%);
}
This is the default for <CanvasFrame> on platform/case-study pages. Honors the 75ch body rule (text stays narrow) while giving figures the horizontal room Canvas data needs.
Pattern 3: Full-bleed
Spans the full viewport width edge-to-edge.
When to use:
- Demonstrate-arc pages (Technology, Signals deep-dive sections)
- Pretext text effects where the heading IS the visualization
- Dark evidence sections with ambient Canvas layers behind content
- Hero moments that want to feel cinematic
CSS: Use the existing <FullBleedSection> component. Don’t reinvent.
Decision framework
Answer three questions, in order:
- Is the figure smaller than the prose measure? → Inline. Don’t widen something that doesn’t need to be wider.
- Does the figure carry narrative weight — is it evidence the reader is expected to stop and examine? If yes, → Breakout. Give it room. If no (it’s decoration or reference), → Inline.
- Is this the narrative climax of the section — the moment the page earns its point? Very rare. → Full-bleed. But only if the page’s composition arc is on the Demonstrate track. If it’s Persuade/Convert/Inform, breakout is the right ceiling.
Anti-patterns
- Ad-hoc widths. If a viz is 800px wide on one page, 940px on another, 720px on a third, that’s drift. Pick one of the three patterns and commit.
- Full-bleed on Persuade/Convert pages. Full-bleed is tonal — it says “stop and look.” Using it on a conversion surface competes with the CTA. Reserve for editorial/demonstrate moments.
- Breakout stacked against breakout. Two breakout figures in the same section effectively create a wide “block” surrounded by narrow text. Usually reads as one composite figure. If that’s intentional, use
<FullBleedSection>as a single wrapping element. If not, space them out or inline one. - Breakout without a real viz inside. If the “figure” is a stock image, a generic graphic, or text-heavy content, it probably belongs in the prose column. Breakout is for dense Canvas data.
Interaction affordance
If the visualization accepts user input (slider, drag, hover-driven reveal), there MUST be a visible prompt that the interaction exists. Required:
- Pulsing text prompt visible before first interaction, hidden after. Examples:
← drag to reduce resolution →(ResolutionComparison)move the slider(sampling-rate controls)drag to compare(before/after splits)
- Pulse animation uses
durations.slowordurations.moderate, easingeasings.default. Opacity oscillates 0.4 ↔ 1.0. - Disappears on first interaction — once the user has engaged, the prompt never returns during the session.
- Reduced-motion fallback — when
prefers-reduced-motion: reduce, the prompt is rendered static (no pulse), text unchanged.
Caption
The picture is the argument. The caption is at most a <=7-word claim label about the customer’s world, and a strong viz may carry none. The visualization itself must make the point at a glance; the caption is a label, not the place the argument finally happens.
This inverts the earlier rule (“the caption is the argument”). Captions that carry the argument are a symptom: the picture isn’t arguing, and prose is patching the gap. The named exemplars (Stripe, Linear, Vercel, Cloudflare, Datadog) ship product visuals with a roughly 4-word claim heading and zero visible captions. Mark Chung’s instrument-grade artifacts read the same way: the trace and the number do the work. Match that bar.
- What the caption may be: a
<=7-wordclaim about the customer’s world. “Degrading rectifier, 21 days early.” “1 Hz misses what 8 kHz reveals.” A claim, not a description of the artifact. - What the caption is NOT: a description of the chart (“phase portrait showing load currents”), a legend in prose, a “how to read this” instruction, or an “illustrative / representative / example” disclaimer. Those belong in alt text or, for the legend, inside the visual. See
categories/composition/communicate-at-a-glance.mdfor the full meta-language ban. - None is allowed, and often best. If the form already argues, drop the caption.
- Voice: plain-language, present tense, no title-case, no marketing copy
- Typography: Inter 0.8125rem,
color.viz.text.metaon dark,color.neutral.500on light - Placement: directly below Canvas, separated by
0.75rempadding-top with a1pxtop border invar(--viz-grid) - Dynamic state: when the visualization’s state changes (e.g., slider moves), the on-canvas readout (label, axis, value) updates. The caption claim stays a fixed claim label; live state lives inside the visual, not in caption prose.
The cover-the-caption test
Mask the caption. If the picture stops arguing, the caption was doing the work, and the picture is what needs fixing, not the caption. A visualization that only makes its point with the caption attached has failed the squint test. Redesign the form: add the comparison, label the axis inside the canvas, mark the fault, surface the one number. Then restore a <=7-word claim label, or none.
“How to read this” is a smell, not a fix
If a visualization needs a “how to read this” sub-caption, interaction narration (“drag the slider”, “toggle to compare”, “hover to inspect”), or a paragraph of explanation to be legible, the form is wrong. Redesign it before you caption it:
- First resort: fix the picture. Make the encoding self-evident. Put the legend inside the visual where the series are. Label the axis on the axis. Mark the anomaly where it happens. Make the affordance look like what it does (a slider that obviously slides) instead of narrating that it exists.
- Last resort only: if a genuinely novel encoding cannot be made self-evident and the page still needs it, a single short “how to read” line is tolerated as a temporary crutch. Treat it as debt, not a pattern. It is never the prescribed fix, and it never substitutes for the redesign.
This does not relax the interaction-affordance requirement below: a slider still needs a visible, pulsing prompt that it is interactive. The prompt is an affordance (“this moves”), not narration that explains how to interpret the result.
Accessibility
All items required, not optional:
role="img"on the<canvas>elementaria-labelthat reflects the current state (not a fixed description). Updates when state changes.- For interactive controls:
aria-valuetexton sliders, describing the current value in human terms (e.g.,"8000 samples per second") - Focus ring on any interactive control:
2px solid var(--viz-trace-primary),4pxoffset - Keyboard parity: anything a mouse/touch can do, keyboard can do (arrow keys on sliders, tab to focus)
- Respects
prefers-reduced-motion: reduce— on-entry animations become instant; pulse prompts become static; slider thumb has no transition
Light vs. dark placement — the adjacency rule
Never stack a light-surface viz frame directly adjacent to a dark-surface viz frame within the same section. This produces the “visual whiplash” observed on Signals where three light-background Lissajous figures sat inches above a dark-background ResolutionComparison.
Correct approaches:
- Pick one treatment per section. If the section’s background is light, all visualizations inside it use
viz.bg.canvas.light. If the section is dark, all useviz.bg.canvas.dark. - Use a section boundary. If you need to change treatment, put the adjacent viz in a separate section with its own composition role (Evidence → Turn, or Evidence → Proof). The background change then reads as intentional narrative beat, not accident.
- Use the same frame chrome. Whatever the Canvas background, the frame (border, radius, padding) stays identical — consistency of chrome is what communicates “these are the same kind of element.”
When to NOT use this framing
- Static SVG diagrams — they don’t need interaction affordance; use the illustration category rules instead
- Decorative waveforms in hero backgrounds — they’re part of page chrome, not a discrete element; no frame
- Sparkline-sized charts inline in prose — too small for a frame; inherit from surrounding prose typography
Related tokens
| Token | Purpose |
|---|---|
color.viz.bg.canvas.{dark,light} |
Frame background |
color.viz.grid |
Frame border + in-Canvas grid |
color.viz.trace.primary |
Focus ring + slider fill + interaction prompt color |
color.viz.text.{label,meta} |
In-Canvas typography + caption |
durations.slow / durations.moderate |
Pulse prompt cycle |
easings.default |
Pulse prompt easing |
durations.reveal / durations.revealLong |
Viewport-entry animation |
easings.revealCubic |
Viewport-entry animation easing |
Source-of-truth implementations
| Specimen | Current React source | Page |
|---|---|---|
| Phase portrait (Lissajous) | LissajousFigure.tsx in www |
/platform/signals |
| Waveform trace | WaveformTrace.tsx in www |
(not yet placed) |
| Harmonic spectrum | HarmonicSpectrum.tsx in www |
(not yet placed) |
| Resolution comparison | ResolutionComparison.tsx in www |
/platform/signals |
See individual specimen docs in this folder for per-viz specifications.
One argument per chapter
Every canonical brand visualization carries a specific argument. The argument, not the shape, is what belongs to each page.
| Visualization | Argument |
|---|---|
| Phase Portrait (Lissajous) | “Every load has a unique electrical fingerprint.” General; reusable where fingerprinting is the point. |
| Resolution Comparison | “1 Hz misses what 8 kHz reveals — diagnostic analysis requires the resolution to exist.” Specific; reserved for Intelligence/analysis pages. |
| Waveform Trace | “The meter captures real harmonic structure at high sampling density.” Specific; reserved for Hardware / measurement pages. |
| Harmonic Spectrum | “The frequency-domain reveals what the time-domain hides.” Specific; reserved for diagnostic / analysis pages. |
| Circuit Topology Map | “Monitoring spans the full power chain hierarchy, with fault propagation visible at every node.” Specific; reserved for Infrastructure / system pages. |
| Training Pulse Animation | “AI workloads have unpredictable power demands that standard metering can’t track.” Specific; reserved for AI-factory / GPU pages. |
| Measurement Bar Reveal | “The measurement IS the meaning.” Brand signature; reserved for demonstrate-arc moments. |
Rule: when placing a visualization on a page, the visualization’s argument and the page’s argument must match. Reusing a visualization on a second page is only legitimate if its argument genuinely applies to both — not because the visual looks cool in that slot.
Why this matters
The site is a narrative. Each chapter (page) makes a specific argument. If two chapters use the same visualization to carry the same claim, the reader moving from one to the other reads the same paragraph twice — the second chapter feels like filler, the first chapter feels less earned.
Concrete example from a real adversarial review (2026-04-24): we placed ResolutionComparison on both /platform/signals and /hardware/ev2. Both pages had the “1 Hz vs 8 kHz” framing, but:
- Signals needed the diagnostic payoff (health ratio revealing aging). That’s ResolutionComparison’s argument.
- EV2 needed the sampling-density story (the hardware captures real structure). That’s
WaveformTrace’s argument.
Borrowing ResolutionComparison for EV2 stole Signals’ punchline to make a hardware point that didn’t actually need it. Fix: WaveformTrace on EV2, ResolutionComparison stays exclusive to Signals. The two pages now each own their distinct argument, and a reader moving from EV2 → Signals experiences earned escalation (“we showed you what we capture; now let’s show you what we can find”).
General-argument visualizations can appear on multiple pages
LissajousFigure is general — “every load has a unique fingerprint” applies anywhere that fingerprinting is relevant. Appearing on Signals + on the homepage + in a blog post about load classification is fine.
Specific-argument visualizations (ResolutionComparison, WaveformTrace, CircuitTopologyMap, TrainingPulseAnimation) are single-page until explicit design review concludes otherwise.
Enforcement
See rules/visual-rules.yml → viz.one-argument-per-chapter for the machine-enforceable rule. An evaluator pass compares each viz’s canonical argument (from the table above) against the surrounding page copy’s argument; duplicates on different pages with the same argument fail the rule.
Anti-patterns
- Dark Canvas on white page with no frame — reads as a foreign object, not a designed element
- Inline
<figure style=>in MDX — bypasses the tokens entirely; use the<CanvasFrame>component in www or equivalent - Different border-radius per visualization — breaks the “same kind of element” signal
- Caption doing the picture’s job. If the visualization only makes its point with the caption attached (fails the cover-the-caption test), fix the picture, not the caption. Generic captions (“Figure 1”, “chart showing data”), artifact descriptions, “how to read this” instructions, and “illustrative / representative” disclaimers are all meta-language and are banned (see
categories/composition/communicate-at-a-glance.md). A caption is at most a<=7-wordclaim, or none. - Interaction without affordance — a slider that’s not obviously a slider is a broken design, not a minimalist one
- Same viz argument reused across chapters — see “One argument per chapter” above. If you catch yourself placing the same interactive twice to make the same point, one of the two placements is wrong.