Skip to content

Commit 1d46297

Browse files
feat(prompts): Okabe-Ito palette + theme-adaptive chrome + VQ-07 review
Phase A of the big plot migration. Swaps #306998 (Python Blue) for #009E73 (Okabe-Ito brand green) across the 11 generation prompts — plot-generator.md, default-style-guide.md, and the 9 library prompts. Each library prompt now ships a theme-adaptive chrome mapping (PAGE_BG / ELEVATED_BG / INK / INK_SOFT / RULE tokens) so implementations render once per theme via ANYPLOT_THEME and emit plot-light.png + plot-dark.png (plus plot-light.html + plot-dark.html for interactive libs). Continuous data uses viridis/cividis/BrBG — never jet/rainbow on Okabe-Ito categorical data. Adds VQ-07 "Palette Compliance" (2 pts) to quality-criteria.md and reduces VQ-04 from 4 to 2 pts (pure contrast/CVD check; palette choice is scored separately now) — Visual Quality stays at 30 pts total. quality-evaluator.md and ai-quality-review.md require inspection of BOTH theme renders; impl-repair-claude.md lists common VQ-07 fixes. Existing implementations still render #306998 on white backgrounds — Phase D will regenerate them incrementally via daily batches once Phase B (multi-language paths + DB schema) and Phase C (dark/light pipeline in workflows) land. A, B, C are independently shippable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f8e44c2 commit 1d46297

16 files changed

Lines changed: 718 additions & 244 deletions

prompts/default-style-guide.md

Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -30,37 +30,45 @@ Two formats are allowed (similar pixel count for consistent font sizing):
3030

3131
## Color Philosophy
3232

33-
### Primary Color
33+
anyplot uses the **Okabe-Ito palette** — a peer-reviewed, colorblind-safe categorical palette designed for scientific publication. It is the single consistency rule that spans every library and every plot.
3434

35-
**Python Blue `#306998`** is the only recommended color for single-series plots. It is the brand anchor for every anyplot visualization.
35+
### Categorical Palette (Okabe-Ito, canonical order)
3636

37-
### Multi-Series Palettes
37+
| # | Hex | Name | Role |
38+
|---|-----|------|------|
39+
| 1 | `#009E73` | bluish green |**brand — ALWAYS first series** |
40+
| 2 | `#D55E00` | vermillion | |
41+
| 3 | `#0072B2` | blue | |
42+
| 4 | `#CC79A7` | reddish purple | |
43+
| 5 | `#E69F00` | orange | |
44+
| 6 | `#56B4E9` | sky blue | |
45+
| 7 | `#F0E442` | yellow | Position ≥7 only — never thin lines or small markers (low luminance on light surfaces) |
46+
| 8 | *adaptive neutral* || `#1A1A1A` on light theme, `#E8E8E0` on dark theme. Reserved for aggregates, residuals, reference lines. |
3847

39-
For plots with multiple series, the AI chooses a cohesive, colorblind-safe palette **starting with Python Blue**:
40-
- The first series is always Python Blue `#306998`
41-
- Additional colors are chosen by the AI to complement Python Blue while maintaining colorblind safety
42-
- No hardcoded second, third, or fourth color — the AI picks what works best for the specific data and context
48+
**Hard rules:**
49+
- **First series is ALWAYS `#009E73`** — across every library, every plot type. A "Gentoo penguin" is the same green in matplotlib as it is in plotly.
50+
- Use positions 1→N in order. Don't cherry-pick.
51+
- Only position 8 changes between light and dark themes; positions 1–7 stay identical so a category keeps its identity.
52+
- Never introduce custom hex values when the Okabe-Ito palette already covers the need.
4353

44-
### Color Restraint
54+
### Continuous Data — Okabe-Ito is NOT used
55+
56+
Categorical palettes on continuous data produce misleading banding. For continuous data, use perceptually-uniform colormaps:
4557

