Skip to content

CVD-safe default categorical palette#61

Merged
FarhanAliRaza merged 3 commits into
mainfrom
cvd-safe-default-palette
Jul 22, 2026
Merged

CVD-safe default categorical palette#61
FarhanAliRaza merged 3 commits into
mainfrom
cvd-safe-default-palette

Conversation

@FarhanAliRaza

Copy link
Copy Markdown
Contributor

Summary

Replaces the Tableau10 default categorical palette with an eight-slot CVD-safe palette (§20/§36 default theme).

Why: Tableau10's adjacent red/green pair collapsed to ΔE 1.2 under deuteranopia — effectively indistinguishable — and slots 1/5/7/9/10 sat below the chroma floor.

The new palette keeps every step in the OKLCH lightness band shared by light and dark surfaces (L 0.48–0.67) and validates against both reference surfaces (#fcfcfb / #1a1a19):

  • worst adjacent-pair CVD ΔE 8.5 (Machado–Oliveira–Fernandes protan/deutan, ≥8 target)
  • worst adjacent normal-vision ΔE 19.1 (≥15 floor)
  • chroma ≥ 0.10 and ≥3:1 contrast for every slot on both surfaces

The slot order is the safety mechanism — adjacency drives the ΔE gate — so the config comment now warns against re-ordering or extending without re-running the validator.

Also in this PR

  • Updated the hardcoded segment colors in the Reflex demo app and the visual-regression chrome shell to the new slots.
  • Regenerated examples/reflex/assets/charts/*.html, which also picks up the current embedded render client (colormap stops, colorbar chrome CSS) from recent main commits.

Checks

  • pre-commit run --all-files, ruff check, ruff format --check all pass.

@codspeed-hq

codspeed-hq Bot commented Jul 16, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 97 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing cvd-safe-default-palette (97e01b9) with main (dbe97ca)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

…lette

Tableau10's adjacent red/green collapsed to dE 1.2 under deuteranopia
and half its slots sat below the chroma floor. The new eight-slot
palette keeps every step in the OKLCH lightness band shared by light
and dark surfaces (L 0.48-0.67) and passes the validator on both
reference surfaces: worst adjacent-pair CVD dE 8.5 (>=8 target), worst
adjacent normal-vision dE 19.1 (>=15 floor), all slots >=3:1 contrast.
Slot order is the safety mechanism (adjacency drives the dE gate), so
the palette must be re-validated before any re-order or extension.

Updates the hardcoded segment colors in the Reflex demo app and the
visual-regression chrome shell.
@FarhanAliRaza
FarhanAliRaza force-pushed the cvd-safe-default-palette branch from b605467 to 5701f77 Compare July 17, 2026 17:38
@Alek99

Alek99 commented Jul 19, 2026

Copy link
Copy Markdown
Member

Reviewed — the palette itself is solid, but the committed demo assets don't match the PR description and actively mislabel colors.

Blocking: the regenerated examples/reflex/assets/charts/*.html files are missing from the commit

The PR body says these assets were regenerated, but the diff only touches 3 files and the committed assets still embed the old Tableau10 colors — custom_chrome.html's spec has "palette":["#4c78a8","#f58518","#54a24b"] and density_scatter.html's density overlay has "color":"#4c78a8". Meanwhile the Reflex demo app's custom legend WAS updated to the new hexes, so the swatch-to-mark mapping is now actively wrong: the legend shows a green #008300 swatch for "Growth" while Growth points render orange #f58518, and magenta #d55181 for "Self serve" while those points render green — worse than before the PR. The visual-regression chrome shell (scripts/visual_regression_smoke.py) wraps the same stale asset, so the mismatch also gets baked into regression screenshots, and the DOM probe only checks element visibility so CI won't catch it.

Fix: run examples/reflex/scripts/build_charts.py and commit the regenerated assets.

Non-blocking:

  • The new comment block in python/xy/config.py points at .claude/skills/xy-dataviz/scripts/validate_palette.py, which doesn't exist in the repo — the "re-run the validator before reordering" instruction is unactionable. Either commit the validator or drop the reference.
  • tests/test_scatter_matrix.py:107 comment still says the base palette has 10 entries; it's 8 now (assertions are length-agnostic and still pass).

The palette design itself checks out — I independently reproduced the CVD claims (Machado protan/deutan simulation + Lab ΔE76): the old palette does collapse (#54a24b#e45756 at deutan ΔE76 ≈ 1.7, and 5 slots below 3:1 contrast on light surfaces), while the new one holds worst-case adjacent CVD ΔE76 ≈ 27.6 with contrast 3.05–5.57 on both surface colors. And every palette consumer (Python payload/raster/SVG, JS renderer via the wire palette) indexes modulo length, so shrinking 10→8 is safe; 148 related tests pass.

@FarhanAliRaza

Copy link
Copy Markdown
Contributor Author

Assets were removed to make it easy to review the pr.

@Alek99 Alek99 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review findings:

  • [P2] python/xy/config.py:70: reducing the default palette to eight makes categories/traces 1 and 9 identical because color assignment cycles modulo palette length. Preserve at least ten distinct defaults or warn/guard above eight categories.
  • [P2] examples/reflex/reflex_xy_app/reflex_xy_app.py:41: the custom legend uses the new palette, but the committed assets/charts/custom_chrome.html still embeds the old palette. Regenerate and commit the asset.

@Alek99

Alek99 commented Jul 21, 2026

Copy link
Copy Markdown
Member

@greptile

@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown

Greptile Summary

This PR replaces the default categorical colors with a CVD-safe palette. The main changes are:

  • Adds an ordered eight-color palette for light and dark surfaces.
  • Centralizes default color cycling and warns when series colors wrap.
  • Warns when categorical values exceed the available distinct colors.
  • Updates palette cycling and warning tests.

Confidence Score: 5/5

This looks safe to merge.

No blocking issues found in the changed code.

T-Rex T-Rex Logs

What T-Rex did

  • I ran targeted runtime tests to verify categorical palette cycling and wrap-warning behavior, and confirmed the eight-color palette persisted through the chart payload and standalone HTML serialization.
  • I verified that the palette-defaults baseline emitted no wrap warnings and that the PR passed the targeted warning and cycling tests.
  • I validated that the palette-payload-export baseline preserved the prior Tableau colors and that the eight-color palette remained consistent in payload and HTML serialization.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
python/xy/config.py Defines the eight-color palette and the shared color-cycling helper.
python/xy/channels.py Warns when a categorical encoding exceeds the default palette.
python/xy/marks.py Uses the shared palette helper for default mark colors.
python/xy/_payload.py Uses the shared helper when payload generation supplies a missing trace color.
tests/test_scatter_matrix.py Covers category and series color wrapping for the new palette.

Reviews (2): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

…resh stale demo assets

Color assignment cycles modulo the palette, so with eight slots the ninth
default-colored trace or category silently wore slot one's color. The
palette stays eight slots by design (the adjacency order is the validated
CVD-safety mechanism and cannot grow a ninth hue without re-clearing the
validator), so the wrap now warns instead of aliasing silently (§28):
default_palette_color() guards every trace-default site, and categorical
resolve warns when categories exceed the default palette.

Also regenerates the two committed Reflex demo assets that still embedded
Tableau10 colors from before the palette swap (custom_chrome.html's
category palette and density_scatter.html's trace color).
# Conflicts:
#	examples/reflex/assets/charts/custom_chrome.html
#	examples/reflex/assets/charts/density_scatter.html
#	examples/reflex/reflex_xy_app/reflex_xy_app.py
#	python/xy/marks.py
#	scripts/visual_regression_smoke.py
@FarhanAliRaza
FarhanAliRaza merged commit abe4c41 into main Jul 22, 2026
29 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants