Skip to content

Latest commit

 

History

History
140 lines (116 loc) · 6.9 KB

File metadata and controls

140 lines (116 loc) · 6.9 KB

2026-08-23 — Site rebuilt from scratch, and the palette turned out to be a constraint on the ring

DDIA: Ch 2, still ~halfway (no reading today) Plan items: none directly — this is groundwork for #1

What I did

Added four skill packages scoped to this repo (.claude/skills/, gitignored, restorable from the committed skills-lock.json): Leonxlnx/taste-skill (13 skills), vercel-labs/agent-skills → web-design-guidelines, a curated 10 of bergside/awesome-design-skills, and @playwright/cli whose SKILL.md ships inside playwright-core.

Then rebuilt site/ from scratch against the taste skill. Four decisions were made deliberately and should not be re-litigated:

Decision Choice
Visual language Instrument. Mono for everything structural and numeric, sans for prose only
Homepage Live figure first, index below
Theme Both, tokenised now
Scope Per-visualization write-ups, published journal, DDIA progress, about page

The site is now: token layer → base layer → layout layer, no CSS framework, no component library. Homepage, visualization page, journal index, journal entry, about, 404. Journal entries are read from journal/*.md at the repo root via import.meta.glob and inlined at build time, so publishing an entry is just writing it.

Findings

The categorical palette is a hard constraint on the ring, and the number is three. This is the finding that actually matters, because it changes what visualization #1 has to draw. Ran the dataviz validator against this site's own surfaces:

Slots in play Pairs checked Result
1-8 adjacent PASS — worst CVD ΔE 9.1 light / 8.4 dark
1-3 all pairs PASS — worst CVD ΔE 9.4, normal-vision ΔE 20.9
1-4 all pairs FAIL — yellow↔orange, normal-vision ΔE 10.6 (floor is 15)
1-5 all pairs FAIL — magenta↔aqua, deutan ΔE 1.6

A ring places nodes by hash, so any two nodes can end up visually adjacent. That is the all-pairs case, not the adjacent case. So a four-node ring already has a pair a full-colour reader struggles with, and a five-node ring has a pair that is effectively identical to a red-green colourblind viewer (ΔE 1.6 on a scale where 15 is the floor).

Consequence for #1: every arc gets a direct label. Colour is a secondary cue past three nodes, never the identity. This is encoded in theme/vizTokens.ts as COLOR_ALONE_SERIES_LIMIT with a requiresDirectLabels() helper, so the renderer cannot quietly forget it.

Command, so it does not need re-deriving:

node .../dataviz/scripts/validate_palette.js \
  "#3987e5,#d95926,#199e70,#c98500" --mode dark --surface "#131417" --pairs all

Canvas renderers must read colours from CSS, and the bridge is written. A canvas has no cascade, so the obvious thing is ctx.fillStyle = '#7cb7ff'. Do that once per visualization and theming is dead: eleven private palettes, none responding to the toggle. theme/vizTokens.ts reads the tokens via getComputedStyle and re-reads on theme change. Verified at runtime that --series-1 resolves to #2a78d6 in light and #3987e5 in dark.

This is the same shape as the 2026-08-15 lesson about writing browser-bound Go I/O-free from the start: near-free at visualization #1, five renderers plus their tests at #5.

Every colour in the palette was measured, not chosen. Contrast against --page, both modes:

Role Light Dark
ink-primary 17.20:1 17.62:1
ink-secondary 7.26:1 8.52:1
ink-muted 4.63:1 6.04:1
accent 6.76:1 7.32:1
accent hover 8.79:1 9.00:1
label on accent button 7.19:1 7.32:1

First pass had light ink-faint at 2.97:1 against a 3:1 requirement. Caught by the script, not by eye — the difference is invisible and the failure is real. Hover is higher contrast than rest in both modes, which is the actual rule (interactive states get more prominent, not just different).

Build output. 79.2 kB gzip JS, 4.7 kB gzip CSS, 374ms build. Fonts are self-hosted Geist and Geist Mono via @fontsource-variable, subset by unicode-range, so a latin reader fetches ~53 kB of the 12 emitted files. Typecheck clean, eslint zero problems.

Tests are red, and that is the correct state. 23 failing in ring.test.ts, all bottoming out in createRng, hash and Ring.* throwing their TODO(human). Nothing was implemented on that side today. The homepage figure frame catches the throw and renders the file path instead of blanking, so the unimplemented state is visible rather than broken.

What I got wrong / missed

  • Sized the display type for a sans face while specifying a mono one. clamp(..., 3.5rem) gave a three-line headline against a two-line cap. Mono runs roughly 25% wider per character; the cap had to come down to 2.625rem. Reading the CSS would never have shown this. Screenshotting did, immediately.
  • Set aspect-ratio inline and then tried to override it from a stylesheet. Inline styles beat every selector regardless of specificity, so the :has(.figurePending) rule silently did nothing and the empty frame stayed 585px tall. Passing --figure-aspect as a custom property instead fixed it. Silent no-ops are the expensive kind of CSS bug — nothing errors, the rule just never applies.
  • Shipped a graph-paper grid on an empty frame and called it plot furniture. It is furniture when there is a plot. On an empty frame it is decoration, which the brief rules out. Now gated on the frame having real content.
  • Printed "PLANNED" on ten consecutive index rows. A column whose value never varies carries no information. Only the exceptions are badged now.
  • Used em-dashes in eleven rendered chapter labels with the zero-em-dash rule loaded at the time. Found by grepping, not by reading.
  • The general failure behind four of those five: I reviewed the code and not the page. Every one was caught in the first screenshot pass and none were caught by reading the source. Look at the rendered thing earlier.
  • No journal entry exists for the session that scaffolded lab/consistent-hashing/ and site/src/visualizations/consistentHashing/ — roughly a week ago, between the 16th and today. The design doc reviews survive in lab/SKILLS.md, but what was decided about run.ts, the seek model, and the test list is not written down anywhere.
  • lab/SKILLS.md was not updated, deliberately. It tracks the human's HLD and implementation skill, and today involved neither a design doc nor an implementation by hand. Adding an entry would have been padding.

Next

  • Resume #1: implement rng.ts, hash.ts, ring.ts by hand. 23 tests are waiting. Watch the wrap case in lookup — there is a test aimed at it.
  • Once those pass, the renderer gets written against Ring and readVizTokens(), with direct labels on every arc per the finding above.
  • Ch 2 is due tomorrow (Aug 24) and has not moved today.