46-
- **2-3 colors** is ideal for most plots
47-
- **4-5 colors** is the practical maximum for categorical data
48-
- **6+ colors** is rare and should only be used when the data demands it (e.g., many categories)
49-
- When many categories exist, consider grouping or using a sequential colormap instead
58+
- **Sequential:** `viridis` or `cividis` (default). `Blues` / `Greens` for single-polarity data that should visually tie to the brand.
59+
- **Diverging:** `BrBG` from ColorBrewer (centered on a meaningful midpoint).
60+
- **Heatmaps:** `viridis` for neutral, or single-polarity `Reds` / `Blues` when polarity is semantic.
61+
- **Forbidden:** `jet`, `hsv`, rainbow colormaps — not perceptually uniform.
5062

51-
### Sequential & Diverging Data
63+
### Color Restraint
5264

53-
For continuous data, use perceptually-uniform colormaps:
54-
- **Sequential**: `viridis`, `plasma`, `inferno`, `cividis`, `Blues`, `Greens`
55-
- **Diverging**: `RdBu`, `PiYG`, `coolwarm` (centered on meaningful midpoint)
56-
- Avoid rainbow colormaps (`jet`, `hsv`) — they are not perceptually uniform
65+
- **2-3 colors** is ideal for most categorical plots.
66+
- **4-5 colors** is the practical maximum.
67+
- **6+ colors** is rare — prefer grouping or a sequential colormap instead.
5768

5869
### Colorblind Safety
5970

60-
All color choices must be distinguishable by people with deuteranopia and protanopia:
61-
- Avoid red-green as the only distinguishing feature
62-
- Use luminance differences (light vs dark), not just hue
63-
- When in doubt, test with a colorblind simulator
71+
The Okabe-Ito palette is already safe for deuteranopia and protanopia. Never override it with custom categorical hexes unless you have a documented reason.
6472

6573
---
6674

@@ -82,9 +90,46 @@ Every visual element must earn its place. If removing an element doesn't reduce
8290

8391
### Background
8492

85-
- Clean white (`#FFFFFF`) is the default
86-
- Very faint warm gray (`#FAFAFA`) is acceptable as an alternative
87-
- Never use colored or dark backgrounds
93+
anyplot plots live inside page surfaces that are warm off-white / near-black, **never pure white or pure black** (pure values make the saturated palette look harsh).
94+
95+
| Theme | Plot background (`bg-surface`) | Elevated (callout boxes, legend frames) |
96+
|-------|--------------------------------|----------------------------------------|
97+
| Light | `#FAF8F1` | `#FFFDF6` |
98+
| Dark | `#1A1A17` | `#242420` |
99+
100+
- The background is theme-dependent and must match the surface where the plot will be embedded on the website.
101+
- Implementations read `os.environ["ANYPLOT_THEME"]` (`"light"` or `"dark"`, default `"light"`) and render accordingly. The pipeline runs each implementation twice to produce `plot-light.png` and `plot-dark.png`.
102+
- Never use pure `#FFFFFF`, pure `#000000`, or unrelated colored backgrounds.
103+
104+
### Theme-adaptive Chrome
105+
106+
In addition to the background, every non-data element (title, axis labels, tick labels, spines, grid, legend text, annotations, callout-box fills, footnotes) must use theme-adaptive tokens. Only the Okabe-Ito data colors (positions 1–7) stay constant.
107+
108+
| Role | Light theme | Dark theme |
109+
|------|-------------|------------|
110+
| Primary text (title, axis labels) | `#1A1A17` | `#F0EFE8` |
111+
| Secondary text (tick labels, legend, subtitles) | `#4A4A44` | `#B8B7B0` |
112+
| Tertiary text (footnotes, meta annotations) | `#8A8A82` | `#6E6D66` |
113+
| Grid lines, rule dividers, thin borders | `rgba(26,26,23,0.10)` | `rgba(240,239,232,0.10)` |
114+
| Callout / legend box fill | `#FFFDF6` | `#242420` |
115+
116+
**Reference Python snippet** (generators must emit logic equivalent to this — exact syntax is library-specific):
117+
118+
```python
119+
import os
120+
THEME = os.getenv("ANYPLOT_THEME", "light")
121+
122+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
123+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
124+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
125+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
126+
INK_MUTED = "#8A8A82" if THEME == "light" else "#6E6D66"
127+
RULE = "rgba(26,26,23,0.10)" if THEME == "light" else "rgba(240,239,232,0.10)"
128+
129+
BRAND = "#009E73" # Okabe-Ito position 1, theme-independent
130+
```
131+
132+
Output file names: `plot-light.png` / `plot-dark.png` (static libraries) plus `plot-light.html` / `plot-dark.html` (interactive libraries).
88133

