M6-10: Design tokens for charts: check for color vision deficiency¶
Status: ready | Depends on: M4-03 | Milestone: M6
Goal¶
After this ticket the token gate (make tokens) checks the chart series color.viz.series.1 to color.viz.series.8 not only for contrast ui against the surfaces (which it does since M0) but also for distinguishability under simulated color vision deficiency: for each theme, the series are transformed with a published simulation model for protanopia, deuteranopia and tritanopia, and every pair of adjacent series (in the fixed order the views use, 25 10 item 2, components.md 11) must keep at least the minimum distance that design/design-rules.json names as cvd_min_delta_e_ok; otherwise the gate fails and names the theme, the deficiency, the pair and the distance. The series are adjusted where they fail, the contrast pairings of design-rules.json stay green in both themes and with every accent of M4-03, docs/design/contrast.md lists the new check, and the previews docs/design/previews/viz-*.png are regenerated. The traffic light uses only the existing state roles (color.success.*, color.warning.*, color.danger.*), which the check holds too. Why: charts must not depend on red-green or blue-yellow distinctions that a part of the audience cannot see (25 10 item 2, 01 8 "Color"); "contrast AA is a test, not a hope" (R-PR-08) applies to the visualizations in the same way.
Read first¶
docs/spec/25-visualization.md: 1 rule 5 (values generated fromdesign/tokens/), 7.4 (states with icon and text), 10 "Shared design language" (items 1 and 2: the rolescolor.viz.*, "distinguishable despite red-green confusion", the traffic light on the state roles, every pairing indesign/design-rules.json), 13 (row "after the early M4 tickets").docs/spec/01-product.md: 8 "Accessibility" (rows "Contrast" and "Color").docs/design/README.md,docs/design/components.md11 "Visualizations" (series order, axis, area fill, grid),docs/design/contrast.md(generated; how rows are listed),docs/design/palette.md,docs/design/tokens.md.design/design-rules.json(the pairing "Charts: series and axes against the surface", the palette guards),design/tokens/color.light.tokens.jsonandcolor.dark.tokens.json(groupviz),design/tokens/color.palette.tokens.json.tools/design-tokens/README.md,tools/design-tokens/tokens.py(build,check,previews --view viz),tools/design-tokens/tokenkit/rules.py(contrast rows,RuleError),tokenkit/color.py(OKLab and conversions),tokenkit/emit_docs.py(contrast.md), and the tests undertools/design-tokens/tests/.- ADR 0030 (design tokens), ADR 0069 (visualization on two levels).
docs/DECISIONS.md: D-W-54, D-W-100 and its extension to M5 and M6 (D-W-120); section 10 (format ofD-M6-10-<n>rows).- Brief (merged names win, D-W-100):
docs/briefs/M4-03-accent-variants.md(accent derivation, howtokens.py checkiterates themes and accents,DsAccent). - Machado, Oliveira and Fernandes, "A Physiologically-based Model for Simulation of Color Vision Deficiency", IEEE Transactions on Visualization and Computer Graphics 15(6), 2009 (the simulation matrices for severity 1.0), or another published model with matrices you can cite; ViƩnot, Brettel and Mollon 1999 is acceptable for protanopia and deuteranopia but has no tritanopia matrix of the same kind.
This ticket touches no API operation, no table, no action, no event and no server configuration key.
Fixed requirements that apply¶
Owner's fixed build rules (apply verbatim):
- No MVP. New from scratch and directly in the final state; stages are only the order.
- Strictly modular: clear module and service boundaries, dependencies only through defined interfaces, no cross access.
- Documentation in code: every public module, class, function and interface with a docstring/doc comment (purpose, parameters, return value, errors, example where useful); comments explain the why, not the what. API documentation is generated from code (OpenAPI from the routes, dartdoc, Python API docs in MkDocs).
- Documentation in the repo: README per module/service, ADR for every architecture decision, CHANGELOG per release, keep the documentation site current. A change without matching documentation is not done.
- Quality gates (SonarQube, linters, type checking, security scanners, test coverage) block the merge.
Rules for every brief:
- Test first: write the failing test, run it and keep the red output for the report, then make it green, then clean up.
- No en dash and no em dash in any file you write; the
text-rulespre-commit hook rejects them. Use a hyphen or rephrase. - White label: examples use
example.comand the people Alex, Sam and Robin; no company, host or personal names, no paths under a home directory. - The minimum distance is a named value in
design/design-rules.jsonwith its source and reason next to it; code and docs name the value, never the number. - Specification first: if the check changes a rule of 25 10 or components.md 11, update them in the same change. The core contract is not edited by this ticket.
Product requirements implemented here:
- Every chart series reaches contrast
uiagainstcolor.surface.canvas,color.surface.listandcolor.surface.raisedin light and dark (the existing pairing stays and stays green). - Adjacent series (1 and 2, 2 and 3, up to 7 and 8, and 8 and 1 because the order wraps when a view needs more than eight groups; components.md 11) stay distinguishable under simulated protanopia, deuteranopia and tritanopia at full severity, measured as the OKLab distance (
delta_e_ok, the measure the palette guards already use) of the simulated colors, at leastcvd_min_delta_e_ok. - The traffic light uses only
color.success.*,color.warning.*andcolor.danger.*; the check holds that green, yellow and red stay pairwise distinguishable under the three simulations by the same minimum, and states always carry an icon and text as well (25 7.4). - The check is part of
make tokensand therefore ofmake check; a token change that breaks it fails the gate.
Scope¶
Create:
tools/design-tokens/tokenkit/cvd.py: the simulation (linear RGB matrices of the chosen model at severity 1.0, applied after sRGB linearization and clipped before re-encoding), and the distance check over a list of role pairs (Design notes 1 and 2).tools/design-tokens/tests/test_cvd.pyasunittest.TestCaseclasses, becausemake tokensrunspython -m unittest discoverover that folder (tools/design-tokens/Makefile) (Tests).
Change:
tools/design-tokens/tokenkit/rules.py: a new rule kindcvd_pairsread fromdesign-rules.json; results as rows like the contrast rows (theme, deficiency, pair, distance, required, passed);RuleErrorfor an unknown role or deficiency.tools/design-tokens/tokens.py(checkandbuild): run the CVD rows with the contrast rows and fail on any failed row; print one line per failure with theme, deficiency, both roles and the distance.tools/design-tokens/tokenkit/emit_docs.py: a section "Color vision deficiency" indocs/design/contrast.mdwith the model, its source, the minimum and one row per checked pair and theme.design/design-rules.json:cvd_min_delta_e_okwithsourceandreason, andcvd_pairs: the adjacent series pairs, the wrap pair, and the traffic light pairs (Design note 3).design/tokens/color.light.tokens.jsonandcolor.dark.tokens.json(andcolor.palette.tokens.jsonif a new palette step is needed): adjust the series that fail (Design note 4).- Regenerated outputs of
make tokensunderdesign/build/anddocs/design/contrast.md;docs/design/previews/viz-light.pngandviz-dark.pngregenerated withtokens.py previews --view viz. docs/design/components.md11 anddocs/spec/25-visualization.md10 item 2: name the check and the value (by name).tools/design-tokens/README.md; rootCHANGELOG.mdunder[Unreleased];docs/DECISIONS.mdsection 10 rowsD-M6-10-1toD-M6-10-4.
Out of scope¶
- The package
bothy_vizand its views (M6-11, M6-12); the TypeScript token output for the Grafana plugin (M7). - Contrast pairs outside the charts, unless a series change moves a shared palette step that another pairing uses (then that pairing must stay green and the report names it).
- Changes to the core contract.
Boundaries¶
- Work in a dedicated git worktree on the branch
feat/M6-10-viz-cvd-check; one writer per worktree. - Conventional Commits with the component as scope (for example
feat(design-tokens): ...,docs(design): ...) and a DCO sign-off on every commit (git commit -s); squash into meaningful commits before the merge (docs/quality/push-discipline.md). - Never push to
main, never tag and never create remote repositories; the only remote steps are the branch push and the pull request ofAGENTS.md. - Do not modify files outside the scope above, except the CHANGELOG, README and
docs/DECISIONS.mdentries named in this brief. - Follow the host rules supplied by the launcher (ports, machine rules).
- Tests make no network call; the previews use Playwright through
npxastokens.py previewsdoes; temporary directories only viamktemp -dortempfile.TemporaryDirectory. - Never read or print secrets. No network calls except package downloads for pinned tools.
- Never lower a threshold, disable a gate, delete a test or loosen a palette guard; never lower a contrast threshold to make a series pass.
- If the core contract lacks something this ticket needs, stop with
Verdict: BLOCKED <reason>instead of building a deviation (P-11). - This brief was written before milestone M1 closed (D-W-100, extended to M5 and M6 by D-W-120). Where the merged code of an earlier ticket names a package, type, function, file or make target differently from this brief, follow the code and list each difference in the report; where it behaves differently from the specification, stop with
Verdict: BLOCKED <reason>.
Design notes¶
-
Model (Ruling for docs/DECISIONS.md). Use the matrices of Machado, Oliveira and Fernandes 2009 at severity 1.0 for protanopia, deuteranopia and tritanopia, applied in linear sRGB (decode sRGB, multiply, clip to
[0, 1], encode). Put the nine numbers of each matrix incvd.pywith a comment that cites the paper and the table they come from, and test them against reference values (Tests). Reason: one model for all three deficiencies, in linear RGB, widely used by browser developer tools; the gate needs a reproducible transformation, not a perceptual study. -
Distance (Ruling for docs/DECISIONS.md). OKLab Euclidean distance of the two simulated colors, the measure
tokenkit/color.pyand the palette guards already use (delta_e_ok). Choosecvd_min_delta_e_okwith a source or a stated derivation (for example from the just noticeable difference in OKLab that the palette guards use, scaled for small chart marks) and write the reason intodesign-rules.json; the report states the chosen value and the measured minimum per theme before and after the adjustment. -
Pairs.
cvd_pairslists role pairs and the deficiencies: seriesnwithn+1fornfrom 1 to 7, series 8 with series 1, andcolor.success.fgwithcolor.warning.fg,color.warning.fgwithcolor.danger.fg,color.success.fgwithcolor.danger.fg. Pairs are checked per theme (light,dark); the accents of M4-03 do not touchcolor.viz.*and the state roles, so the check runs once per theme; if the accent derivation of M4-03 maps an accent onto a state role, check that combination as well and say so. -
Adjusting the series. Change the fewest series possible, keep the hue order of components.md 11 recognisable (for example by moving lightness or chroma before moving hue), keep every palette guard green (no Discord blurple, no forbidden warm hues where a guard forbids them) and keep contrast
uiagainst the three surfaces. Record every changed value with before and after in the report and inD-M6-10-3. If the traffic light pair fails under tritanopia, do not change the state roles silently: change them only if the contrast pairings of the banners and badges stay green, otherwise stop withVerdict: BLOCKEDnaming the pair (the state roles carry many components). -
Outputs.
contrast.mdandtokens.resolved.jsongain the CVD rows; the Dart output ofdesign_systemchanges only through the changed values;make tokensregenerates everything in a temporary folder and compares as for every output. -
Rulings for
docs/DECISIONS.md(section 10):D-M6-10-1the simulation model (note 1);D-M6-10-2the distance andcvd_min_delta_e_ok(note 2);D-M6-10-3the changed series values (note 4);D-M6-10-4the pairs including the wrap pair and the traffic light (note 3).
Tests (write them first)¶
tools/design-tokens/tests/test_cvd.py (unittest.TestCase, run by make tokens):
test_matrices_match_reference: each matrix applied to the primaries and to white reproduces the reference values of the paper (white stays white within rounding); a cited sample color gives the simulated value of the source within1e-3.test_simulation_clips_and_roundtrips: inputs at the gamut edge stay within[0, 1]; gray stays gray.test_adjacent_pairs_checked_with_wrap: a fake rules file with eight series yields eight pairs per deficiency and theme.test_failing_pair_fails_gate: two nearly identical series under deuteranopia produce a failed row and a non-zero exit oftokens.py checkwith the expected message.test_unknown_role_or_deficiency_is_rule_error.test_real_tokens_pass: the realdesign/passes the CVD check and the contrast check in both themes.test_contrast_md_lists_cvd_rows: the generatedcontrast.mdcontains the section and one row per pair, theme and deficiency.
Extend test_gate.py (M0-01): a changed series value without regeneration fails make tokens as before; a series that breaks the CVD rule fails it with the CVD message.
Commands that must pass, from the repository root:
make tokens(runs the unit tests oftools/design-tokens/testswithunittest)make tokens,make docs,make linkcheckmake check
Acceptance criteria¶
-
make tokenschecks protanopia, deuteranopia and tritanopia for every adjacent series pair, the wrap pair and the traffic light pairs in both themes, and fails belowcvd_min_delta_e_ok. - Every series reaches contrast
uiagainst the three surfaces in both themes; every palette guard stays green. - The simulation is unit tested against reference values of the cited model.
-
contrast.md, the previews, components.md 11 and 25 10 are current; the rulings are recorded. -
make checkis green.
Definition of done¶
- Code and tests as specified; docstrings on every public Python symbol (ruff
D); comments explain why. tools/design-tokens/README.mdupdated in English (the CVD check, the model, the value and how to read a failure).- Rulings of Design note 6 recorded in
docs/DECISIONS.mdsection 10. - Root
CHANGELOG.mdentry under 'Unreleased'. - An ADR only if you take an architecture decision beyond this brief (MADR 4 as in
docs/adr/0000-template-madr-4.md, headings and text in English, next free number,make adr-index). make checkgreen.- An independent code review over the diff, then the red team gate (
docs/quality/red-team-gate.md, verdictJAorNEIN); onNEINfix with a test per finding and have the delta checked again. Record both verdicts. - Land the change as
AGENTS.md("How work lands onmain") describes: a pull request from this branch, squash-merged only when review, red team andmake check,ci-okand the review threads are green; thengit worktree remove.
Report¶
Write to {{REPORT_PATH}}:
- First line exactly
Verdict: DONEorVerdict: BLOCKED <reason>. - Commits (SHA and subject), changed files with line counts, test results with counts (the red run before the change and the green run after), the summary of
make check. - Review and red team verdicts and the findings fixed.
- The model and its source, the chosen
cvd_min_delta_e_okwith reason, the minimum measured distance per theme and deficiency before and after, every changed token value. - What is not proven (a simulation is not a test with people).
- Follow-ups that could not be done in this scope, each with its reason, every difference to the names of this brief (D-W-100), and spec gaps found.
Then write {{DONE_PATH}} with exit=0 sha=<sha> or exit=1 <reason>.