Skip to content

feat(palette): adopt variant D as the anyplot palette#7617

Merged
MarkusNeusinger merged 21 commits into
mainfrom
feature/adopt-palette-variant-d
May 23, 2026
Merged

feat(palette): adopt variant D as the anyplot palette#7617
MarkusNeusinger merged 21 commits into
mainfrom
feature/adopt-palette-variant-d

Conversation

@MarkusNeusinger
Copy link
Copy Markdown
Owner

Summary

Rename the categorical palette from Okabe-Ito to anyplot palette and swap positions 2–7 to variant D ("balanced") from #5817. Brand position 1 (#009E73) is preserved.

This is the test-phase adoption — daily-regen will start producing plots in the new palette so the user can review whether to keep it.

Categorical palette (positions 2–7 changed)

Pos Before After
1 #009E73 #009E73
2 #D55E00 #9418DB
3 #0072B2 #B71D27
4 #CC79A7 #16B8F3
5 #E69F00 #99B314
6 #56B4E9 #D359A7
7 #F0E442 #BA843E

Selection: Petroff-style max-min ΔE in CAM02-UCS, paper-ink corridor J' ∈ [45,72], C ∈ [22,36]. First-4 worst-CVD ΔE: 11.73 (Okabe-Ito) → 15.61 (variant D). See docs/reference/palette-variants/D-balanced.html for the derivation, CVD analysis, and per-condition swatch tables.

Continuous cmaps — only two allowed

  • anyplot_seq (single-polarity): #009E73 → #003D94
  • anyplot_div (diverging): #BB0D22 ↔ #A2A598 ↔ #007AD9

All other cmaps are forbidden: viridis, cividis, BrBG, Reds, Blues, Greens, jet, hsv, rainbow. Palette identity now carries through to continuous data.

Semantic exception

When categories carry strong real-world color cues (grass→green, wood→tan, blood→red), the AI may reassign positions to palette members that match. Colors must still come from the anyplot palette; mapping must be obvious from the labels. Default to canonical order for abstract categories.