89134
### Whitespace
90135

@@ -160,8 +205,8 @@ DPI-based libraries use `figsize=(16, 9)` + `dpi=300` = 4800x2700px. Pixel-based
160205
The AI makes the following design decisions for each visualization:
161206

162207
**Color & Palette:**
163-
- Specific colors beyond Python Blue (for multi-series)
164-
- Colormap choice for sequential/diverging data
208+
- Use Okabe-Ito positions 1→N in order for categorical data (see Color Philosophy). No custom hexes.
209+
- Colormap choice for continuous data: `viridis`/`cividis` sequential, `BrBG` diverging, `viridis` or single-polarity `Reds`/`Blues` for heatmaps
165210
- Alpha/opacity values based on data density
166211

167212
**Layout & Structure:**
@@ -177,8 +222,8 @@ The AI makes the following design decisions for each visualization:
177222

178223
**Grid & Background:**
179224
- Grid on/off, and which axes
180-
- Grid opacity (within 15-25% range)
181-
- Background shade (white or faint gray)
225+
- Grid opacity (within 10-15% range — subtle, never competes with data)
226+
- Plot background follows the theme (`#FAF8F1` light / `#1A1A17` dark) — not chosen freely
182227

183228
**Data Presentation:**
184229
- Emphasis techniques (bold color, size variation, focal points)

prompts/library/altair.md

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,19 +71,60 @@ chart = chart.interactive()
7171

7272
## Colors
7373

74+
Use the Okabe-Ito palette (see `prompts/default-style-guide.md` "Categorical Palette"). First series is **always** `#009E73`.
75+
7476
```python
75-
# Single-series: always Python Blue
76-
alt.value('#306998')
77+
OKABE_ITO = ['#009E73', '#D55E00', '#0072B2', '#CC79A7',
78+
'#E69F00', '#56B4E9', '#F0E442']
79+
80+
# Single-series
81+
alt.Chart(df).mark_circle(color=OKABE_ITO[0]).encode(x='x', y='y')
7782

78-
# Multi-series: AI picks cohesive palette starting with Python Blue
79-
# No hardcoded second color — choose what works for the data
80-
alt.Scale(range=['#306998', ...]) # AI selects additional colors
83+
# Multi-series
84+
alt.Chart(df).mark_circle().encode(
85+
x='x', y='y',
86+
color=alt.Color('category:N', scale=alt.Scale(range=OKABE_ITO)),
87+
)
88+
89+
# Continuous — NOT Okabe-Ito:
90+
# Sequential: scheme='viridis' or 'cividis'
91+
# Diverging: scheme='brownbluegreen' (BrBG in altair naming)
92+
alt.Color('value:Q', scale=alt.Scale(scheme='viridis'))
93+
alt.Color('delta:Q', scale=alt.Scale(scheme='brownbluegreen'))
94+
```
95+
96+
## Theme-adaptive Chrome (altair mapping)
97+
98+
```python
99+
import os
100+
THEME = os.getenv("ANYPLOT_THEME", "light")
101+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
102+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
103+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
104+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
105+
106+
chart = (
107+
base_chart
108+
.properties(background=PAGE_BG, width=1600, height=900)
109+
.configure_view(fill=PAGE_BG, stroke=INK_SOFT)
110+
.configure_axis(
111+
domainColor=INK_SOFT, tickColor=INK_SOFT,
112+
gridColor=INK, gridOpacity=0.10,
113+
labelColor=INK_SOFT, titleColor=INK,
114+
)
115+
.configure_title(color=INK)
116+
.configure_legend(
117+
fillColor=ELEVATED_BG, strokeColor=INK_SOFT,
118+
labelColor=INK_SOFT, titleColor=INK,
119+
)
120+
)
81121

82-
# Colorblind-safe required. Avoid red-green as only distinguishing feature.
83-
# For sequential data: use perceptually-uniform colormaps (viridis, plasma, cividis)
122+
chart.save(f'plot-{THEME}.png')
123+
chart.save(f'plot-{THEME}.html')
84124
```
85125

