Skip to content

feat(palette): adopt imprint — new categorical palette + named API + frontend rebuild#7692

Merged
MarkusNeusinger merged 24 commits into
mainfrom
feat/palette-variants-v1
May 26, 2026
Merged

feat(palette): adopt imprint — new categorical palette + named API + frontend rebuild#7692
MarkusNeusinger merged 24 commits into
mainfrom
feat/palette-variants-v1

Conversation

@MarkusNeusinger
Copy link
Copy Markdown
Owner

@MarkusNeusinger MarkusNeusinger commented May 23, 2026

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. 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.tscolors.okabecolors.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.tsxCLUSTER_PALETTE + 7-Okabe swatch row swapped (now click-to-copy), palette_okabe_ito analytics event gone.
  • MapPage.tsxCLUSTER_COLORS rebuilt with 8 imprint hexes.
  • AboutPage.tsx, ScienceNote.tsx — palette prose reframed around imprint.
  • CodeShowcase.tsx, CodeHighlighter.tsx — syntax hexes updated, okabeItoThemeimprintTheme.
  • 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 --run507 / 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

MarkusNeusinger and others added 2 commits May 24, 2026 00:20
The style-guide had two contradicting values for grid/rule opacity: the
Theme-adaptive Chrome token table at 0.10 vs the dedicated Grid Guidelines
section "opacity 15-25%, very subtle". Per-library prompts (matplotlib,
plotly, bokeh, makie, highcharts, altair, plotnine, seaborn rcParams) all
inherited the 0.10 token; seaborn's hardcoded ax.yaxis.grid call already
used 0.2 (following the verbal rule).

Harmonize the spec at 0.15 — the lower edge of the Grid Guidelines band —
so the token table and the verbal rule agree, and propagate to every
library prompt that referenced the 0.10 token. Generated plots stay subtle
but become clearly readable against the warm paper-ink surfaces.

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

