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:

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:

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:

CSS: Use the existing <FullBleedSection> component. Don’t reinvent.

Decision framework

Answer three questions, in order:

  1. Is the figure smaller than the prose measure?Inline. Don’t widen something that doesn’t need to be wider.
  2. 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.
  3. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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:

  1. 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)
  2. Pulse animation uses durations.slow or durations.moderate, easing easings.default. Opacity oscillates 0.4 ↔ 1.0.
  3. Disappears on first interaction — once the user has engaged, the prompt never returns during the session.
  4. 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.

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:

  1. 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.
  2. 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:

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:

  1. 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 use viz.bg.canvas.dark.
  2. 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.
  3. 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

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:

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.ymlviz.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

  1. Dark Canvas on white page with no frame — reads as a foreign object, not a designed element
  2. Inline <figure style=> in MDX — bypasses the tokens entirely; use the <CanvasFrame> component in www or equivalent
  3. Different border-radius per visualization — breaks the “same kind of element” signal
  4. 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-word claim, or none.
  5. Interaction without affordance — a slider that’s not obviously a slider is a broken design, not a minimalist one
  6. 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.