86-
## Output File
126+
## Output Files
87127

88-
`plots/{spec-id}/implementations/altair.py`
128+
- Implementation: `plots/{spec-id}/implementations/altair.py` — executed twice with different `ANYPLOT_THEME`.
129+
- Generated artifacts: `plot-light.png` + `plot-dark.png` + `plot-light.html` + `plot-dark.html`.
89130

prompts/library/bokeh.md

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,19 +72,66 @@ p.line(..., line_width=3)
7272

7373
## Colors
7474

75+
Use the Okabe-Ito palette (see `prompts/default-style-guide.md` "Categorical Palette"). First series is **always** `#009E73`.
76+
7577
```python
76-
# Single-series: always Python Blue
77-
color = '#306998'
78+
OKABE_ITO = ['#009E73', '#D55E00', '#0072B2', '#CC79A7',
79+
'#E69F00', '#56B4E9', '#F0E442']
80+
81+
# Single-series
82+
p.scatter(x, y, color=OKABE_ITO[0])
83+
84+
# Multi-series: iterate in canonical order
85+
for i, group in enumerate(groups):
86+
p.scatter(..., color=OKABE_ITO[i], legend_label=group)
7887

79-
# Multi-series: AI picks cohesive palette starting with Python Blue
80-
# No hardcoded second color — choose what works for the data
81-
colors = ['#306998', ...] # AI selects additional colors
88+
# Continuous — NOT Okabe-Ito. Use bokeh's built-in palettes:
89+
from bokeh.palettes import Viridis256, Cividis256, BrBG11
90+
# Sequential: Viridis256, Cividis256
91+
# Diverging: BrBG11 (or BrBG9 for coarser binning)
92+
```
93+
94+
## Theme-adaptive Chrome (bokeh mapping)
8295

83-
# Colorblind-safe required. Avoid red-green as only distinguishing feature.
84-
# For sequential data: use perceptually-uniform colormaps (viridis, plasma, cividis)
96+
```python
97+
import os
98+
THEME = os.getenv("ANYPLOT_THEME", "light")
99+
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
100+
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
101+
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
102+
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
103+
104+
p.background_fill_color = PAGE_BG
105+
p.border_fill_color = PAGE_BG
106+
p.outline_line_color = INK_SOFT
107+
108+
p.title.text_color = INK
109+
p.xaxis.axis_label_text_color = INK
110+
p.yaxis.axis_label_text_color = INK
111+
p.xaxis.major_label_text_color = INK_SOFT
112+
p.yaxis.major_label_text_color = INK_SOFT
113+
p.xaxis.axis_line_color = INK_SOFT
114+
p.yaxis.axis_line_color = INK_SOFT
115+
p.xaxis.major_tick_line_color = INK_SOFT
116+
p.yaxis.major_tick_line_color = INK_SOFT
117+
118+
p.xgrid.grid_line_color = INK
119+
p.ygrid.grid_line_color = INK
120+
p.xgrid.grid_line_alpha = 0.10
121+
p.ygrid.grid_line_alpha = 0.10
122+
123+
if p.legend:
124+
p.legend.background_fill_color = ELEVATED_BG
125+
p.legend.border_line_color = INK_SOFT
126+
p.legend.label_text_color = INK_SOFT
127+
128+
from bokeh.io import export_png, output_file, save
129+
export_png(p, filename=f'plot-{THEME}.png')
130+
output_file(f'plot-{THEME}.html'); save(p)
85131
```
86132

87-
## Output File
133+
## Output Files
88134

89-
`plots/{spec-id}/implementations/bokeh.py`
135+
- Implementation: `plots/{spec-id}/implementations/bokeh.py` — executed twice with different `ANYPLOT_THEME`.
136+
- Generated artifacts: `plot-light.png` + `plot-dark.png` + `plot-light.html` + `plot-dark.html`.
90137

0 commit comments

Comments
 (0)