# Corpus dataviz — implementation spec (`corpus.html`)

**Task #64, Phase 2 (infodesign).** Confirmed design brief from the `/impeccable shape`
run on 2026-06-06. Phase 1 (data-scientist EDA) is DONE — `tide_eda.py` →
`eda_report.json`. This doc is the handoff so the build can start clean post-/compact.

## What we're building
A standalone, scroll-driven **data essay**: *"ParVagues, by the numbers."* 6 curated
stories from `eda_report.json`. Prototype at `armada/tide-table/corpus.html` (served by
`serve.py`, `:8731/corpus.html`); built **self-contained static** so it deploys to
**`me.nech.pl/parvagues/viz`** (a tiny build step inlines `eda_report.json` + `tokens.json`
into the HTML → one portable file; relative asset paths).

## Register & direction (from PRODUCT.md / DESIGN.md — the Ship's Bridge)
- **Editorial scrollytelling**, public-facing → narrative + shareable, but on the dark
  instrument language (not SaaS slop; see PRODUCT.md anti-references).
- **Color: Restrained chrome + Full-palette data.** Surface `#0a0a0a`, ink `#e8e8ea`,
  brand magenta `#d900ff` reserved for ONE earned accent (the breakout beat). The
  **sample-family fleet colors carry the charts** — this viz is the palette's showcase.
- **Never hue alone** (Principle 4): every series keeps glyph + label. Glyphs/colors come
  from `tokens.json` (`sample[].glyph/base/shades`, `role`, `agree`).
- **Type:** Geist (display/body) + Geist Mono (all data, bpm, mini-notation). Scene: dark,
  contrast-hardened to survive a daylight phone. Anchors: Information is Beautiful, The
  Pudding, Ableton.
- **Voice:** "by the numbers" masthead; terse maker-voice, **first-person where it fits**,
  abstract narrator otherwise.

## Data source — `eda_report.json` field map (exact)
- `coverage` → masthead counts: `tracks`(73), `canonical_gigs`(37), span 2021–2026.
- **Story 1 The Creep:** `tempo.creation_tempo_by_year` {2021:110…2026:126} = STUDIO line.
  STAGE line = per-gig median (recompute from gig dates × track tempo, OR add to report in
  build step). `tempo.by_creation` [{name,track,created,bpm,morph}] = scatter points.
  `tempo.ac_delta` [{track,score_bpm,meta_bpm,delta}] = the felt-vs-written 2× inset.
  `tempo.histogram` {80:8…} = distribution ridge. `tempo.median`=120, `morphing_tracks`.
- **Story 2 Breakout:** `vocabulary_growth` {YYYY-MM:n} (334 @ 2024-07) = bars;
  `cadence` {2022:2…2026:3} = gig step/area. Shared time axis.
- **Story 3 Palette:** `families` {break:107…} = proportion (treemap/stacked bar), color =
  `tokens.json sample[key].base`, glyph = `sample[key].glyph`. `palette_top` {sound:n} =
  top-sounds strip. Note honest "+unclassified" slice.
- **Story 4 Accent:** `idioms_top` [{norm,n_tracks}] (f*16 ×62) = horizontal bars, label =
  `norm` in mono. `idioms_counts` {shared,repeated,total} = context line.
- **Story 5 Collab fingerprint:** `collab_fingerprint` {who:{n,bpm_median,bpm_min,bpm_max,
  distinctive_samples:[{sound,lift,n}]}}. Small-multiples: bpm range strip + tell chips.
  raph ~138 [102–170] vs nova(solo) ~117. `collab` {who:count} for sizing.
- **Story 6 Set-staples:** `recurrence_top` [{name,gigs,track}] (Sunny Side Up ×11). Ranked
  bars; highlight the Café trilogy (Tiède/Glacé/Bouillant) as a suite.
- (Held back this pass: `pairings_top`, `styles`, agreement landscape — for a v2 / the
  pairing explorer.)

## studio↔club toggle (first-class lens)
Sticky mini-control top-right. **studio** = creation-date dimension (when written);
**club** = gig-date dimension (when performed). Story 1 responds live (swap the active
line/x-basis); Story 2 partially. Single-lens charts show the toggle **disabled + tooltip**
("score-derived: one lens only") — honest per Principle 1, never fake a dimension.

## Chart inventory (all vanilla SVG, no chart lib)
1. dual line + scatter + small inset (2× conflicts) + ridge histogram
2. combo: month columns (vocab) + year step (gigs) on shared time axis, magenta breakout flash
3. treemap or stacked-proportion bar, 12 families, fleet colors + glyph legend
4. horizontal bar, mono mini-notation labels
5. small-multiples: horizontal bpm range strips + sample chips
6. ranked horizontal bars, suite-grouped

## Tech / states / motion
- Self-contained HTML; vanilla JS + small SVG helpers; **zero npm deps** (like triangle.html).
- Build step (in `tide.py` or a `--inline` flag) embeds the two JSONs for deploy.
- Static-first render; IntersectionObserver draws charts in on enter (150–250ms ease-out,
  stagger within a chart only). `prefers-reduced-motion` → instant. Reveal only ENHANCES an
  already-visible default (no content gated on transitions — headless/deploy safe).
- Load-fail → in-page banner with the fix (mirror triangle.html). Sparse series (1-track
  collab) shown labeled, not hidden. Mobile single-column, charts reflow ≤360px.
- Contrast ≥4.5:1 body; mono data legible on dim monitor + bright phone.

## Build order (post-compact)
1. Scaffold `corpus.html` shell: masthead + 6 `<section>`s + sticky toggle + token load +
   load-fail banner. 2. SVG chart helpers (axis, bars, line, treemap). 3. Stories 1→6 in
   narrative order, each headline + chart + annotation. 4. studio/club wiring. 5. motion +
   reduced-motion + mobile reflow. 6. `--inline` deploy build. 7. (later) fold into #63 SPA.
   Verify visually via `serve.py` (PLN eyeballs — no screenshot tool here).

## Open / deferred
- STAGE-tempo line needs per-gig median (compute in build step from gig date × tempo).
- Deploy plumbing to `me.nech.pl/parvagues/viz` is a separate step (static copy).
- v2 cuts: pairing explorer (`pairings_top`), style mix, agreement landscape.