Second round of palette exploration (#5817 follow-up). v0 (variants A-F)
compared against Okabe-Ito; v1 (this) measures everything against live D,
which has shipped as ANYPLOT_PALETTE since the v0 round.

New deliverable at docs/reference/palette-variants-v1/:

  - D-baseline.html   — live anyplot palette rendered with the same
                        template as candidates, so it sits in the lineup
                        as the bar to beat
  - D1 tight-chroma   — C ∈ [24, 32], position-1 pinned to red band
                        [15°, 35°] so the corridor still yields a true
                        crimson (#AE3030) rather than a rust pick
  - D3 expand-8       — live D's 7 hues + 1 freely-picked 8th hue
                        (indigo #7981FD), diametrically opposite tan;
                        fills both remaining wheel gaps without forcing
                        a swap
  - T  tetradic       — 4 hue anchors 90° apart starting at brand green,
                        3 mid-quadrant fillers
  - W  warm-pole      — D's max-min selection plus a warm-pole scoring
                        bonus centred at 55°, with #B71D27 pinned for
                        semantic-red availability
  - index.html        — hero color wheel with candidate-overlay toggles +
                        baseline-card layout flip + Δ-vs-D coloring
  - compare.html      — side-by-side card grid with D as the reference row

New generator scripts/palette-variants-v1.py extends the v0 algorithm with:
  - forbidden_hue_bands / warm_bonus knobs in select_palette
  - tetradic + d-* strategy branches in _strategy_bands
  - n_hues parameter on Variant (D3 uses 8)
  - render_color_wheel — pre-rendered CAM02-UCS PNG disk (perceptually
    honest chroma fade, no slice seams) with palette dots placed at their
    actual (C, H) coords, optional chroma-corridor toggle, overlay live-D
    comparison

scripts/_palette_common.py: bump v1 sample-chart gridlines from 0.06 to
0.15 (catch-up to the new style-guide token), bump PAGE_CSS --rule from
0.10 to 0.15 for the same consistency. v0 (palette-variants/) untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 23, 2026 22:21
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

This PR (1) harmonizes the “grid/rule” opacity design token to 0.15 across generator prompts/style guide, and (2) adds a new v1 palette-variant diagnostic generator under scripts/ intended to produce docs/reference/palette-variants-v1/* HTML pages comparing candidates against the live variant D palette in core/images.py.

Changes:

  • Update grid/rule opacity token from 0.100.15 across the default style guide, plot generator prompt, and multiple per-library prompt snippets.
  • Add scripts/palette-variants-v1.py, a new palette-variants v1 generator (baseline = live D) including a CAM02-UCS color wheel visualization.
  • Align the palette diagnostics shared CSS/sample-chart grid rendering in scripts/_palette_common.py to the new 0.15 rule/grid token.

Reviewed changes

Copilot reviewed 12 out of 19 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
scripts/palette-variants-v1.py New v1 palette diagnostic generator (baseline vs live D) with variant pages, index, compare page, and color wheel rendering.
scripts/_palette_common.py Updates diagnostic sample-chart gridline opacity and CSS --rule token to 0.15 for consistency.
prompts/default-style-guide.md Updates the canonical grid/rule token to 0.15 and keeps reference snippet consistent.
prompts/plot-generator.md Updates matplotlib-style grid alpha token to 0.15 in the generator guidance snippet.
prompts/library/seaborn.md Updates seaborn rcParams grid.alpha guidance token to 0.15.
prompts/library/plotnine.md Updates plotnine panel_grid_major alpha to 0.15.
prompts/library/plotly.md Updates Plotly GRID rgba token to 0.15.
prompts/library/matplotlib.md Updates matplotlib ax.grid alpha token to 0.15.
prompts/library/makie.md Updates Makie grid RGBA alpha to 0.15 in examples.
prompts/library/highcharts.md Updates Highcharts GRID rgba token to 0.15.
prompts/library/bokeh.md Updates Bokeh grid line alpha guidance to 0.15.
prompts/library/altair.md Updates Altair gridOpacity guidance token to 0.15.

Comment on lines +19 to +27
Five new candidates explore "refine vs. rethink":

D1 — d-tight-chroma (D's max-min but C ∈ [24, 32] — narrower paper-ink)
D2 — d-wide-spread (D's max-min with 60° pairwise hue spread target)
D3 — d-swap-tan (D's max-min but hue band [50°, 90°] banned at pos 6
— forces an alternative to the live tan #BA843E)
T — tetradic (4 anchors 90° apart, brand-green anchored, 3 fillers)
W — warm-pole (D's max-min plus a warm-hue scoring bonus 30°–80°)

Comment on lines +358 to +362
"""Pick 7 hues for a variant. Greedy max-min ΔE selection under all 4
CVD conditions, with per-position hue bands and the per-variant chroma
corridor as candidate masks. If no candidate matches the strictest band,
the band half-width is widened in 10° steps until something fits.

Comment on lines +367 to +373
v1 additions:
- ``forbidden_hue_bands``: a list of (center_deg, half_width_deg) bands
to EXCLUDE from every position globally. Used by D3 (d-swap-tan) to
ban the tan band [50°, 90°] so a different 7th hue gets picked.
- ``warm_bonus``: (center_deg, half_width_deg, weight) — a soft additive
score bonus for candidates whose hue is within (half-width) of the
center. Used by W (warm-pole) to bias picks toward 30°–80°.
Comment on lines +1138 to +1142
Variant(
"D1", "tight-chroma", "d-tight-chroma",
"d-tight-chroma",
"live D's max-min ΔE selection but with the paper-ink chroma corridor narrowed to C ∈ [24, 32] — predicts cleaner co-existence in dense charts at the cost of some headroom. live D's semantic red #B71D27 is pinned at position 1 so loss/error/bad can map to the expected colour rather than a tight-corridor brown",
),
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Callable
@codecov
Copy link
Copy Markdown

codecov Bot commented May 23, 2026

Codecov Report

❌ Patch coverage is 22.68519% with 167 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
app/src/pages/PalettePage.tsx 0.00% 150 Missing ⚠️
core/palette.py 78.37% 8 Missing ⚠️
app/src/pages/LandingPage.tsx 57.14% 6 Missing ⚠️
app/src/components/PaletteStrip.tsx 0.00% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

MarkusNeusinger and others added 3 commits May 24, 2026 11:25
D1-8 mirrors D3's expand-8 approach for the tight-chroma corridor: D1's
7 picks leave a 75° purple→red back-gap, the 8th slot is greedy-picked
there for a matte rosé (#954477) that bridges purple and red while
staying inside C ∈ [24, 32].

Also introduces ``reorder_pure_cvd_greedy`` (opt-in via
USE_PURE_CVD_REORDER) for D1-8 — the original wheel-gap-first heuristic
in ``reorder_first_4`` was picking a 60°-valid but CVD-weak first-4
like {green, blue, tan, mauve} whenever the 8th hue opened new
gap-valid quadruples. Pure-CVD greedy keeps the worst-pair curve high:
n=4 lifts from 10.70 → 17.44 (now beats D3's 15.61 at the same n).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ortings

palette-variants-v2 narrows the field from v1's 5 candidates to the two
real contenders (vivid-8 = D3, muted-8 = D1-8) and adds tooling to
compare slot orderings side-by-side:

  - hero with both color wheels (chroma corridor always visible) and
    hue-sorted strips for direct hue-by-hue comparison between palettes
  - sticky TOC linking the 4 sortings: pure-CVD greedy, wheel-gap-first,
    hue-order (rainbow), every-other-hue (interleaved)
  - per-sorting scorecard with per-n winner strips for CVD + normal
    vision (green = vivid, blue = muted, grey = tie)
  - per-cell chart stack: light theme block + dark theme block, each
    with lines, bars (all 8), pie (first 4), stocks (first 4),
    edge-cluster overlap scatter (centre mix)
  - column-major pair grid so strips/tables align between palettes
    even when intros wrap to different line counts
  - per-n worst-pair ΔE table with normal-vision row + CVD-min row

Reuses v1 utilities via importlib (hyphen in filename).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 24, 2026 22:26
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

Copilot reviewed 13 out of 22 changed files in this pull request and generated 4 comments.

Comment on lines +82 to +90
_V1_SPEC = importlib.util.spec_from_file_location(
"palette_variants_v1", REPO_ROOT / "scripts" / "palette-variants-v1.py"
)
assert _V1_SPEC is not None and _V1_SPEC.loader is not None
v1 = importlib.util.module_from_spec(_V1_SPEC)
# Register before exec_module so @dataclass can resolve cls.__module__ during
# class construction (otherwise dataclasses.py raises NoneType.__dict__).
sys.modules["palette_variants_v1"] = v1
_V1_SPEC.loader.exec_module(v1)
Comment on lines +46 to +48
1. pure-CVD greedy max-min (slowest possible per-n ΔE degradation;
pos 0 fixed brand-green; pos 1 fixed at
muted-8's semantic red for stability)
Comment on lines +11 to +13
"""Palette variants v2 — head-to-head: vivid-8 (D3) vs muted-8 (D1-8).

v1 produced five candidate variants (D-baseline, D1, D1-8, D3, T, W). Two
Comment on lines +2077 to +2082
WARM_BONUS: dict[str, tuple[float, float, float]] = {
# W — additive bonus centred at 55° (warm orange-red), half-width 30°,
# weight 3.0 ΔE units at the centre. Strong enough to nudge selection
# toward warms without overriding the no-clash gap mask.
"warm-pole": (55.0, 30.0, 3.0),
}
MarkusNeusinger and others added 2 commits May 25, 2026 00:50
- Documented unanimous expert verdict favoring muted-8 over vivid-8
- Highlighted recurring themes and critiques from five independent reviews
- Recommended next steps for palette optimization and semantic picking
Final muted-8 slot order via hybrid-v3 algorithm (coarse 6-band hue
partition with deferred red anchor). Includes side-by-side viewer
(hybrid-v3 vs pure-cvd-greedy) and a full transparency document
covering CVD prevalence, per-n ΔE measurements, trade-offs, and
comparison to Okabe-Ito / Tol / ColorBrewer / Tableau / D3 / Petroff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 26, 2026 18:21
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

Copilot reviewed 16 out of 26 changed files in this pull request and generated 8 comments.

Comment on lines +46 to +48
1. pure-CVD greedy max-min (slowest possible per-n ΔE degradation;
pos 0 fixed brand-green; pos 1 fixed at
muted-8's semantic red for stability)
Comment on lines +43 to +77
from typing import Callable, Sequence

import numpy as np


REPO_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO_ROOT / "scripts"))


def _load(module_name: str, filename: str):
spec = importlib.util.spec_from_file_location(
module_name, REPO_ROOT / "scripts" / filename
)
assert spec is not None and spec.loader is not None
mod = importlib.util.module_from_spec(spec)
sys.modules[module_name] = mod
spec.loader.exec_module(mod)
return mod


v1 = _load("palette_variants_v1", "palette-variants-v1.py")
v2 = _load("palette_variants_v2", "palette-variants-v2.py")

from _palette_common import ( # noqa: E402
DARK_THEME_FULL,
LIGHT_THEME_FULL,
PAGE_CSS,
PAGE_JS,
hex_to_rgb1,
pairwise_delta_e,
render_sample_bars,
render_sample_chart,
to_jab,
worst_cvd_pairwise_delta_e,
)
Comment on lines +184 to +188
# Full sorting set for the comparison table (the new hybrid + v2's 4).
ALL_SORTINGS: list[tuple[str, str, Callable[[list[str]], list[str]]]] = [
("hybrid-v3", "hybrid-v3 (family-diverse first 4, red deferred, CVD-greedy tail)", sort_hybrid_v3),
("pure-cvd-greedy", "pure-CVD greedy max-min", v2.sort_pure_cvd_greedy),
]
Comment on lines +491 to +493
"all five sortings use the identical 8 hexes. only the slot order "
"differs. ★ marks the recommended sort. the per-n table shows the "
"weakest pair&apos;s ΔE under the min of the 3 CVD simulations "
Comment on lines +20 to +22
v3 introduces a hybrid sort that fixes both: the first ``first_n`` slots are
constrained to come from distinct hue bins (45° wide by default) AND picked
by greedy max-min worst-CVD ΔE; the tail is unconstrained pure-CVD greedy.
Comment on lines +75 to +78
This corroborates the project memory note
[`palette_must_anchor_semantic_red`](file:///home/meake/.claude/projects/-home-meake-projects-anyplot/memory/feedback_palette_must_anchor_semantic_red.md)
— the rule that candidate palettes must allow a true red, via explicit
seeding if needed.
Comment on lines +245 to +248
This is documented in the project memory
[`feedback_palette_semantic_exception`](file:///home/meake/.claude/projects/-home-meake-projects-anyplot/memory/feedback_palette_semantic_exception.md)
— the rule that conventions like "bad → red, good → green,
warning → amber" are first-class concerns, not categorical-distinctness
Comment on lines +11 to +12
"""Palette variants v2 — head-to-head: vivid-8 (D3) vs muted-8 (D1-8).

MarkusNeusinger and others added 2 commits May 26, 2026 20:39
…er + 2 adaptive neutrals)

The 8 categorical hues don't cover three semantic roles cleanly:
caution/warning (ochre reads as earth/brown, not amber), totals/baseline
(needs structural neutral, not a category), and other/rest in stacked
charts (needs soft neutral). Add three named anchors that live OUTSIDE
palette[:n]:

- palette.amber = #DDCC77 (Paul Tol "muted" yellow) — min ΔE_CVD = 14.52
  to all 8 categorical hexes. The two saturated alternatives #D4A017 and
  #D4AF37 both collapse to ΔE_CVD ≈ 2.3 against lime under deuteranopia.
- palette.neutral — theme-adaptive ink (#1A1A17 light / #F0EFE8 dark).
  Already exists as NEUTRAL_LIGHT/NEUTRAL_DARK in _palette_common.py;
  now exposed via the named API.
- palette.muted — theme-adaptive ink-muted (#6B6A63 light / #A8A79F dark).
  For "other"/disabled series and confidence bands.

Updates:
- New "Semantic anchors" section on the v3 page (between hero and finalist)
  with split-swatch visualisation for the two adaptive neutrals
- decision-rationale.md: amber distance table, theme-adaptive neutral
  explanation, corrected named-API surface, semantic.warning now maps to
  amber (not ochre)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Document the muted-aesthetic / WCAG-3:1 tension honestly. Lighter members
of muted-8 (lavender, lime, cyan, ochre) plus the amber semantic anchor
fall below 3:1 against the cream bg #F5F3EC — the same limitation
Okabe-Ito (#F0E442), Tol "muted" (#DDCC77, #88CCEE), and ColorBrewer
Set2 share, since muted palettes earn distinguishability through chroma
rather than L-spread.

Recommended industry-standard fix: a thin ink-color stroke on affected
series. Stroke contrast (#1A1A17 on #F5F3EC = 15.71:1) always passes
regardless of fill colour.

amber stays a fixed hex (it lives outside the categorical pool, only
reached intentionally via palette.amber for caution/warning) — the same
outline rule applies wherever it's used.

Visual demo in index.html#contrast: every categorical hue + amber as a
48px dot on both theme bgs, with the sub-3:1 members shown both as-is
and with a 2px ink ring (light stage rescues 5, dark stage rescues 1).
rationale.md gains a "Contrast caveats & the outline pattern" section
with full per-theme ratio table.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 26, 2026 18:51
1px matches the typical stroke width used in real plot rendering (line /
scatter edges) and is still clearly visible on the 48px audit dots. Keeps
the demo aligned with the guidance it's illustrating.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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

Copilot reviewed 16 out of 26 changed files in this pull request and generated 5 comments.

Comment on lines +231 to +232
"""Worst-CVD ΔE of the weakest pair inside the first-n subset, for n=2..N.
Takes the min across normal + 3 CVD simulations."""
Comment on lines +215 to +219
# Full sorting set for the comparison table (the new hybrid + v2's 4).
ALL_SORTINGS: list[tuple[str, str, Callable[[list[str]], list[str]]]] = [
("hybrid-v3", "hybrid-v3 (family-diverse first 4, red deferred, CVD-greedy tail)", sort_hybrid_v3),
("pure-cvd-greedy", "pure-CVD greedy max-min", v2.sort_pure_cvd_greedy),
]
Comment on lines +75 to +78
This corroborates the project memory note
[`palette_must_anchor_semantic_red`](file:///home/meake/.claude/projects/-home-meake-projects-anyplot/memory/feedback_palette_must_anchor_semantic_red.md)
— the rule that candidate palettes must allow a true red, via explicit
seeding if needed.
Comment on lines +245 to +249
This is documented in the project memory
[`feedback_palette_semantic_exception`](file:///home/meake/.claude/projects/-home-meake-projects-anyplot/memory/feedback_palette_semantic_exception.md)
— the rule that conventions like "bad → red, good → green,
warning → amber" are first-class concerns, not categorical-distinctness
edge cases.
Comment on lines +11 to +15
"""palette variants v3 — muted-8 finalist with hybrid sort.

After 5 independent expert reviewers unanimously picked muted-8 over vivid-8
(see ``../palette-variants-v2/expert-reviews.md``), only the slot order was
still open. The v2 head-to-head exposed a real trade-off: pure-CVD-greedy
MarkusNeusinger and others added 2 commits May 26, 2026 21:13
The rationale was current on technical decisions (slot order, semantic
anchors, contrast audit) but missing three points that came up in later
discussion:

- "Why n=8?" — the fundamental pool-size choice (live anyplot has 7+1
  with documented cyan-or-lavender gap; 8 is the CVD discrimination
  sweet spot; matches Okabe-Ito/ColorBrewer Set2/Petroff 2021 lineage;
  360°/45° = 8 clean hue-wheel bins)
- "Red anchor — considered alternatives" — full 5-candidate analysis
  (#AE3030 / #BE2B2B / #C8322C / #B71D27 / #A41E22) with CAM02-UCS +
  WCAG + CVD data. Documents that #BE2B2B would marginally improve
  dark-bg WCAG but the practical impact is small; #AE3030 stays as the
  shipping choice with the outline pattern handling the gap
- Next steps gain entries 6 (per-theme hex sets) and 7 (OKLCH + P3
  notation) so future maintainers see what was considered and what's
  parked

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Copilot AI review requested due to automatic review settings May 26, 2026 20:20
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

Copilot reviewed 18 out of 28 changed files in this pull request and generated 6 comments.

Comment thread core/images.py Outdated
Comment on lines +53 to +55
from .palette import (
IMPRINT as ANYPLOT_PALETTE,
GREEN as ANYPLOT_GREEN,
Comment thread core/images.py Outdated
Comment on lines +62 to +64
LIME as ANYPLOT_LIME,
AMBER as ANYPLOT_AMBER,
palette,
Comment thread core/images.py Outdated
Comment on lines +64 to +65
palette,
register_with_matplotlib as _register_imprint_cmaps,
Comment thread core/palette.py
Comment on lines +33 to +36
from types import SimpleNamespace
from typing import Callable

from matplotlib.colors import LinearSegmentedColormap
Comment on lines +11 to +15
"""palette variants v3 — muted-8 finalist with hybrid sort.

After 5 independent expert reviewers unanimously picked muted-8 over vivid-8
(see ``../palette-variants-v2/expert-reviews.md``), only the slot order was
still open. The v2 head-to-head exposed a real trade-off: pure-CVD-greedy
Comment thread core/palette.py Outdated
Comment on lines +155 to +161
theme : "light" | "dark", default "light"
On light bg, midpoint = warm cream #F5F3EC; on dark bg, midpoint
= warm near-black #1A1A17.
"""
midpoint = "#F5F3EC" if theme == "light" else "#1A1A17"
return LinearSegmentedColormap.from_list(
f"imprint_div_{theme}", [RED, midpoint, BLUE]
MarkusNeusinger and others added 2 commits May 26, 2026 22:37
…attern

Phase 2 of the imprint palette rollout: bring the prompt system in line
with the new 8-hex palette + 3 semantic anchors so every newly generated
implementation inherits the right colours and the n>4 redundant-encoding
rules.

Changes
- default-style-guide.md: full Color Philosophy rewrite — 8-hex hybrid-v3
  table, new "Semantic anchors" section (amber + theme-adaptive neutral
  and muted), "Color Restraint & series-count guidance" table tying
  concrete CVD ΔE values to per-n redundant-encoding requirements, new
  "Optional outline pattern (AI judgment)" section, updated continuous
  cmap definitions (imprint_seq / imprint_div with theme-adaptive
  midpoint).
- plot-generator.md: inline hex string list + cmap definitions updated.
- 11 library prompts (matplotlib / seaborn / plotly / bokeh / altair /
  plotnine / pygal / highcharts / ggplot2 / letsplot / makie):
  ANYPLOT_PALETTE = [...] list updated to 8 hexes in hybrid-v3 order +
  ANYPLOT_AMBER constant added; continuous cmap definitions renamed
  (anyplot_seq → imprint_seq, anyplot_div → imprint_div) with new
  endpoints (green→blue, red↔midpoint↔blue).
- quality-criteria.md + quality-evaluator.md: VQ-07 scoring rules
  updated for positions 1–8 + the 3 anchors. Legacy variant-D hexes now
  listed as auto-reject signals at the 0-point tier.
- workflow-prompts/ai-quality-review.md, impl-generate-claude.md,
  impl-repair-claude.md: cmap names + position ranges updated.

The 7 plot implementations under plots/**/python/ that hardcode the old
palette locally are still untouched on purpose (Phase 4 skipped) — they
keep working with the old hexes until regenerated via /regen, at which
point the new prompts kick in.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 5a of the imprint palette rollout — palette-zentrale frontend
files. The /palette page is now the canonical visual reference for the
new 8-hex palette + 3 semantic anchors, with all the data from the
decision-rationale embedded as live interactive sections.

PalettePage.tsx — complete rebuild (~500 LOC)
- Hero: chroma wheel SVG with 8 swatch dots positioned at their CAM02
  hue angles, brand-green pin on slot 0
- Palette grid: 8 swatches with hex + name + family + role
- Semantic anchors: 3 cards (amber + 2 split-swatch adaptive neutrals)
- Copy & paste: language tabs (Python / R / Julia / JS / Go / Rust),
  OKLCH toggle that switches every snippet between hex and oklch()
  notation, per-language standalone code (no `pip install anyplot`)
- CVD safety: per-n ΔE table with color-coded verdict (safe /
  borderline / fail) and concrete redundant-encoding guidance
- WCAG audit (collapsible): full per-theme contrast table for all 9
  hexes including amber, with pass/fail markers
- History (collapsible): v0 Okabe-Ito → v1 variant D → v2 D1-8 →
  v3 imprint, each with swatch strip + summary

PaletteStrip.tsx — SWATCHES array swapped from 7 Okabe hexes + ink to
8 imprint hexes in hybrid-v3 sort.

theme/index.ts — colors.okabe namespace replaced by colors.imprint
with all 8 hues + amber. Semantic colors remapped: error #D55E00 →
#AE3030 (matte red, slot 4), warning #E69F00 → #DDCC77 (amber anchor),
info #0072B2 → #4467A3 (slot 2). tooltipLight #56B4E9 → #2ABCCD (cyan,
slot 5). accent #E69F00 → #BD8233 (ochre).

tokens.css — --ok-* CSS variables replaced by --imprint-* (green,
lavender, blue, ochre, red, cyan, rose, lime, amber). Code-syntax
theme rebuilt on imprint hues for both light and dark modes.

Not touched (Phase 5b — separate PR)
- LandingPage.tsx (CLUSTER_PALETTE + okabe swatch row + analytics
  tracking event named 'palette_okabe_ito')
- MapPage.tsx (PLOT_TYPE_COLORS)
- AboutPage.tsx / ScienceNote.tsx (text refs)
- CodeShowcase.tsx / CodeHighlighter.tsx (syntax-highlighting hexes)
- LandingPage.test.tsx (event name)

Validated: yarn lint (0 errors), yarn tsc --noEmit (clean),
yarn test --run (507/507 pass), visual smoke test via chrome-devtools
at /palette with both collapsibles expanded.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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

Copilot reviewed 30 out of 40 changed files in this pull request and generated 17 comments.

Comment thread core/palette.py
Comment on lines +33 to +36
from types import SimpleNamespace
from typing import Callable

from matplotlib.colors import LinearSegmentedColormap
Comment thread core/images.py Outdated
Comment on lines +53 to +66
from .palette import (
IMPRINT as ANYPLOT_PALETTE,
GREEN as ANYPLOT_GREEN,
LAVENDER as ANYPLOT_LAVENDER,
BLUE as ANYPLOT_BLUE,
OCHRE as ANYPLOT_OCHRE,
RED as ANYPLOT_RED,
CYAN as ANYPLOT_CYAN,
ROSE as ANYPLOT_ROSE,
LIME as ANYPLOT_LIME,
AMBER as ANYPLOT_AMBER,
palette,
register_with_matplotlib as _register_imprint_cmaps,
)
| 6 | `#D359A7` | pink | |
| 7 | `#BA843E` | tan | |
| 8 | *adaptive neutral* | — | `#1A1A1A` on light theme, `#E8E8E0` on dark theme. Reserved for aggregates, residuals, reference lines. |
8 hues in hybrid-v3 sort order. First 4 slots span 4 distinct perceptual hue families (green / purple / blue / yellow), then greedy max-min CVD distance for the tail. Slot 4 is the deferred semantic-red anchor.
Comment on lines 131 to 136
# Sequential / single-polarity (intensity, magnitude, density)
anyplot_seq = LinearSegmentedColormap.from_list("anyplot_seq", ["#009E73", "#003D94"])
imprint_seq = LinearSegmentedColormap.from_list("imprint_seq", ["#009E73", "#4467A3"])

# Diverging (signed deviations, residuals, correlations)
anyplot_div = LinearSegmentedColormap.from_list("anyplot_div", ["#BB0D22", "#A2A598", "#007AD9"])
imprint_div = LinearSegmentedColormap.from_list("imprint_div", ["#AE3030", "#F5F3EC", "#4467A3"])

Comment thread prompts/library/plotly.md Outdated
Comment on lines 95 to 99
# Continuous — only the two anyplot palette-derived cmaps are allowed:
ANYPLOT_SEQ = [[0.0, "#009E73"], [1.0, "#003D94"]] # sequential / single-polarity
ANYPLOT_DIV = [[0.0, "#BB0D22"], [0.5, "#A2A598"], [1.0, "#007AD9"]] # diverging
ANYPLOT_SEQ = [[0.0, "#009E73"], [1.0, "#4467A3"]] # sequential / single-polarity
ANYPLOT_DIV = [[0.0, "#AE3030"], [0.5, "#F5F3EC"], [1.0, "#4467A3"]] # diverging
# Sequential: color_continuous_scale=ANYPLOT_SEQ
# Diverging: color_continuous_scale=ANYPLOT_DIV
Comment thread prompts/library/pygal.md Outdated
Comment on lines +136 to +138
# Sequential (single-polarity): #009E73 → #4467A3
seq_stops = tuple(_lerp_hex("#009E73", "#4467A3", i / (n - 1)) for i in range(n))
# Diverging (around a meaningful midpoint): #AE3030 ↔ #F5F3EC ↔ #4467A3
Comment thread app/src/styles/tokens.css
Comment on lines +22 to 26
/* Semantic anchors outside the categorical pool */
--imprint-amber: #DDCC77; /* warning / caution (fixed hex) */
/* --imprint-neutral and --imprint-muted are aliased to --ink / --ink-muted
so they automatically flip per theme (totals/baseline → ink, other/rest → ink-muted) */

Comment on lines +276 to +282
function CodeBlock({ code }: { code: string }) {
const [copied, setCopied] = useState(false);
const handleCopy = () => {
navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};
Comment on lines +304 to +314
<Tooltip title={copied ? 'copied' : 'copy'} placement="left">
<IconButton
size="small"
onClick={handleCopy}
sx={{
position: 'absolute', top: 6, right: 6,
color: copied ? colors.primary : 'var(--ink-muted)',
}}
>
<ContentCopyIcon fontSize="small" />
</IconButton>
Comment on lines +324 to +327
<Box
component="button"
onClick={() => setOpen(o => !o)}
sx={{
MarkusNeusinger and others added 2 commits May 26, 2026 23:19
Iteration 2 incorporating reviewer feedback on the page:

Hero
- Terser opening ("every plot on anyplot uses imprint…") with sober tone
  emphasising "easier on the eyes" and "let the data speak" — same family
  as palettes shipped at scale in scientific publishing and editorial
  dashboards. No marketing copy, no link to the markdown rationale.
- Real chroma wheel (Adobe / Dracula palette-page style): CSS conic-gradient
  over OKLCH hues + radial-gradient chroma fade to centre. 8 imprint dots
  positioned by their actual (hue, chroma) coords in CAM02-UCS.
- Compare-with toggle: click Okabe-Ito, Tol muted, or anyplot variant D
  (previous) and the matching palette overlays as hollow rings on the same
  wheel — visualises the chroma + hue differences against imprint.

Palette grid
- slot 1 / slot 2 now carry real role labels (creative / artistic for
  lavender, cool / water / info for blue) instead of just "slot N".

Semantic anchors
- Amber hint rewritten to focus on functional rationale (max ΔE against
  lime under deuteranopia) — dropped the "Paul Tol muted yellow" name-drop
  per the request to keep the copy sober.

Code snippets
- Trimmed to the 4 languages anyplot actually targets: Python, R, Julia,
  JavaScript. Go and Rust gone.
- Dropped the "standalone snippets, no pip install" header — copy speaks
  for itself, the intro text was overhead.

CVD section
- Added a "show alt. sort" toggle that surfaces the pure-CVD-greedy ordering
  alongside imprint (16.34 → 21.45 at n=3, 13.98 → 19.81 at n=4). A short
  explainer below clarifies imprint trades a few ΔE points for visually
  distinct hue families in the first 4 slots; n=8 floor is identical (8.81)
  because both sortings ship the same 8 hexes.

History
- Dropped the dead-end "Used by anyplot until v1" line.
- Each historic palette now has an external link where it leads naturally:
  Okabe-Ito → jfly.uni-koeln.de (same source the LandingPage links to),
  variant D → Petroff 2021 arXiv (since variant D was selected via a
  Petroff-style ΔE search).

Validated: yarn tsc --noEmit (clean), yarn lint (0 errors, 2 pre-existing
warnings), yarn test --run (507/507), visual smoke test via chrome-devtools
including the compare toggle (Okabe-Ito and Tol muted overlays render
correctly with hollow rings, both collapsibles expand cleanly).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Iteration 3 on the /palette page, addressing reviewer feedback that the
8 hex grid was too vertical, per-slot metadata was scattered, and the
CVD-optimal sort needed to be reachable for users who actually want max
discrimination under colorblindness.

Compact slot grid
- 8 hexes are now an 8-column compact grid (4 cols on mobile) matching
  the docs/reference/palette-variants-v3 reference layout. Each cell
  shows the swatch (hex inside, ink-color picked for legibility), then
  below the swatch: slot N + name + role + H=…° + norm/cvd ΔE + WCAG
  ratio per theme (☼ light / ☽ dark). All the per-slot metadata
  collapsed into one card.
- Per-slot min ΔE values pre-computed (vs all other imprint members,
  under normal vision + worst-of-3 CVD sims). Tooltips on the norm/cvd
  cells explain what they mean.

Sort toggle (imprint default ↔ CVD-optimal)
- Pure-CVD-greedy max-min sort surfaced as a one-click toggle alongside
  the categorical grid. Switching it reorders the grid, the strip, the
  wheel-dot mapping (slot 0 stays brand), and the copy snippet — so a
  CVD-first audience can copy the alternative ordering directly.
- An inline note below the toggle calls out the trade-off:
  CVD-optimal sort puts two greens (brand + lime) and two purples
  (lavender + rose) side by side in the first 4 slots — better ΔE
  numbers, worse hue-family diversity. Default stays imprint.
- The per-n ΔE table now shows BOTH sortings side-by-side (with ↑/↓
  arrows so the difference is visible at a glance) instead of a
  conditional toggle column — the trade-off should be visible without
  another click.

Hero text
- "lower-chroma than Tableau" → "low-chroma, warm-tinted for cream
  paper" — kept the spirit, dropped the specific competitor name-drop
  to keep the copy sober.
- Compare-with palettes are now pill buttons WITH a mini-swatch strip
  per palette (not just text links), promoted out of the prose into
  their own block under the description. Easier to scan, more visual.

Wheel
- Size bumped from 300 → 380px on desktop so the chroma differences are
  legible when comparing palettes against each other.

Continuous cmaps (NEW section between anchors and copy/paste)
- imprint_seq (green→blue) and imprint_div (red↔midpoint↔blue) shown
  as live CSS-gradient strips with their endpoint hexes labelled and a
  note about the theme-adaptive midpoint (#F5F3EC light / #1A1A17 dark).
- Cmap definitions also baked into the copy snippets for all 4
  languages — Python uses matplotlib LinearSegmentedColormap, R uses
  scale_color_gradient/_gradient2, Julia uses cgrad, JS uses two-stop
  arrays. midpoint is always theme-adaptive in the code.

PaletteStrip.tsx now accepts an optional `hexes` prop so the palette
page can pass the sorted set without forking the component.

Validated: yarn tsc --noEmit (clean), yarn test --run (507/507),
visual smoke test via chrome-devtools confirms grid+sort+cmap+history
all render and interact correctly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 26, 2026 21:32
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

Copilot reviewed 30 out of 40 changed files in this pull request and generated 10 comments.

Comment thread core/palette.py
Comment on lines +33 to +36
from types import SimpleNamespace
from typing import Callable

from matplotlib.colors import LinearSegmentedColormap
Comment on lines +214 to +220
function snippet(lang: Lang, oklch: boolean, sortedPalette: Swatch[]): string {
const values = oklch ? sortedPalette.map(s => s.oklch) : sortedPalette.map(s => s.hex);
const amberVal = oklch ? 'oklch(0.841 0.108 98.3)' : '#DDCC77';
const seqStart = oklch ? 'oklch(0.620 0.130 165.5)' : '#009E73';
const seqEnd = oklch ? 'oklch(0.516 0.104 260.6)' : '#4467A3';
const divStart = oklch ? 'oklch(0.503 0.163 25.2)' : '#AE3030';
const divEnd = oklch ? 'oklch(0.516 0.104 260.6)' : '#4467A3';
Comment on lines +250 to +253
return `const ANYPLOT_PALETTE = [
${values.map(v => oklch ? ` "${v}"` : ` colorant"${v}"`).join(',\n')},
]
const ANYPLOT_AMBER = ${oklch ? `"${amberVal}"` : `colorant"${amberVal}"`} # warning
Comment on lines +231 to +235
# continuous data — sequential + diverging cmaps
from matplotlib.colors import LinearSegmentedColormap
imprint_seq = LinearSegmentedColormap.from_list("imprint_seq", ["${seqStart}", "${seqEnd}"])
midpoint = "#F5F3EC" if THEME == "light" else "#1A1A17" # theme-adaptive
imprint_div = LinearSegmentedColormap.from_list("imprint_div", ["${divStart}", midpoint, "${divEnd}"])`;
Comment on lines +245 to +248
# continuous data — ggplot2 gradient scales
midpoint <- if (theme == "light") "#F5F3EC" else "#1A1A17"
scale_color_gradient(low = "${seqStart}", high = "${seqEnd}") # sequential
scale_color_gradient2(low = "${divStart}", mid = midpoint, high = "${divEnd}", midpoint = 0) # diverging`;
Comment on lines +258 to +262
# continuous data — Makie cgrad
using ColorSchemes
midpoint = theme == "light" ? colorant"#F5F3EC" : colorant"#1A1A17"
const ANYPLOT_SEQ = cgrad([colorant"${seqStart}", colorant"${seqEnd}"])
const ANYPLOT_DIV = cgrad([colorant"${divStart}", midpoint, colorant"${divEnd}"])`;
Comment on lines +272 to +275
// continuous data — two-stop / three-stop gradients
const midpoint = theme === "light" ? "#F5F3EC" : "#1A1A17"; // theme-adaptive
const IMPRINT_SEQ = [[0, "${seqStart}"], [1, "${seqEnd}"]];
const IMPRINT_DIV = [[0, "${divStart}"], [0.5, midpoint], [1, "${divEnd}"]];`;
Comment on lines +385 to +389
const handleCopy = () => {
navigator.clipboard.writeText(code);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
};
Comment on lines +431 to +434
<Box
component="button"
onClick={() => setOpen(o => !o)}
sx={{
## Color Philosophy

anyplot uses the **anyplot palette** — a bespoke, colorblind-safe categorical palette tuned for paper-ink rendering on both warm-cream and warm-black surfaces. It is the single consistency rule that spans every library and every plot.
anyplot uses the **anyplot palette** (codename **imprint**) — a bespoke, colorblind-safe categorical palette tuned for paper-ink rendering on both warm-cream and warm-black surfaces. It is the single consistency rule that spans every library and every plot. Full design rationale: [`docs/reference/palette-variants-v3/decision-rationale.md`](../docs/reference/palette-variants-v3/decision-rationale.md).
MarkusNeusinger and others added 2 commits May 26, 2026 23:45
…op redundant strip

Iteration 4 on /palette per feedback:

Hero
- Title "imprint" → "anyplot's imprint palette" (better positioning).
- Compare-with palettes now 4: Okabe-Ito, Tol muted, ColorBrewer Set2
  (NEW), and the previous anyplot palette renamed from "variant D
  (previous)" to plain "anyplot (previous)".
- "lower-chroma than Tableau" already softened in iteration 3; no
  competitor name-drops on the page.
- The full-width PaletteStrip below the hero is gone — it duplicated
  the new 8-card grid below and added no information.

Chroma wheel
- Each imprint dot and overlay ring now has an SVG `<title>` element so
  hover surfaces the hex code natively (no JS tooltip needed). Dots use
  `cursor: pointer` to signal interactivity.

The 8 categorical hues
- Grid expanded from 8-column compact to 4-column × 2-row layout so the
  per-slot metadata is actually readable (was 9px font, now 11px). Each
  swatch is now 96px tall instead of 72px.
- Click a swatch → hex is copied to clipboard, swatch text flips to
  "copied ✓" for 1.5s. Tooltip on hover: "click to copy hex".
- Removed "no other cmap is used — not viridis, not jet, not rainbow"
  from the cmaps section. We don't need to enumerate what we don't use.

History
- Per-version date column removed — "2026" / "May 2026" / "2002 / 2011"
  added noise without telling the reader anything load-bearing. Title
  + external link (where present) are enough.

PaletteStrip
- Component is no longer imported from this page but retains the
  `hexes` prop it gained in iteration 3 — it's still used by other
  callers (LandingPage).

Validated: yarn tsc --noEmit (clean), visual smoke test confirms
script-italic section headers render correctly (font-feature-settings:
ss02, MonoLisa italic) — same rendering pipeline as the LandingPage
section headers.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ded view

The "best variant for CVD users" section was just restating per-slot ΔE
numbers that were already visible on every card in the 8-hue grid —
the only thing it actually added was the trade-off comparison, which is
only relevant if someone is *considering* switching to the CVD-optimal
sort. So move that content into the inline note that appears when the
sort toggle is set to CVD-optimal.

When sort = imprint (default), the page is now shorter — palette grid,
anchors, cmaps, copy, then directly into WCAG / history. No empty
section repeating data.

When sort = CVD-optimal, an expanded block appears between the toggle
and the grid with:
- why-use rationale (CVD-first audience, small-n, colour-only encoding)
- compact per-n table showing imprint default vs CVD-optimal with the
  ΔE gain in green where it's positive (n=2..6) and grey where it's
  identical (n=7..8)
- explicit trade-off explanation (two greens + two purples in first 4)
- "when to switch" guidance

Cleanup: dropped the unused verdictFor helper that the removed section
was the only caller of; renumbered the section comments.

Validated: yarn tsc --noEmit (clean), yarn test --run (507/507),
visual smoke test confirms the expanded block renders correctly when
CVD-optimal is active and disappears when switching back to imprint.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 26, 2026 21:49
…is enough

The hex → "copied ✓" swap on the swatch itself is already a clear
interaction signal; the tooltip explaining what the click does was
adding noise on hover for a behaviour that's obvious from the result.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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

Copilot reviewed 30 out of 40 changed files in this pull request and generated 19 comments.

Comment thread core/palette.py
Comment on lines +33 to +36
from types import SimpleNamespace
from typing import Callable

from matplotlib.colors import LinearSegmentedColormap
Comment thread core/images.py Outdated
Comment on lines +63 to +66
AMBER as ANYPLOT_AMBER,
palette,
register_with_matplotlib as _register_imprint_cmaps,
)
Comment thread app/src/styles/tokens.css
Comment on lines +12 to +15
/* imprint palette — 8 categorical hues in hybrid-v3 sort order */
--imprint-green: #009E73; /* slot 0 — brand */
--imprint-lavender: #C475FD; /* slot 1 */
--imprint-blue: #4467A3; /* slot 2 */
Comment on lines 131 to 136
# Sequential / single-polarity (intensity, magnitude, density)
anyplot_seq = LinearSegmentedColormap.from_list("anyplot_seq", ["#009E73", "#003D94"])
imprint_seq = LinearSegmentedColormap.from_list("imprint_seq", ["#009E73", "#4467A3"])

# Diverging (signed deviations, residuals, correlations)
anyplot_div = LinearSegmentedColormap.from_list("anyplot_div", ["#BB0D22", "#A2A598", "#007AD9"])
imprint_div = LinearSegmentedColormap.from_list("imprint_div", ["#AE3030", "#F5F3EC", "#4467A3"])

Comment thread prompts/library/altair.md
Comment on lines 142 to 146
# Diverging: three-stop range with domainMid at 0
alt.Color('delta:Q', scale=alt.Scale(
range=['#BB0D22', '#A2A598', '#007AD9'],
range=['#AE3030', '#F5F3EC', '#4467A3'],
domainMid=0,
))
Comment on lines +46 to +48
1. pure-CVD greedy max-min (slowest possible per-n ΔE degradation;
pos 0 fixed brand-green; pos 1 fixed at
muted-8's semantic red for stability)
Comment thread core/images.py Outdated
Comment on lines +48 to +52
# anyplot categorical palette — "imprint" (v3 hybrid-v3 ordering).
# Defined as a separate module so the project's named-API (palette.green,
# palette.semantic.bad, etc.) plus sequential / diverging cmaps live in one
# place. Full design rationale:
# docs/reference/palette-variants-v3/decision-rationale.md
Comment on lines +420 to +423
<IconButton
size="small"
onClick={handleCopy}
sx={{
Comment on lines +439 to +442
<Box
component="button"
onClick={() => setOpen(o => !o)}
sx={{
Comment thread app/src/styles/tokens.css
Comment on lines +22 to +25
/* Semantic anchors outside the categorical pool */
--imprint-amber: #DDCC77; /* warning / caution (fixed hex) */
/* --imprint-neutral and --imprint-muted are aliased to --ink / --ink-muted
so they automatically flip per theme (totals/baseline → ink, other/rest → ink-muted) */
MarkusNeusinger and others added 2 commits May 27, 2026 00:08
…print

Closes the residual Okabe-Ito footprint after the imprint palette
adoption. Frontend pages, components, tests, and the canonical style
guide now consistently reference the imprint palette and its semantic
anchors.

Phase 5b — frontend consumers
- LandingPage.tsx: CLUSTER_PALETTE swapped (#009E73/#AE3030/#4467A3),
  the 7-Okabe PALETTE swatch row replaced by the 8-hex imprint set
  with proper names. Click-to-copy now works on each swatch (hex flips
  to "copied ✓" for 1.5s, same UX as PalettePage). Palette section
  prose rewritten — drops the Okabe & Ito attribution since the
  palette is no longer just an Okabe import. The 'palette_okabe_ito'
  analytics tracking event is gone with the link it used to attach to.
- MapPage.tsx: CLUSTER_COLORS rebuilt with the 8 imprint hexes in
  hybrid-v3 sort order (slots 0..7) — previously 7 Okabe-Ito hexes.
- AboutPage.tsx: palette paragraph reframed around imprint (still
  cites Okabe-Ito / Tol / ColorBrewer as neighbours of the same
  family).
- ScienceNote.tsx: blockquote attribution changed from "Okabe & Ito,
  Color Universal Design (2008)" to "anyplot imprint, design rationale"
  — the surrounding values copy still applies, just sourced honestly.
- CodeShowcase.tsx: 19 hardcoded Okabe hexes (#56B4E9/#E69F00/#009E73)
  in the syntax-highlighting demo block swapped to imprint dark-theme
  syntax tones (#2ABCCD cyan keywords, #99B314 lime functions,
  #DDCC77 amber strings) — terminal box is always-dark so theme-aware
  CSS vars don't apply. Also fixed a now-broken --ok-green reference
  to --imprint-green.
- CodeHighlighter.tsx: `okabeItoTheme` constant renamed to
  `imprintTheme`. Body of the theme stays unchanged (uses CSS vars
  from tokens.css which Phase 5a already migrated).
- LandingPage.test.tsx: drops the test that asserted the
  'palette_okabe_ito' analytics event and the Okabe-Ito link click —
  both are gone from the page.
- PalettePage.tsx: drops the "same neighbourhood as palettes that
  have shipped at scale in scientific publishing and editorial
  dashboards" line per feedback — kept the copy sober.

Phase 3 — docs/reference/style-guide.md
- Section 4.1 ("The Okabe-Ito Palette") fully rewritten as "The
  imprint Palette" with the new hue table, semantic-anchors table,
  hybrid-v3 sort rationale, and a link to the v3 decision rationale.
- Section 4.5 (Status Colors) remapped onto imprint hues + amber
  anchor: error → #AE3030 matte red, warning → #DDCC77 amber, info →
  #4467A3 blue, hover → #BD8233 ochre.
- Section 4.6 (Plot-only Colors) updated — lavender / cyan / rose /
  lime are the new plot-only set.
- Section 9.1 — replaces the "yellow on white" caveat with a broader
  WCAG note linking to the outline pattern in the v3 rationale.
- Section 9.2 — categorical-vs-continuous guidance now points to the
  ANYPLOT-shipped `imprint_seq` and `imprint_div` cmaps rather than
  recommending viridis/cividis/BrBG (which were never anyplot's
  endorsed choice — clarified now).
- Section 9.5 — Python implementation reference updated to the new
  `core.palette` module API; CSS variable block renamed --ok-* →
  --imprint-* and extended with all 8 categorical slots + the amber
  anchor.
- Section 11 — reference CSS skeleton updated with full imprint set.
- All remaining text references to "Okabe-Ito" / `--ok-green` etc.
  rewritten to point at imprint. Okabe-Ito only kept in the historical
  references list (§13) and where named as a neighbour family.

Validated: yarn tsc --noEmit (clean), yarn lint (0 errors, 2 pre-
existing warnings), yarn test --run (507/507).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…e refs

Final cross-cutting cleanup after Phase 5b + Phase 3. Audit grep across
*.md / *.py / *.ts / *.tsx / *.css / *.yml / *.yaml for any remaining
Okabe-Ito or variant-D hex references; only legitimate historical mentions
(comparison families, history sections, release notes, palette-variants
exploration scripts) were left intact.

Updates
- app/src/pages/LandingPage.tsx: file-header comment "the same Okabe-Ito
  palette" → "the same imprint palette".
- core/images.py: 3 doc comments referencing `--ok-green` updated to
  `--imprint-green` to match the tokens.css migration from Phase 5a.
- docs/reference/plausible.md: drop the `palette_okabe_ito` analytics
  event row — the event was removed in Phase 5b.
- docs/reference/anyplot-landing-mockup.html: prepend a HISTORICAL header
  comment explaining this mockup predates both the MonoLisa-only
  typography and the imprint palette, and pointing readers at the
  current design system. Body left unchanged (44 hex refs preserved as
  design-archaeology context).
- scripts/style-variants.yaml: the `palette_tableau` sanity-check variant
  was mapping from the old variant-D hexes — rewired to the imprint set
  so the substitution still produces a Tableau-coloured plot.
- scripts/palette-analysis.py: docstring extended with a HISTORICAL
  marker pointing to core/palette.py and the v3 decision rationale as
  the current sources of truth.
- .serena/memories/style_guide.md: "Color — Okabe-Ito palette" section
  rewritten as "Color — imprint palette" with the 8-slot table and 3
  semantic anchors. Broken `--ok-green` refs updated to `--imprint-green`.
  Continuous-cmap note updated to point at imprint_seq / imprint_div.

What stays as legitimate historical refs (no change needed)
- core/palette.py module docstring + GREEN constant comment — acknowledge
  brand green originated with Okabe-Ito
- app/AboutPage.tsx, app/PalettePage.tsx, docs/reference/style-guide.md,
  prompts/default-style-guide.md — cite Okabe-Ito / Tol muted /
  ColorBrewer Set2 as members of the same academic-publishing family
- prompts/quality-criteria.md — lists legacy variant-D hexes as
  auto-reject signals in VQ-07 scoring
- agentic/runs/release-v2.0.0-notes.md — historical v2.0.0 release notes
- docs/reference/palette-variants-v1/v2/v3/ — full design history
- scripts/palette-variants-v1/v2/v3.py + scripts/_palette_common.py —
  variant exploration scripts that produced the current spec
- .playwright-mcp/ — auto-generated test recording yaml files

Validated: yarn tsc --noEmit (clean), yarn lint (0 errors, 2 pre-existing
warnings), yarn test --run (507/507).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 26, 2026 22:16
@MarkusNeusinger MarkusNeusinger changed the title feat(palette): variants v1 + grid token harmonization to 0.15 feat(palette): adopt imprint — new categorical palette + named API + frontend rebuild May 26, 2026
CI lint run flagged the imports in core/images.py:
- E402 (module-level import not at top of file): the `from .palette import …`
  block sat inside the design-tokens section after constants
- I001 (unsorted import block): related; ruff prefers alphabetical with
  one-per-line `as` aliases

Fix: lift the palette imports to the top of core/images.py next to the
other module imports. Ruff reformatted them to one-per-line `from .palette
import X as ANYPLOT_X` style. Added `# noqa: F401` on the three symbols
that are re-exported public API (`ANYPLOT_PALETTE`, `ANYPLOT_AMBER`,
`palette`) so ruff doesn't flag them as unused.

Also picked up `ruff format` on core/palette.py — cosmetic only (line-wrap
of the SimpleNamespace `semantic=...` to fit the configured line length).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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

Copilot reviewed 43 out of 53 changed files in this pull request and generated 8 comments.

Comment thread core/palette.py
Comment on lines +33 to +35

from types import SimpleNamespace

Comment on lines 319 to +323
<Box
className="hex-label"
key={background}
component="button"
onClick={() => handleCopy(hex)}
className="swatch"
Comment on lines 127 to +130
from matplotlib.colors import LinearSegmentedColormap
anyplot_seq = LinearSegmentedColormap.from_list("anyplot_seq", ["#009E73", "#003D94"])
anyplot_div = LinearSegmentedColormap.from_list("anyplot_div", ["#BB0D22", "#A2A598", "#007AD9"])
# Sequential / single-polarity heatmaps: cmap=anyplot_seq
# Diverging (signed deviations, residuals, correlations): cmap=anyplot_div
imprint_seq = LinearSegmentedColormap.from_list("imprint_seq", ["#009E73", "#4467A3"])
midpoint = "#F5F3EC" if THEME == "light" else "#1A1A17" # theme-adaptive
imprint_div = LinearSegmentedColormap.from_list("imprint_div", ["#AE3030", midpoint, "#4467A3"])
Comment thread prompts/library/plotly.md Outdated
Comment on lines 95 to 99
# Continuous — only the two anyplot palette-derived cmaps are allowed:
ANYPLOT_SEQ = [[0.0, "#009E73"], [1.0, "#003D94"]] # sequential / single-polarity
ANYPLOT_DIV = [[0.0, "#BB0D22"], [0.5, "#A2A598"], [1.0, "#007AD9"]] # diverging
ANYPLOT_SEQ = [[0.0, "#009E73"], [1.0, "#4467A3"]] # sequential / single-polarity
ANYPLOT_DIV = [[0.0, "#AE3030"], [0.5, "#F5F3EC"], [1.0, "#4467A3"]] # diverging
# Sequential: color_continuous_scale=ANYPLOT_SEQ
# Diverging: color_continuous_scale=ANYPLOT_DIV
Comment on lines +1 to +6
# palette-v3 — muted-8 finalist (decision rationale)

> Generated by [`scripts/palette-variants-v3.py`](../../../scripts/palette-variants-v3.py).
> Companion to [`index.html`](./index.html) (live charts) and the v2 review at
> [`../palette-variants-v2/expert-reviews.md`](../palette-variants-v2/expert-reviews.md).

Comment thread core/images.py
Comment on lines 53 to +71
RESPONSIVE_SIZES = [1200, 800, 400]
RESPONSIVE_FORMATS: list[tuple[str, str, dict]] = [("png", "PNG", {}), ("webp", "WEBP", {"quality": 80})]
WEBP_FULL_QUALITY = 85

# GCS bucket for static assets (fonts)
GCS_STATIC_BUCKET = "anyplot-static"
MONOLISA_FONT_PATH = "fonts/MonoLisaVariableNormal.ttf"
MONOLISA_ITALIC_FONT_PATH = "fonts/MonoLisaVariableItalic.ttf"
FONT_CACHE_DIR = Path("/tmp/anyplot-fonts")

# =============================================================================
# Design tokens — match docs/reference/style-guide.md (§4 Color System)
# =============================================================================
#
# Theme dicts so OG images can be rendered light or dark from the same code.
# Token names mirror the CSS custom properties defined in the React app
# (`--bg-page`, `--ink`, `--ok-green`, etc.) so the OG cards read as a direct
# translation of the in-product surfaces.

# anyplot categorical palette — variant D ("balanced") from the palette
# exploration in #5817. Position 1 (#009E73) carries the same bluish green
# as Okabe-Ito's first slot so the brand identity (`any.plot()` dot, first
# series) is preserved; positions 2–7 are selected via Petroff-style
# max-min ΔE search in the CAM02-UCS paper-ink corridor (J' ∈ [45,72],
# C ∈ [22,36]). See docs/reference/palette-variants/D-balanced.html for
# the derivation and CVD analysis.
ANYPLOT_GREEN = "#009E73" # brand anchor — the dot in any.plot(); ALWAYS first series
ANYPLOT_PURPLE = "#9418DB"
ANYPLOT_RED = "#B71D27"
ANYPLOT_SKY = "#16B8F3"
ANYPLOT_LIME = "#99B314"
ANYPLOT_PINK = "#D359A7"
ANYPLOT_TAN = "#BA843E"

ANYPLOT_PALETTE = [ANYPLOT_GREEN, ANYPLOT_PURPLE, ANYPLOT_RED, ANYPLOT_SKY, ANYPLOT_LIME, ANYPLOT_PINK, ANYPLOT_TAN]
# (`--bg-page`, `--ink`, `--imprint-green`, etc.) so the OG cards read as a direct
# translation of the in-product surfaces. The imprint palette itself is
# re-exported from core/palette at the top of this module.
Comment on lines +300 to +304
const handleCopy = (hex: string) => {
navigator.clipboard.writeText(hex);
setCopiedHex(hex);
setTimeout(() => setCopiedHex((c) => (c === hex ? null : c)), 1500);
};
Comment thread prompts/library/makie.md Outdated
const ANYPLOT_SEQ = cgrad([colorant"#009E73", colorant"#003D94"]) # sequential / single-polarity
const ANYPLOT_DIV = cgrad([colorant"#BB0D22", colorant"#A2A598", colorant"#007AD9"]) # diverging
const ANYPLOT_SEQ = cgrad([colorant"#009E73", colorant"#4467A3"]) # sequential / single-polarity
const ANYPLOT_DIV = cgrad([colorant"#AE3030", colorant"#F5F3EC", colorant"#4467A3"]) # diverging (midpoint #F5F3EC light / #1A1A17 dark)
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

Copilot reviewed 43 out of 53 changed files in this pull request and generated 6 comments.

Comment on lines +300 to +304
const handleCopy = (hex: string) => {
navigator.clipboard.writeText(hex);
setCopiedHex(hex);
setTimeout(() => setCopiedHex((c) => (c === hex ? null : c)), 1500);
};
Comment on lines 319 to +323
<Box
className="hex-label"
key={background}
component="button"
onClick={() => handleCopy(hex)}
className="swatch"
Comment thread docs/reference/style-guide.md Outdated
Comment on lines 1185 to 1187
- **Lavender (`#C475FD`), cyan (`#2ABCCD`), rose (`#954477`), or lime (`#99B314`) in UI chrome.** These are plot-only colors. Using them in navigation or buttons breaks the color hierarchy.
- **Brand green in backgrounds, body text emphasis, or non-logo icons.** Reserve `#009E73` for the seven approved contexts (§4.4).
- **Categorical palettes on continuous data.** Use viridis/cividis/BrBG instead — see §9.2.
Comment thread core/images.py
Comment on lines 73 to 74
LIGHT_THEME: dict[str, str] = {
"bg_page": "#F5F3EC", # warm cream — matches `--bg-page` in app/src/styles/tokens.css
Comment on lines +88 to +92
Categorical palettes on continuous data produce misleading banding. anyplot ships exactly two palette-derived continuous colormaps; **no other cmap is allowed** — not viridis, not cividis, not BrBG, not Blues/Greens/Reds, and never jet/hsv/rainbow. Both anyplot cmaps are perceptually-uniform in CAM02-UCS:

- **`imprint_seq` (sequential):** brand green → blue. Use for single-polarity continuous data (intensity, magnitude, density, single-polarity heatmaps). Build with `LinearSegmentedColormap.from_list("imprint_seq", ["#009E73", "#4467A3"])` (matplotlib) or the library's equivalent two-stop gradient API.
- **`imprint_div` (diverging):** matte-red ↔ near-neutral ↔ blue. Use when the data has a meaningful midpoint (correlations, residuals, signed deviations, diverging heatmaps). The midpoint is theme-adaptive — use `#F5F3EC` on light bg, `#1A1A17` on dark bg. Build with `LinearSegmentedColormap.from_list("imprint_div", ["#AE3030", midpoint, "#4467A3"])`.

Comment thread core/palette.py
Comment on lines +145 to +158
def diverging(theme: str = "light") -> LinearSegmentedColormap:
"""Diverging cmap: matte-red ↔ near-neutral ↔ blue.

The midpoint flips per theme to keep the diverging "zero" reading as
part of the chart bg rather than as an opaque grey blob.

Parameters
----------
theme : "light" | "dark", default "light"
On light bg, midpoint = warm cream #F5F3EC; on dark bg, midpoint
= warm near-black #1A1A17.
"""
midpoint = "#F5F3EC" if theme == "light" else "#1A1A17"
return LinearSegmentedColormap.from_list(f"imprint_div_{theme}", [RED, midpoint, BLUE])
Triaged the 14 inline comments from the two most recent Copilot reviews
(IDs 4367910318 + 4367970615). Applied 9 of them; documented 3 as skip
in the PR thread (Callable already removed, PR-title-mismatch already
fixed by the title rewrite, "rule opacity 0.10/0.15" extended into a
real cross-file sweep below).

Diverging cmap midpoint — switched from #F5F3EC (bg-page) to #FAF8F1
(bg-surface) so the "zero" band fades into the actual plot surface
rather than the page background.
- core/palette.py:diverging("light") midpoint updated.
- prompts/default-style-guide.md, prompts/plot-generator.md,
  prompts/workflow-prompts/impl-generate-claude.md, all 11 library
  prompts, docs/reference/style-guide.md, app/src/pages/PalettePage.tsx
  (gradient strip + 4 language snippets + descriptive paragraph) all
  updated to match.
- Dark theme midpoint stays #1A1A17 (also bg-surface in dark).

Theme-adaptive midpoint in cmap snippets — several library prompts
were hardcoding the light midpoint and would render the wrong neutral
in dark theme. Made them theme-adaptive:
- matplotlib.md: already used `midpoint = "#…" if THEME == "light" else "#…"` — only the hex changed.
- seaborn.md, bokeh.md: added the same conditional + use a midpoint
  variable in the cmap definition.
- plotly.md: renamed ANYPLOT_SEQ / ANYPLOT_DIV to imprint_seq /
  imprint_div for consistency with the rest of the prompt corpus, and
  switched to a theme-adaptive midpoint.
- makie.md: added `const _midpoint = THEME == "light" ? … : …` and use
  it in the cgrad call.

Frontend interaction polish — clipboard API can throw in insecure
contexts and returns a Promise that can reject:
- LandingPage.tsx and PalettePage.tsx (both copyHex and the CodeBlock
  handleCopy) now wrap the writeText call in .then/.catch so the
  "copied" state only flips on success and unhandled rejections don't
  leak to the console.
- All `<Box component="button">` instances in PalettePage and
  LandingPage now set `type="button"` explicitly so they can't
  accidentally submit if dropped inside a `<form>`. Swatch buttons
  also got aria-label="Copy {hex} to clipboard" for screen readers.

core/images.py — removed the import-time matplotlib colormap
registration side effect. core.images is PIL-only and adding a
matplotlib import on the OG-image router path was excessive. Callers
that render with matplotlib should call
`core.palette.register_with_matplotlib()` themselves. Comment block
in core/images.py documents this. The unused import alias is gone.

Rule-opacity 0.10 → 0.15 cross-file sweep — Copilot flagged that
core/images.py rule tokens were still the old 0.10-flattened values
even though prompts moved to 0.15 in commit 0b4f417. Closing the
loop:
- core/images.py LIGHT_THEME["rule"]: #DFDDD6 → #D4D2CC (0.15
  flattened on cream bg)
- core/images.py DARK_THEME["rule"]: #1E1E1B → #33332F (0.15
  flattened on warm near-black bg)
- app/src/styles/tokens.css --rule (both themes): 0.10 → 0.15

docs/reference/style-guide.md — anti-pattern section at line 1187
still recommended viridis/cividis/BrBG for continuous data, which
contradicted §9.2's updated rule. Fixed to point at imprint_seq /
imprint_div. Same fix at line 314 (Python library palette listing).

Validated: yarn tsc --noEmit (clean), yarn lint (0 errors, 2
pre-existing warnings unrelated), yarn test --run (511/511 passing),
uv run ruff check . (clean), backend smoke-test confirms
ANYPLOT_PALETTE + palette named API + LIBRARY_COLORS all work after
the registration call was removed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@MarkusNeusinger
Copy link
Copy Markdown
Owner Author

Copilot review feedback — triage & fixes

Pulled the 14 inline comments from the two most recent Copilot reviews and applied the actionable ones. Push: 9099e4dd7.

Applied (9)

  • core/palette.py: light-theme diverging midpoint moved from #F5F3EC (bg-page) to #FAF8F1 (bg-surface) so the "zero" band visually disappears into the plot card rather than the page bg. Dark midpoint stays #1A1A17 (also bg-surface in dark theme). Propagated across all 11 library prompts, plot-generator.md, default-style-guide.md, workflow-prompts/impl-generate-claude.md, style-guide.md, and the /palette page (gradient strip + all 4 language snippets + descriptive paragraph).
  • Theme-adaptive midpoint in cmap snippets: matplotlib.md already had it; added the same midpoint = "#…" if THEME == "light" else "#…" conditional to seaborn.md, bokeh.md, plotly.md, makie.md (and renamed plotly's ANYPLOT_SEQ/DIV to imprint_seq/div for consistency).
  • Clipboard API hardening in LandingPage.tsx + PalettePage.tsx (both copyHex and CodeBlock.handleCopy): wrapped navigator.clipboard.writeText() in .then() / .catch() — "copied" state only flips on success, unhandled-rejection warnings gone in insecure contexts.
  • type="button" + aria-label="Copy {hex} to clipboard" on all <Box component="button"> swatch buttons in PalettePage (4 buttons) and LandingPage (1).
  • core/images.py: removed the import-time _register_imprint_cmaps() side effect. core.images is PIL-only; matplotlib cmap registration is now opt-in via core.palette.register_with_matplotlib(). Documented in a comment.
  • Anti-pattern line in style-guide.md §10.3 + Python library palette listing in §3 updated to point at imprint_seq / imprint_div rather than viridis/cividis/BrBG (was contradicting §9.2).

Extended pickup — rule opacity 0.10 → 0.15
Copilot flagged that core/images.py's rule tokens were still the old 0.10-flattened values even though prompts moved to 0.15 in commit 0b4f41735. Confirmed tokens.css had the same drift. Closed the loop:

  • tokens.css --rule: rgba(…, 0.10)rgba(…, 0.15) in both themes.
  • core/images.py LIGHT_THEME[rule]: #DFDDD6#D4D2CC (precomputed 0.15 on cream).
  • core/images.py DARK_THEME[rule]: #1E1E1B#33332F (precomputed 0.15 on warm near-black).

Skipped (3)

  • Unused Callable import in core/palette.py: already removed by ruff --fix in commit c5c7266c6.
  • PR description / title mismatch: already corrected to "feat(palette): adopt imprint — new categorical palette + named API + frontend rebuild" with full body refresh.
  • Repeated suggestions from earlier review rounds that are already addressed in later commits.

Validated: yarn tsc --noEmit clean, yarn lint 0 errors, yarn test --run 511/511 passing, uv run ruff check . clean, backend smoke-test confirms ANYPLOT_PALETTE + palette named API + LIBRARY_COLORS all import correctly after the registration call was removed.

🤖 Generated with Claude Code

@MarkusNeusinger MarkusNeusinger merged commit b2d5b0a into main May 26, 2026
7 checks passed
@MarkusNeusinger MarkusNeusinger deleted the feat/palette-variants-v1 branch May 26, 2026 22:46
MarkusNeusinger added a commit that referenced this pull request May 27, 2026
…generate (#7713)

## Summary

- Fixes a regression from #7692 where `import core.images` started
transitively requiring matplotlib via `core.palette`'s module-level
`from matplotlib.colors import LinearSegmentedColormap`.
- That broke the `Process plot images (light + dark)` step in
`impl-generate.yml` for **non-Python** libraries (ggplot2 / makie),
where the runner only installs PIL helpers — not matplotlib.
raincloud-basic R and Julia auto-failed 3× with `ModuleNotFoundError: No
module named 'matplotlib'`.
- Keeps `core/palette.py` matplotlib-free at module level. Cmap factory
(`diverging`) and `imprint_seq` / `imprint_div_{light,dark}` are
constructed lazily on first attribute access via module `__getattr__`.

## Why this is the right layering

`core/images.py` is documented as PIL-only — the whole point of the
reviewer feedback on #7692 was that importing it must not pull
matplotlib in as a side effect. R/Julia plot scripts hardcode hex codes;
they don't need matplotlib for rendering. But the workflow calls `python
-m core.images process plot.png` afterwards to optimise the rendered PNG
with PIL — and that's where the missing matplotlib bites.

## Public API preserved

- `from core.palette import imprint_seq` ✅ (lazy)
- `core.palette.imprint_seq.name == "imprint_seq"` ✅
- `register_with_matplotlib()` still registers all three cmaps ✅
- Re-import in same process still cached after first build ✅

## Test plan

- [x] `uv run ruff check .` clean
- [x] `uv run ruff format --check .` clean
- [x] Local smoke test: hex constants + lazy cmaps +
register_with_matplotlib
- [x] Local smoke test simulating no-matplotlib env: `import
core.images` succeeds, `LIBRARY_COLORS` populated
- [ ] CI green
- [ ] Re-trigger raincloud-basic ggplot2 + makie after merge — confirm
green

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

---------

Co-authored-by: Claude Opus 4.7 <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