What's NOT in this PR

  • React app UI (tokens.css, PaletteStrip.tsx, CodeShowcase.tsx) still uses Okabe-Ito — UI rebrand is a separate decision after the user reviews the new plots.
  • docs/reference/style-guide.md (marketing-site brand doc) — held back for the same reason.
  • Existing plots/*/implementations/* files will be overwritten by daily-regen.

Test plan

  • core.images imports clean, ANYPLOT_PALETTE has 7 hex values
  • tests/unit/prompts/test_prompts.py — 87 passed
  • tests/unit/core/test_images.py + tests/unit/agentic/regen/ — 116 passed
  • CI green
  • After merge: daily-regen produces a few new-palette samples for review

🤖 Generated with Claude Code

MarkusNeusinger and others added 17 commits May 20, 2026 23:19
Adds scripts/palette-analysis.py and docs/reference/palette-analysis.html as the
measurement anchor for the color-optimization work tracked in #5817.

The diagnostic reads the canonical palette from core.images, computes pairwise
ΔE in CAM02-UCS (Luo et al. 2006) under normal vision and three 100%-severity
CVD conditions (Machado et al. 2009 via colorspacious), and renders:

  - sample line charts on the real bg-page surfaces (light + dark)
  - swatch table for the 7 Okabe-Ito hues + 2 adaptive neutrals
  - "first-n" cumulative worst-pair table (the practical question:
    at what palette size does the weakest pair drop below comfort)
  - paired ΔE matrices: normal vision | worst of 3 cvd
  - homepage hero mockup pair with live WCAG contrast badges
  - surface & chrome token analysis for both themes
  - 4-step CAM02-UCS scale citing Petroff (2021): <5 confusable,
    5-10 marginal, 10-15 okay, ≥15 optimal (Petroff comfort target)

Baseline finding: Okabe-Ito is optimal under normal vision at every palette size
but green×blue drops to 11.7 under tritanopia from n=3 onwards. Five hue pairs
land in the 10-15 "okay" zone in worst-CVD. This is the gap variant work will
try to close in the next commit.

Run: uv run --script scripts/palette-analysis.py
Output: docs/reference/palette-analysis.html (self-contained, 92.7 kB)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generates six candidate replacement palettes inspired by Anselmoo's
dracula-palette generator (https://anselmoo.github.io/dracula-palette/).
All anchored at brand green #009E73 and selected by greedy max-min ΔE
in CAM02-UCS under normal vision + 3 CVD conditions (deuteranopia,
protanopia, tritanopia at 100% severity).

Variants:

  A — analogous            hues clustered within ±90° of brand
  B — triadic              three hue anchors 120° apart
  C — split-complementary  green + two flanking complements
  D — balanced             tightest chroma corridor, no hue rule
  E — okabe-tuned          within ±22° of each Okabe-Ito hue
  F — okabe-shifted        hue-preserving projection onto the corridor

E and F preserve Okabe-Ito's canonical position order
(green/vermillion/blue/purple/orange/sky/yellow). A-D reorder positions
2-4 by max-min worst-CVD ΔE so the first 4 are the "most beautiful subset"
(since 95% of plots use 2-4 series).

Each variant gets its own self-contained HTML in
docs/reference/palette-variants/ with the same sample-chart + first-N +
ΔE matrix pair + hero mockup + continuous colormap pipeline as the
baseline diagnostic, so direct comparison is easy. An index.html links
all six with the first-4 worst-CVD ΔE prominently displayed.

Paper-ink lever: the chroma corridor (CAM02-UCS C ∈ [22, 50]) plus a
strict gamut filter (tol 0.001). Caligo sits at C ≈ 60-90, Okabe-Ito at
C ≈ 40-75 — capping C is what keeps the picks away from neon. Per-variant
chroma sub-ranges differentiate D (most muted, C ∈ [22, 36]) from E
(okabe-honest, C ∈ [30, 50]).

First-4 worst-CVD min ΔE achieved (target ≥ 15 per Petroff 2021):
  A: 16.34   B: 20.21   C: 22.73
  D: 22.59   E: 15.99   F: 10.07*
  baseline (Okabe-Ito): 11.73

* F dips below baseline because the minimal-shift projection mutes
  several Okabe hues without picking a different position; it is the
  most conservative option, not the optimal one.

Refactor: extracted ~1000 LOC of rendering helpers from
scripts/palette-analysis.py into scripts/_palette_common.py so both
scripts share the same pipeline. palette-analysis.py is now ~150 LOC
of layout assembly + main.

Run: uv run --script scripts/palette-variants.py
Output: docs/reference/palette-variants/{index, A..F-*.html}

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a full-width baseline card at the top of the variants grid so the
current Okabe-Ito palette is the first thing seen — the bar every variant
tries to clear. The card carries the same hex-strip + first-4 worst-CVD
ΔE + all-pairs normal ΔE layout as the variant cards, so direct
comparison stays one glance away. Dashed border + "current" pill mark
it as the reference rather than another candidate. Clicking opens the
full baseline diagnostic at docs/reference/palette-analysis.html.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Variants B (triadic) and C (split-complementary) were cycling through
3 hue anchors, which meant positions 1/4, 2/5, 3/6 all landed in the
same hue band — three purples in a row was the most obvious symptom.

Replace the anchor cycle with 7 distinct per-position hue targets so
each pick lives in its own hue region:

  triadic    : brand · +120° · +240° · +60° · +180° · +300° · +30°
               (3 primaries + 3 midpoints + 1 warm fill)
  split-comp : brand · +150° · +210° · +90° · +270° · +60° · +300°
               (brand + 2 split anchors + 4 gap-fillers)
  analogous  : brand · ±30° · ±60° · ±90°
               (spread across the ±90° band rather than clustering)

For the balanced strategy (no hue band by design) add a hue-diversity
penalty at score time: subtract 0.3 × max(0, 50° − min_hue_dist_to_any_selected)
from the ΔE score so successive picks stay ≥50° apart in hue. Without
this, greedy max-min lands on three nearly-identical purples again.

Also drop variant F (okabe-shifted). The minimal hue-preserving
projection was the most conservative option but scored 10.07 worst-CVD
— below the Okabe-Ito baseline of 11.73, since muting some hues
without picking a different position made the worst pair slightly
tighter rather than wider. Variant E already covers the "respect
Okabe-Ito" case while staying above the baseline.

Achieved first-4 worst-CVD min ΔE after restructure:
  A: 15.03   B: 19.77   C: 17.99   D: 22.08   E: 15.99
  (baseline Okabe-Ito = 11.73)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…5817)

After re-reading Caligo's actual sources (OKLCH harmony modes + intent layers,
syntax C ≈ 0.04-0.14 — not neon as previously assumed), three concrete
upgrades to the variant generator:

1. **Hard min-hue-gap mask** in `select_palette`: every variant now enforces a
   minimum pairwise hue spacing on the colour wheel (60% of the strategy's
   diversity target). Per-position bands and the diversity penalty stay as
   soft guides, but the gap mask is never relaxed — even when the band
   fallback drops the hue rule, no two picks can land within the gap of each
   other. This fixes the previous run's 4 sub-30° clashes (triadic two-azures,
   split-comp two-blues, okabe-tuned two-blues + two-greens).

2. **New variant F — harmonic**: same max-min ΔE selection as balanced but
   with the paper-ink chroma corridor widened to C ∈ [22, 60]. Tests whether
   more chroma headroom yields more pleasing hue choices without crossing
   into Caligo-style territory. Continuous colormap green → blue → magenta.

3. **E renamed okabe-tuned → okabe-spread + harmony upgrade**: instead of
   just nudging Okabe-Ito's native hues into the paper-ink corridor, snap
   them onto an even 7-slot lattice anchored at brand green (~51° apart),
   then reorder for max first-4 worst-CVD ΔE. Removes the canonical-order
   preservation that locked the previous E into Okabe's blue/sky/purple
   cluster.

Achieved scores (first-4 worst-CVD min ΔE, baseline Okabe-Ito 11.73):

  A analogous:        15.03  (+3.30)
  B triadic:          19.30  (+7.57)
  C split-comp:       15.53  (+3.81)
  D balanced:         21.89  (+10.16)
  E okabe-spread:     21.89  (+10.16)
  F harmonic:         19.77  (+8.04)

All 6 variants now have minimum pairwise hue gap ≥ 30° except A-analogous
(29.7°), which is the geometric limit for 7 picks inside a ±90° wedge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…5817)

- Remove variant okabe-spread: with reorder_first_4 enabled it converged on
  the same hex set as balanced (both 21.89 first-4 worst-CVD ΔE), so the
  redundancy was not earning its slot. harmonic now occupies E.
- Sample charts in both palette-variants.html and palette-analysis.html show
  the first 4 colours (was 3) — matching the "first-4 most beautiful" subset
  semantics of the variant reorder and the practical n=4 case for line charts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#5817)

After picking D-balanced as the front-runner, two aesthetic concerns
remained: pos 2 #9B4A00 read as brown (not yellow), and pos 3 #99B314 was
a second green-family colour next to brand green. Two targeted fixes:

1. CandidatePool.build now drops "muddy" warm picks — sub-band specific:
     • red-orange   (H ∈ [30, 65))  requires J' ≥ 52
     • yellow-lime  (H ∈ [65, 100]) requires J' ≥ 62
   Caligo's OKLCH syntax defaults set Yellow at L 0.72 (≈ J' 70) for the
   same reason: yellows below that read as olive. The split-threshold
   spares deep reds (real reds at J' 45) while still killing #9B4A00.

2. reorder_first_4 now enforces a ≥60° pairwise hue gap among the four
   chosen positions (degrading 5° at a time if the 7-hue pool can't
   satisfy it). Stops green×lime and purple×magenta from co-occupying
   the top-4.

Achieved first-4 worst-CVD min ΔE (baseline Okabe-Ito 11.73):

  A analogous       15.03  (top-4 hue gap 51° — analogous wedge bound)
  B triadic         15.61  (was 19.30, traded for hue uniqueness)
  C split-comp      15.53
  D balanced        15.61  (was 21.89, top-4 now green/purple/red/azure)
  E harmonic        15.61  (was 19.77)

All variants stay above Petroff's 15 ΔE comfort threshold.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After tightening triadic/split-comp bands to 12°, B-triadic and D-balanced
still converged on the same purple/red picks because both bands fell on
the CVD-optimal hue. Three changes:

1. Tighten triadic/split-comp PRIMARY bands to 4° (filler positions stay
   at 12°). At H_STEP=5° this snaps to ±1 grid hue — triadic locks at
   ~290°/45° (true purple + amber-red) instead of drifting to 305°/25°
   where balanced lives.
2. reorder_first_4 now accepts a `pinned` parameter. Triadic and
   split-comp pin positions 1-2 (the strategy primaries) so the reorder
   only searches for the best 4th slot. Without pinning the reorder
   would silently move the strategy anchors out of the top-4.
3. New variant F-okabe-anchored: pos 0 = brand-green and pos 1 =
   vermillion (#D55E00) both pinned. Both are already paper-ink-compliant
   (J=58.8/59.3, C=25.1/33.5) so no chroma reduction needed. select_palette
   gained an `extra_seeds` parameter for this. Continuous colormap
   green → near-neutral → vermillion (diverging).

Final top-4 hue layout per variant (pos 1, pos 2, pos 3):
  A analogous       lime 115°   blue 245°    amber 55°
  B triadic         purple 290° amber 45°    azure 230°
  C split-comp      magenta 315° red 20°     lime 85°
  D balanced        purple 305° red 25°      azure 230°
  E harmonic        purple 305° red 30°      azure 230°  (higher C)
  F okabe-anchored  vermillion 51° purple 300° azure 230°

Scores (first-4 worst-CVD min ΔE, baseline Okabe-Ito 11.73):
  A 15.03   B 15.61   C 17.84   D 15.61   E 15.61   F 15.61

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- B-triadic: one_liner now says "purple · amber-red" (was "magenta · azure")
  matching the tightened triadic anchors at H=290°/45°.
- B continuous_label "green ↔ purple diverging" (was "green ↔ magenta").
- C-split-comp: clearer one_liner naming the 150°/210° flanks; cmap
  label "green ↔ red diverging" (was "vermillion").
- F-okabe-anchored: drop the pin on pos 1. Vermillion stays in top-4
  because it satisfies the ≥60° hue-gap to brand-green and gives strong
  CVD distance, but reorder_first_4 may now push it to pos 5-7 if a
  future change favours a different 4-set.
- HTML strategy summary shows the per-variant C corridor instead of the
  global [22, 50]. Per-variant ranges: A [24,40], B [26,42], C [26,42],
  D [22,36], E [22,60], F [22,42]. The ranges visibly differ between
  variants and explain why E feels more vivid than D.

No palette hex values changed (run-output identical).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In B-triadic and C-split-comp, position 6 was hardcoded to brand+30° as
the "extra" filler beyond the 3-primaries-plus-3-midpoints layout. For
brand-green at H=166° this landed at H=196° (cyan), which:
- Sat only 29° from brand-green (visually same teal-cyan family)
- Clashed with the brand+60°=226° azure at pos 3 (only 35° apart)
- Had worst-CVD ΔE 3.48 against azure → "confusable" under tritanopia

Drop the hardcoded target; position 6 now uses None (no per-position
band), so the greedy max-min ΔE pass — already running under the ≥27°
hue-gap mask — fills the largest remaining hue gap. New picks:

  B-triadic pos 6: #C18A10  (warm yellow, H=80°, between amber 45° and
                              lime 115° — the previously empty 70° gap)
                   worst-CVD ΔE 7.54 (vs 3.48 before)
  C-split-comp pos 6: #22AFC8 (cyan-azure, H=215° — fills the cool gap)
                      worst-CVD ΔE 8.46 (was 5.40)

No changes to top-4 of either variant; only the trailing filler.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Variant pages and the baseline diagnostic now render each continuous
colormap applied to MATLAB's peaks bivariate function — a 240×144 PNG
inlined as a base64 data URI. The 1D gradient strips hide perceptual
issues (banding, lightness inversions, hue swerves) that jump out on
real 2D data. Implementation in render_cmap_demo (uses Pillow which was
already in the dep stack).

File-size impact per variant: +21 kB (peaks rendered once per cmap,
PNG-optimised). palette-analysis.html grew 93→162 kB (three cmaps).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#5817)

Four enhancements addressing the "what else can we look at" question:

1. **Continuous cmaps now derived from the palette.** Previously each
   variant had one hardcoded cmap (A: green→teal hardcoded, B/C: ended
   at palette[1]/palette[2], D/E: both endpoints hardcoded). Now every
   variant has TWO cmaps, both palette-derived:
   - **sequential**: brand-green → dark version of the palette member
     whose hue sits closest to 240° (the blue zone). J' 22 / C 35 at
     the terminus gives a monotonic lightness ramp regardless of where
     the source palette member sits in J/C.
   - **diverging**: warmest palette pick (hue closest to 30°) ↔ near-
     neutral mid ↔ coolest palette pick (closest to 240°). Both poles
     normalised to J' 45 / C 38 for symmetric weight.
   Labels are auto-generated from the matched palette members
   (e.g. "amber-red ↔ azure diverging").

2. **Sample charts: lines + bars + scatter.** render_sample_charts
   now emits 3 chart types × 2 themes = 6 SVGs per variant, using the
   first-4 palette colours. Bars test categorical reading, scatter
   tests the palette in dense small marks.

3. **Cmap peaks demo on both surfaces.** render_cmap_demo wraps the
   PNG in a light-bg AND dark-bg frame side by side so the user sees
   the cmap against both production surfaces without toggling.

4. **compare.html** — one-page side-by-side of all 6 variants. Each
   card shows: header + score · all 9 swatches · sequential cmap
   strip + mini peaks · diverging cmap strip + mini peaks · link to
   the full variant page. Links in nav from index and variant pages.

File-size impact: variant pages ~150 kB each (was 76 kB), compare.html
~250 kB, baseline diagnostic ~250 kB (was 163 kB). Higher byte cost
buys decision-readiness in one view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
palette-analysis.html had no link back to the variants — easy to land
on, hard to escape. Added a variant-nav with links to the variants
grid and the side-by-side compare page.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Every page (baseline diagnostic, grid index, compare, each variant) now
shows the same top-level nav with: grid · compare · ★ baseline · A · B
· C · D · E · F. Current page highlighted via .current class. Lets the
user hop between baseline and any variant from any page without
walking back through the grid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The .variant-nav button styling was only inlined in render_variant_page,
so the baseline diagnostic and the grid/compare pages rendered plain
text links instead of the button row. Moved the rules into PAGE_CSS so
every page that uses the shared stylesheet gets the same nav strip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rename the categorical palette from Okabe-Ito to "anyplot palette" and
swap positions 2–7 to variant D ("balanced") from the palette exploration
in #5817. Position 1 (#009E73) stays — brand identity is preserved.

## Categorical palette (positions 2–7 changed)

| Pos | Before          | After           |
|-----|-----------------|-----------------|
| 1   | #009E73 (green) | #009E73 (green) |
| 2   | #D55E00         | #9418DB         |
| 3   | #0072B2         | #B71D27         |
| 4   | #CC79A7         | #16B8F3         |
| 5   | #E69F00         | #99B314         |
| 6   | #56B4E9         | #D359A7         |
| 7   | #F0E442         | #BA843E         |

Selection: Petroff-style max-min ΔE under the paper-ink corridor (J' ∈
[45,72], C ∈ [22,36] in CAM02-UCS). First-4 worst-CVD ΔE improves from
11.73 (Okabe-Ito) to 15.61 (variant D).

## Continuous colormaps — only two are now allowed

- anyplot_seq (single-polarity): #009E73 → #003D94
- anyplot_div (diverging):       #BB0D22 ↔ #A2A598 ↔ #007AD9

All other cmaps are forbidden: viridis, cividis, BrBG, Reds, Blues,
Greens, jet, hsv, rainbow. Palette identity now carries through to
continuous data instead of breaking at the categorical/continuous edge.

## Semantic exception

The default rule "use positions 1→N in order" gets a deliberate softener:
when category labels carry strong real-world color cues (grass→green,
wood→tan, blood→red), the AI may reassign positions to palette members
that match. Colors must still come from the anyplot palette — no custom
hexes — and the semantic mapping must be obvious from the data labels.

## Scope

- core/images.py: OK_* → ANYPLOT_*, hex values updated, LIBRARY_COLORS
  remapped by position. Dead legacy aliases (ANYPLOT_BLUE/_YELLOW) dropped.
- 18 prompt files updated: style guide, plot generator, all 10 library
  prompts, all QA + workflow prompts, plus agentic/commands/regen.md.
- scripts/style-variants.yaml palette_tableau patches updated to find
  new hexes.
- scripts/palette-{analysis,variants}.py: Okabe-Ito hexes inlined so the
  baseline-comparison research scripts keep working post-rename.
- Tests + docstrings: cosmetic Okabe-Ito → anyplot palette renames.
- Merges feature/color-optimization (#5817) for the palette analysis
  scripts and diagnostic HTML pages under docs/reference/palette-variants/.

## Intentionally NOT updated in this PR

- app/src/styles/tokens.css + app/src/components/{PaletteStrip,CodeShowcase}.tsx:
  React app UI still uses Okabe-Ito for code highlighting and palette-strip
  marketing. Test phase only changes generated plots; UI rebrand is a
  separate decision after the user reviews the new plots.
- docs/reference/style-guide.md: marketing-site brand document, also held
  back until UI rebrand decision.
- Existing plots/*/implementations/*: generated artifacts that will be
  rewritten by daily-regen with the new palette.

## Test plan

- [x] core.images imports clean, ANYPLOT_PALETTE has 7 hex values
- [x] tests/unit/prompts/test_prompts.py — 87 passed
- [x] tests/unit/core/test_images.py + tests/unit/agentic/regen/ — 116 passed
- [ ] CI green
- [ ] daily-regen picks up new palette on next run

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Copilot AI review requested due to automatic review settings May 23, 2026 07:33
Auto-format pass after the variant D palette adoption — ruff caught
column / spacing drift around the new constant block. No behavior change.
User confirmed: "bad quality red, good quality green" is a valid case
for reassigning anyplot palette positions away from canonical order.
Generalise the exception in default-style-guide.md from real-world
objects only (grass/wood/blood/sky) to also cover conventional status
codes (bad/error/loss→red, good/ok/profit→green, neutral/warning→tan
or pink).

Constraints unchanged — match must come from the anyplot palette (no
custom hexes), and the semantic mapping must be obvious from the data
labels (legend literally says Pass/Fail, Profit/Loss, OK/Error, etc.).
Abstract categories (groups A/B/C, anonymous bins) still default to
canonical order.
User pushback: the prior wording read as a definitive enum of allowed
cases (real-world objects + status), but there are many more — stock
chart up/down bars, P&L deltas, sentiment polarity, traffic lights,
temperature hot/cold, etc. Reframe so:

- The examples are explicitly illustrative ("some examples (not
  exhaustive)"), not a closed list.
- The test is "would a typical reader expect this category to look
  like this color?", not membership in any enum.
- Constraints stay tight: must be from the anyplot palette, must be
  obvious from labels, abstract categories still default to canonical.

Stock-chart bullish/bearish bars are now called out explicitly so
finance charts don't get penalised for breaking canonical order.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adopts palette variant D (“balanced”) as the new canonical anyplot categorical palette (preserving brand green #009E73) and updates the prompt/style-guide ecosystem so future implementation generation, repair, and review enforce the new categorical order and the new “only two allowed” continuous colormaps (anyplot_seq, anyplot_div).

Changes:

  • Switch core.images categorical constants and library accent mapping to the anyplot palette (variant D).
  • Update default style guide + per-library prompts + workflow prompts to reference the anyplot palette and new continuous colormap restrictions.
  • Add palette analysis/variant-generation scripts and reference HTML pages to document/compare palette variants.

Reviewed changes

Copilot reviewed 28 out of 36 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
tests/unit/prompts/test_prompts.py Updates prompt-color test wording to “anyplot” branding while keeping the #009E73 check.
tests/unit/agentic/regen/test_metadata.py Updates fixture text to reference “anyplot palette” instead of “Okabe-Ito”.
scripts/style-variants.yaml Updates the “Tableau sanity-check” patch mapping to target the new palette hexes.
scripts/palette-variants.py Adds a generator for candidate palettes + continuous cmaps and renders HTML comparison pages.
scripts/palette-analysis.py Adds a baseline diagnostic HTML generator (currently hardcoded to Okabe-Ito as baseline).
scripts/_palette_common.py Adds shared color-math + HTML-rendering utilities used by the palette scripts.
prompts/workflow-prompts/impl-similarity-claude.md Updates “mandated palette” language to anyplot palette.
prompts/workflow-prompts/impl-repair-claude.md Updates palette/cmap compliance guidance to anyplot palette + anyplot cmaps only.
prompts/workflow-prompts/impl-generate-claude.md Updates generation requirements for categorical order + continuous cmap rules.
prompts/workflow-prompts/ai-quality-review.md Updates review instructions for palette/cmap compliance (VQ-07) to new rules.
prompts/quality-evaluator.md Updates evaluation examples and VQ-07 phrasing to anyplot palette/cmaps.
prompts/quality-criteria.md Updates VQ-07 definition/scoring to anyplot palette + anyplot continuous cmaps.
prompts/plot-generator.md Updates base generator guidance/comments to anyplot palette terminology.
prompts/library/seaborn.md Updates categorical palette snippet and replaces continuous cmap guidance with anyplot cmaps only.
prompts/library/pygal.md Updates palette constants and revises continuous guidance to anyplot-stop interpolation.
prompts/library/plotnine.md Updates categorical palette usage and provides anyplot-stop gradient examples for continuous.
prompts/library/plotly.md Updates discrete palette and provides anyplot continuous scale stop lists.
prompts/library/matplotlib.md Updates palette constants and provides anyplot LinearSegmentedColormap examples.
prompts/library/makie.md Updates categorical palette constants and restricts continuous colormaps to anyplot stops only.
prompts/library/letsplot.md Updates categorical/continuous scale guidance to anyplot palettes/stops.
prompts/library/highcharts.md Updates series colors and restricts color_axis gradients to anyplot stops only.
prompts/library/ggplot2.md Updates palette constants and replaces viridis/brewer guidance with anyplot-stop gradients.
prompts/library/bokeh.md Updates palette and provides anyplot-stop interpolation examples for continuous mappers.
prompts/library/altair.md Updates categorical scale range and provides anyplot-stop continuous scale examples.
prompts/default-style-guide.md Renames Okabe-Ito → anyplot palette, updates canonical order, adds semantic exception + anyplot cmaps-only rule.
docs/reference/palette-variants/index.html Adds/updates the rendered “grid” page for variant comparison.
core/images.py Replaces Okabe-Ito constants with anyplot palette constants and updates library accent mapping.
agentic/commands/regen.md Updates regen guidance to reference anyplot palette as the mandated data colors.

Comment thread scripts/palette-variants.py Outdated
Comment on lines +13 to +17
Generates 5 candidate replacement palettes inspired by Anselmoo's
``dracula-palette`` generator (https://anselmoo.github.io/dracula-palette/),
all anchored at the brand green ``#009E73`` and selected by max-min ΔE in
CAM02-UCS under normal vision + 3 CVD conditions (deuteranomaly,
protanomaly, tritanomaly, each at 100% severity).
Comment on lines +21 to +26
A — analogous (harmony over distinctness; hues clustered)
B — triadic (three hue anchors 120° apart)
C — split-complementary (green plus two flanking complements)
D — balanced (Petroff-style max-min, paper-ink chroma)
F — harmonic (balanced rule but relaxed C corridor)

Comment thread scripts/palette-variants.py Outdated
Comment on lines +39 to +40
Output: ``docs/reference/palette-variants/{A..E}-<name>.html`` plus an
``index.html`` linking the five.
Comment thread scripts/palette-variants.py Outdated
Comment on lines +1044 to +1063
# Baseline card — current Okabe-Ito palette as the reference everyone
# compares against. Linked to the full diagnostic for drill-down.
baseline_first4 = measure_first_4(OKABE_PALETTE)
baseline_normal = measure_all_normal_min(OKABE_PALETTE)
baseline_chip_top = "".join(
f'<span class="big" style="background:{hx}" title="{hx}"></span>'
for hx in OKABE_PALETTE[:4]
)
baseline_chip_tail = "".join(
f'<span class="small" style="background:{hx}" title="{hx}"></span>'
for hx in OKABE_PALETTE[4:]
)
baseline_score_class = cell_class(baseline_first4)
baseline_card = f"""
<a class="variant-card baseline-card" href="../palette-analysis.html">
<div class="card-head">
<span class="key">★</span>
<h3>baseline — okabe-ito <em>(current)</em></h3>
</div>
<p class="one-liner">today's plot palette. every variant below tries to clear this bar — the bar is the green×blue tritanopia collapse at ΔE 11.73.</p>
Comment on lines +70 to +72
NEUTRAL_LIGHT = "#1A1A17"
NEUTRAL_DARK = "#F0EFE8"

Comment on lines +623 to +629
<section class="intro">
<p>five candidate palettes inspired by Anselmoo's <code>dracula-palette</code> generator —
all anchored at brand green <code>#009E73</code>, generated by greedy max-min ΔE
selection in CAM02-UCS under normal vision + 3 CVD conditions (deuteranopia, protanopia,
tritanopia at 100% severity). all colours sit inside a paper-ink corridor
(J' ∈ [45, 72], C ∈ [22, 50]) — that's the lever that
keeps the palette away from caligo-style neon while staying perceptually uniform.</p>
Comment on lines +640 to +642
<h3>baseline — okabe-ito <em>(current)</em></h3>
</div>
<p class="one-liner">today's plot palette. every variant below tries to clear this bar — the bar is the green×blue tritanopia collapse at ΔE 11.73.</p>
<span class="key">F</span>
<h3>okabe-anchored</h3>
</div>
<p class="one-liner">Okabe-Ito's vermillion (#D55E00) seeded into the 7-hue pool alongside brand-green — both already paper-ink-compliant. reorder_first_4 then chooses top-4 freely; vermillion stays in if it earns the spot..</p>
Comment thread scripts/palette-analysis.py Outdated
Comment on lines +13 to +19
Generates a single self-contained HTML page that visualises the current anyplot
palette across three domains, each evaluated for normal vision and three
100%-severity CVD simulations (deuteranopia, protanopia, tritanopia):

A. Categorical plot palette (Okabe-Ito 7 + 2 adaptive neutrals)
B. Continuous plot colormaps (viridis, cividis, BrBG)
C. Website surface & chrome tokens — including homepage hero mockup
8 of 9 Copilot suggestions on #7617 applied — all concern docstring /
diagnostic-page text that drifted out of sync as variant F was added
or as variant D was promoted from candidate to active palette:

- scripts/palette-variants.py: docstring said "5 variants" / "F=harmonic"
  / "{A..E}"; code defines 6 (A-F) with E=harmonic, F=okabe-anchored.
  Fixed count, alphabetical mapping, and output path.
- scripts/palette-variants.py: index-page baseline card said okabe-ito
  is "(current)" / "today's plot palette" — now "(legacy)" with a
  pointer that variant D has been adopted as the active palette.
- scripts/palette-analysis.py: docstring claimed the script visualises
  the "current anyplot palette", but the implementation is hardcoded
  to Okabe-Ito as the baseline reference. Reworded to say so plainly
  and point to D-balanced.html for the active palette.
- docs/reference/palette-variants/index.html: same three updates as
  above ("five candidate palettes" → "six", baseline card "(current)"
  → "(legacy)", spotted "spot.." double-period typo).

Skipped (1 of 9): scripts/_palette_common.py NEUTRAL_LIGHT/NEUTRAL_DARK
disagree with the style-guide "adaptive neutral" position-8 hex. The
analysis scripts use the theme ink (#1A1A17 / #F0EFE8) but the style
guide specifies a slightly different adaptive neutral (#1A1A1A /
#E8E8E0). This is a pre-existing inconsistency in the research branch,
affects only the position-8 swatch in the diagnostic HTML (categorical
positions 1–7 are unaffected), and aligning would require regenerating
all the variant HTML pages. Defer to a follow-up if variant D is
confirmed adopted.
@MarkusNeusinger MarkusNeusinger merged commit 9ab41a6 into main May 23, 2026
9 checks passed
@MarkusNeusinger MarkusNeusinger deleted the feature/adopt-palette-variant-d branch May 23, 2026 07:43
@codecov
Copy link
Copy Markdown

codecov Bot commented May 23, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

MarkusNeusinger added a commit that referenced this pull request May 26, 2026
Hard switch from variant-D (PR #7617) to imprint — the v3 hybrid-v3
muted-8 palette designed in feat/palette-variants-v1. Full rationale:
docs/reference/palette-variants-v3/decision-rationale.md.

Changes
- NEW core/palette.py: single source of truth for the 8 categorical
  hues, 3 semantic anchors (amber + 2 theme-adaptive neutrals), named
  API (palette.green, palette.semantic.bad, ...), sequential cmap
  (imprint_seq: green→blue), and diverging cmap (imprint_div_light /
  imprint_div_dark: red↔neutral↔blue with theme-adaptive midpoint).
- core/images.py: drop the 7 variant-D hex constants and ANYPLOT_PALETTE
  list; re-import from core.palette. ANYPLOT_GREEN keeps its hex
  (#009E73); ANYPLOT_RED changes from #B71D27 to #AE3030; PURPLE/SKY/
  PINK/TAN are gone (replaced by LAVENDER/BLUE/CYAN/OCHRE — exposed
  with ANYPLOT_* prefix for symmetry).
- LIBRARY_COLORS: rebuilt with library-brand-match strategy (matplotlib
  keeps red, plotly stays blue, bokeh stays purple, seaborn picks rose
  for its warm statistical mood, highcharts picks cyan to stay distinct
  from plotly's blue).
- matplotlib cmap registration runs at core.images import time.

Not touched (intentional, for follow-up PRs)
- 7 plot implementations in plots/**/python/*.py hardcode the old
  variant-D hex list locally — they keep working but show the old
  palette until regenerated via /regen.
- Frontend app/src/pages/PalettePage.tsx and PaletteStrip.tsx still
  show Okabe-Ito — separate frontend PR.
- Prompt system (prompts/default-style-guide.md, prompts/library/*.md,
  prompts/plot-generator.md) still references the old palette — Phase 2
  of the rollout.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
MarkusNeusinger added a commit that referenced this pull request May 26, 2026
…frontend rebuild (#7692)

## Summary

Replaces the previous **variant D** palette (PR #7617) with **imprint**
— anyplot's new 8-hue colourblind-safe categorical palette + 3 semantic
anchors (amber for warning, theme-adaptive neutral and muted). Tuned for
warm-paper rendering, validated against deuteranopia / protanopia /
tritanopia, sits in the academic-publishing family next to Okabe-Ito,
Paul Tol "muted", and ColorBrewer Set2.

Full design rationale:
[`docs/reference/palette-variants-v3/decision-rationale.md`](https://github.com/MarkusNeusinger/anyplot/blob/feat/palette-variants-v1/docs/reference/palette-variants-v3/decision-rationale.md).
Earlier variants (v1, v2, expert reviews) preserved under
`docs/reference/palette-variants-*/` for design archaeology.

## What's in this PR

**Phase 1 — Backend foundation (`core/`)**
- NEW `core/palette.py` — single source of truth: 8 categorical hexes in
hybrid-v3 sort order, 3 semantic anchors, named API (`palette.green`,
`palette.semantic.bad`, …), sequential cmap `imprint_seq` (green→blue) +
theme-adaptive diverging cmap `imprint_div_{light,dark}`
(red↔midpoint↔blue), matplotlib registration.
- `core/images.py` refactored — drops old 7 ANYPLOT_* constants,
re-imports from `core/palette.py`. `LIBRARY_COLORS` rebuilt with
library-brand-match strategy.

**Phase 2 — Prompt system (18 files)**
- `prompts/default-style-guide.md` — full Color Philosophy rewrite with
8-hex hybrid-v3 table, new Semantic anchors section, series-count
guidance with concrete ΔE_CVD numbers, optional outline pattern.
- 11 library prompts (matplotlib / seaborn / plotly / bokeh / altair /
plotnine / pygal / highcharts / ggplot2 / letsplot / makie) —
`ANYPLOT_PALETTE` lists updated to 8 hexes + `ANYPLOT_AMBER`, cmaps
renamed with new endpoints.
- Quality + workflow prompts — VQ-07 scoring extended to positions 1-8 +
3 anchors, legacy variant-D hexes listed as auto-reject signals.

**Phase 3 — Style guide doc**
- `docs/reference/style-guide.md` Section 4.1 rewritten as "The imprint
Palette" with hue table, semantic-anchors table, hybrid-v3 sort
rationale. Status colours (§4.5) and plot-only colours (§4.6) remapped
onto imprint. CSS variables + Python implementation reference updated.

**Phase 5a — Frontend palette page (`/palette`)**
- `app/src/pages/PalettePage.tsx` completely rebuilt across 5
iterations: real chroma wheel (CSS conic-gradient OKLCH + chroma fade)
with hover tooltips, compare-with toggle (Okabe-Ito / Tol muted /
ColorBrewer Set2 / anyplot previous), compact 4×2 slot grid with
**click-to-copy hex**, sort toggle (imprint default ↔ CVD-optimal
max-min) with inline trade-off info, semantic anchors cards, continuous
cmaps gradient strips, copy snippets in 4 languages (Python / R / Julia
/ JS) with OKLCH toggle, collapsible WCAG audit + palette history.
- `theme/index.ts` — `colors.okabe` → `colors.imprint`, semantic colours
remapped.
- `styles/tokens.css` — `--ok-*` → `--imprint-*`, code-syntax theme
rebuilt for both light and dark.
- `components/PaletteStrip.tsx` — 8-hex imprint set, optional `hexes`
prop.

**Phase 5b — Frontend consumers**
- `LandingPage.tsx` — `CLUSTER_PALETTE` + 7-Okabe swatch row swapped
(now click-to-copy), `palette_okabe_ito` analytics event gone.
- `MapPage.tsx` — `CLUSTER_COLORS` rebuilt with 8 imprint hexes.
- `AboutPage.tsx`, `ScienceNote.tsx` — palette prose reframed around
imprint.
- `CodeShowcase.tsx`, `CodeHighlighter.tsx` — syntax hexes updated,
`okabeItoTheme` → `imprintTheme`.
- `LandingPage.test.tsx` — drops the gone Okabe-Ito tracking-event test.

**Final audit sweep**
- `docs/reference/plausible.md` (gone event row),
`.serena/memories/style_guide.md` (Color section),
`scripts/style-variants.yaml` (palette_tableau map),
`scripts/palette-analysis.py` +
`docs/reference/anyplot-landing-mockup.html` (HISTORICAL markers).
Historical / comparison references (Okabe-Ito as family-neighbour,
variant-D hexes as auto-reject signals in VQ-07, palette-variants
exploration scripts) preserved intact.

## Intentionally NOT in this PR (Phase 4)

7 plot implementations under `plots/**/implementations/python/*.py`
still hardcode the old variant-D palette inline. Per the design
discussion these stay untouched and will pick up imprint via the next
`/regen` cycle, when the new prompts kick in.

## Validation

- `yarn tsc --noEmit` — clean
- `yarn lint` — 0 errors (2 pre-existing warnings, neither
palette-related)
- `yarn test --run` — **507 / 507 passing**
- Backend smoke-test confirms named-API + cmap registration
- Visual smoke tests via chrome-devtools on `/`, `/palette`, `/map`,
`/about`

## Test plan

- [ ] Verify `/palette` page renders the chroma wheel, palette grid,
semantic anchors, cmap previews, collapsibles
- [ ] Try the compare-with toggle (Okabe-Ito / Tol muted / Set2 /
anyplot previous) — overlay rings appear on the wheel
- [ ] Try the sort toggle (imprint default ↔ CVD-optimal) — grid + strip
+ copy snippet all reorder
- [ ] Click a swatch in the grid — copies hex to clipboard, swatch flips
to "copied ✓"
- [ ] Switch OKLCH toggle in the copy section — all 4 language snippets
switch notation
- [ ] Expand the WCAG audit and palette history collapsibles
- [ ] Verify LandingPage palette section + map cluster preview render
with imprint colours
- [ ] Sanity-check dark mode on `/` and `/palette`
- [ ] Watch Cloud Build: OG image regeneration should pick up the new
LIBRARY_COLORS

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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