Skill Tracer
Skills › Scientific Agent Skills › scientific-visualization › color_palettes.md

color_palettes.md

227 lines · skills/scientific-visualization/references/color_palettes.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Color, Contrast, and Palette Selection
2
3Reviewed 2026-10-01. Source IDs resolve in `sources.md`. A named “colorblind-safe” palette is not a guarantee that a rendered figure is accessible: background, line thickness, adjacency, text, category count, display/print conversion, and redundant encoding all matter.
4

Color, Contrast, and Palette Selection

Reviewed 2026-10-01. Source IDs resolve in sources.md. A named “colorblind-safe” palette is not a guarantee that a rendered figure is accessible: background, line thickness, adjacency, text, category count, display/print conversion, and redundant encoding all matter.

Raw lines 1 to 4
5## Start with data semantics
6
7- **Qualitative:** unordered categories. Hue can separate groups; lightness should not imply an unintended ranking.
8- **Sequential:** ordered low-to-high values. Use monotonic perceived lightness.
9- **Diverging:** departure around a meaningful center. Use a neutral midpoint and document the normalization.
10- **Cyclic:** periodic values where endpoints meet.
12Do not use a diverging map merely because values contain positive and negative numbers; the center must have scientific meaning. Do not use a rainbow map as a generic ordered scale. Paul Tol documents false visual transitions, lack of inherent magnitude ordering, and color-vision problems in ordinary rainbow schemes [TOL].
14With Matplotlib, colormap and normalization are separate:
16```python
17import matplotlib as mpl
19norm = mpl.colors.TwoSlopeNorm(vmin=-2, vcenter=0, vmax=5)
20image = ax.imshow(values, cmap="RdBu_r", norm=norm)
21fig.colorbar(image, ax=ax, label="Change (unit)")
22```
24Use `LogNorm` for strictly positive orders of magnitude, `SymLogNorm` for signed data with a disclosed linear zone, `BoundaryNorm` for meaningful classes, and `TwoSlopeNorm` for unequal ranges around a center [MPL-NORM].

Start with data semantics

  • Qualitative: unordered categories. Hue can separate groups; lightness should not imply an unintended ranking.
  • Sequential: ordered low-to-high values. Use monotonic perceived lightness.
  • Diverging: departure around a meaningful center. Use a neutral midpoint and document the normalization.
  • Cyclic: periodic values where endpoints meet.

Do not use a diverging map merely because values contain positive and negative numbers; the center must have scientific meaning. Do not use a rainbow map as a generic ordered scale. Paul Tol documents false visual transitions, lack of inherent magnitude ordering, and color-vision problems in ordinary rainbow schemes [TOL].

With Matplotlib, colormap and normalization are separate:

import matplotlib as mpl

norm = mpl.colors.TwoSlopeNorm(vmin=-2, vcenter=0, vmax=5)
image = ax.imshow(values, cmap="RdBu_r", norm=norm)
fig.colorbar(image, ax=ax, label="Change (unit)")

Use LogNorm for strictly positive orders of magnitude, SymLogNorm for signed data with a disclosed linear zone, BoundaryNorm for meaningful classes, and TwoSlopeNorm for unequal ranges around a center [MPL-NORM].

Raw lines 5 to 25
26## Okabe-Ito / Wong colors
28The eight colors commonly reproduced from Wong’s Nature Methods article are [WONG]:
30```python
31OKABE_ITO = [
32 "#E69F00", # orange
33 "#56B4E9", # sky blue
34 "#009E73", # bluish green
35 "#F0E442", # yellow
36 "#0072B2", # blue
37 "#D55E00", # vermillion
38 "#CC79A7", # reddish purple
39 "#000000", # black
41```
43Several light colors do not reach 3:1 against white. For thin lines or required graphical objects on white, start with the bundled five-color subset:
45```python
46OKABE_ITO_ON_WHITE = [
47 "#0072B2",
48 "#D55E00",
49 "#009E73",
50 "#CC79A7",
51 "#000000",
53```
55This subset is derived by WCAG sRGB contrast calculation; it is not a published palette or a compliance certification. Use marker/line-style redundancy and audit the actual rendering.

Okabe-Ito / Wong colors

The eight colors commonly reproduced from Wong’s Nature Methods article are [WONG]:

OKABE_ITO = [
    "#E69F00",  # orange
    "#56B4E9",  # sky blue
    "#009E73",  # bluish green
    "#F0E442",  # yellow
    "#0072B2",  # blue
    "#D55E00",  # vermillion
    "#CC79A7",  # reddish purple
    "#000000",  # black
]

Several light colors do not reach 3:1 against white. For thin lines or required graphical objects on white, start with the bundled five-color subset:

OKABE_ITO_ON_WHITE = [
    "#0072B2",
    "#D55E00",
    "#009E73",
    "#CC79A7",
    "#000000",
]

This subset is derived by WCAG sRGB contrast calculation; it is not a published palette or a compliance certification. Use marker/line-style redundancy and audit the actual rendering.

Raw lines 26 to 56
57## Paul Tol qualitative schemes
59Paul Tol’s canonical site moved to `sronpersonalpages.nl` in July 2026. The current technical note remains SRON/EPS/TN/09-002, issue 3.2, dated 2021-08-18 [TOL] [TOL-HOME].
61The note states that the bright, high-contrast, vibrant, muted, and medium-contrast qualitative schemes are color-blind safe under its design/testing assumptions. It also states:
63- high-contrast is the strongest choice for grayscale/monochrome separation;
64- medium-contrast provides three pairs but weaker grayscale separation;
65- light is reasonably distinct and intended mainly for labeled cell fills;
66- pale/dark are not sufficiently distinct for multi-series lines or maps and are meant for text backgrounds/foregrounds;
67- most multi-color qualitative schemes do not remain fully separable in grayscale.
69Bundled fixed-order values:
71```python
72TOL_BRIGHT = [
73 "#4477AA", "#EE6677", "#228833", "#CCBB44",
74 "#66CCEE", "#AA3377", "#BBBBBB",
77TOL_HIGH_CONTRAST = ["#004488", "#DDAA33", "#BB5566"]
79TOL_VIBRANT = [
80 "#EE7733", "#0077BB", "#33BBEE", "#EE3377",
81 "#CC3311", "#009988", "#BBBBBB",
84TOL_MUTED = [
85 "#CC6677", "#332288", "#DDCC77", "#117733", "#88CCEE",
86 "#882255", "#44AA99", "#999933", "#AA4499",
89TOL_MEDIUM_CONTRAST = [
90 "#6699CC", "#004488", "#EECC66",
91 "#994455", "#997700", "#EE99AA",
93```
95The maximum intended series counts are 7, 3, 7, 9, and 6 respectively. Do not interpolate qualitative palettes.

Paul Tol qualitative schemes

Paul Tol’s canonical site moved to sronpersonalpages.nl in July 2026. The current technical note remains SRON/EPS/TN/09-002, issue 3.2, dated 2021-08-18 [TOL] [TOL-HOME].

The note states that the bright, high-contrast, vibrant, muted, and medium-contrast qualitative schemes are color-blind safe under its design/testing assumptions. It also states:

  • high-contrast is the strongest choice for grayscale/monochrome separation;
  • medium-contrast provides three pairs but weaker grayscale separation;
  • light is reasonably distinct and intended mainly for labeled cell fills;
  • pale/dark are not sufficiently distinct for multi-series lines or maps and are meant for text backgrounds/foregrounds;
  • most multi-color qualitative schemes do not remain fully separable in grayscale.

Bundled fixed-order values:

TOL_BRIGHT = [
    "#4477AA", "#EE6677", "#228833", "#CCBB44",
    "#66CCEE", "#AA3377", "#BBBBBB",
]

TOL_HIGH_CONTRAST = ["#004488", "#DDAA33", "#BB5566"]

TOL_VIBRANT = [
    "#EE7733", "#0077BB", "#33BBEE", "#EE3377",
    "#CC3311", "#009988", "#BBBBBB",
]

TOL_MUTED = [
    "#CC6677", "#332288", "#DDCC77", "#117733", "#88CCEE",
    "#882255", "#44AA99", "#999933", "#AA4499",
]

TOL_MEDIUM_CONTRAST = [
    "#6699CC", "#004488", "#EECC66",
    "#994455", "#997700", "#EE99AA",
]

The maximum intended series counts are 7, 3, 7, 9, and 6 respectively. Do not interpolate qualitative palettes.

Raw lines 57 to 96
97## ColorBrewer
99ColorBrewer 2.0 is an authoritative interactive resource for sequential, diverging, and qualitative cartographic schemes. It allows filtering by “colorblind safe,” “print friendly,” and “photocopy safe,” and exposes the supported number of data classes [COLORBREWER].
101Use the exact class count shown by ColorBrewer. A scheme marked safe at one class count may not be marked safe at another. ColorBrewer’s flags are design guidance for its intended mapping context, not a WCAG conformance result for arbitrary line widths, backgrounds, or text.
103Matplotlib exposes many ColorBrewer-derived maps. Verify the exact map direction and class count rather than relying on its name.

ColorBrewer

ColorBrewer 2.0 is an authoritative interactive resource for sequential, diverging, and qualitative cartographic schemes. It allows filtering by “colorblind safe,” “print friendly,” and “photocopy safe,” and exposes the supported number of data classes [COLORBREWER].

Use the exact class count shown by ColorBrewer. A scheme marked safe at one class count may not be marked safe at another. ColorBrewer’s flags are design guidance for its intended mapping context, not a WCAG conformance result for arbitrary line widths, backgrounds, or text.

Matplotlib exposes many ColorBrewer-derived maps. Verify the exact map direction and class count rather than relying on its name.

Raw lines 97 to 104
105## Perceptually uniform continuous maps
107Matplotlib recommends selecting maps based on data semantics and discusses lightness behavior in its colormap guide [MPL-CMAP]. Common continuous candidates:
109- `viridis`, `plasma`, `inferno`, `magma`: perceptually uniform sequential families;
110- `cividis`: designed with color-vision deficiencies in mind;
111- `RdBu_r`, `PuOr`, `BrBG`: possible diverging candidates after checking the center, direction, contrast, and grayscale behavior.
113No list is universally safe. A map can be perceptually uniform yet still fail to distinguish a narrow feature at the chosen size, or lose detail during RGB-to-CMYK conversion.

Perceptually uniform continuous maps

Matplotlib recommends selecting maps based on data semantics and discusses lightness behavior in its colormap guide [MPL-CMAP]. Common continuous candidates:

  • viridis, plasma, inferno, magma: perceptually uniform sequential families;
  • cividis: designed with color-vision deficiencies in mind;
  • RdBu_r, PuOr, BrBG: possible diverging candidates after checking the center, direction, contrast, and grayscale behavior.

No list is universally safe. A map can be perceptually uniform yet still fail to distinguish a narrow feature at the chosen size, or lose detail during RGB-to-CMYK conversion.

Raw lines 105 to 114
115## WCAG contrast: what to test
117WCAG 2.2 is normative for web content [WCAG22]:
119- normal text: 4.5:1 against its background (SC 1.4.3 AA);
120- large text: 3:1 (SC 1.4.3 AA);
121- graphical objects required to understand content: 3:1 against adjacent colors (SC 1.4.11 AA);
122- color must not be the only visual means of conveying information (SC 1.4.1 A).
124W3C’s understanding document uses line and pie charts as examples and explains that required graphical objects are tested against adjacent colors; all data-series colors do not automatically need 3:1 against each other when they do not overlap [WCAG-NONTEXT].
126Run:
128```bash
129uv run --isolated --no-project --python 3.13 \
130 python scripts/palette_audit.py \
131 --palette okabe_ito_on_white \
132 --background FFFFFF \
133 --role graphical
134```
136The CLI reports:
138- exact WCAG sRGB contrast against the chosen background;
139- pairwise contrast;
140- pairwise CIE L* separation after removing hue.
142Its default grayscale threshold (ΔL* 10) is a heuristic. It is not a WCAG threshold and does not simulate every color-vision deficiency, printer, profile, or viewing condition.

WCAG contrast: what to test

WCAG 2.2 is normative for web content [WCAG22]:

  • normal text: 4.5:1 against its background (SC 1.4.3 AA);
  • large text: 3:1 (SC 1.4.3 AA);
  • graphical objects required to understand content: 3:1 against adjacent colors (SC 1.4.11 AA);
  • color must not be the only visual means of conveying information (SC 1.4.1 A).

W3C’s understanding document uses line and pie charts as examples and explains that required graphical objects are tested against adjacent colors; all data-series colors do not automatically need 3:1 against each other when they do not overlap [WCAG-NONTEXT].

Run:

uv run --isolated --no-project --python 3.13 \
  python scripts/palette_audit.py \
  --palette okabe_ito_on_white \
  --background FFFFFF \
  --role graphical

The CLI reports:

  • exact WCAG sRGB contrast against the chosen background;
  • pairwise contrast;
  • pairwise CIE L* separation after removing hue.

Its default grayscale threshold (ΔL* 10) is a heuristic. It is not a WCAG threshold and does not simulate every color-vision deficiency, printer, profile, or viewing condition.

Raw lines 115 to 143
Runs code python scripts/palette_audit.py :130
144## Redundant encoding
146Use at least one non-color cue:
148```python
149colors = ["#0072B2", "#D55E00", "#009E73"]
150linestyles = ["-", "--", "-."]
151markers = ["o", "s", "^"]
153for index, series in enumerate(series_list):
154 ax.plot(
155 x,
156 series,
157 color=colors[index],
158 linestyle=linestyles[index],
159 marker=markers[index],
160 markevery=5,
161 label=labels[index],
162 )
163```
165For bars, use edge contrast, labels, and restrained hatching. For images, consider accessible channel combinations plus separate grayscale panels. Direct labels often outperform distant legends.

Redundant encoding

Use at least one non-color cue:

colors = ["#0072B2", "#D55E00", "#009E73"]
linestyles = ["-", "--", "-."]
markers = ["o", "s", "^"]

for index, series in enumerate(series_list):
    ax.plot(
        x,
        series,
        color=colors[index],
        linestyle=linestyles[index],
        marker=markers[index],
        markevery=5,
        label=labels[index],
    )

For bars, use edge contrast, labels, and restrained hatching. For images, consider accessible channel combinations plus separate grayscale panels. Direct labels often outperform distant legends.

Raw lines 144 to 166
167## Missing and out-of-range colors
169Assign explicit colors to missing and out-of-range values:
171```python
172import matplotlib as mpl
174cmap = mpl.colormaps["viridis"].with_extremes(
175 bad="#777777",
176 under="#222222",
177 over="#FDE725",
179```
181Label these states in the colorbar/legend. Never let missing values default to the low end of a quantitative scale.

Missing and out-of-range colors

Assign explicit colors to missing and out-of-range values:

import matplotlib as mpl

cmap = mpl.colormaps["viridis"].with_extremes(
    bad="#777777",
    under="#222222",
    over="#FDE725",
)

Label these states in the colorbar/legend. Never let missing values default to the low end of a quantitative scale.

Raw lines 167 to 182
183## Seaborn and Plotly
185Seaborn 0.13.2 accepts palette names, lists, and dictionaries through `palette=`/`set_palette()`. Apply a Matplotlib style before Seaborn only if the subsequent `sns.set_theme()` call will not overwrite the intended rc settings; pass `rc=` explicitly when needed [SEABORN-PALETTE] [SEABORN-THEME].
187```python
188import seaborn as sns
190sns.set_theme(style="ticks", context="paper")
191sns.set_palette(OKABE_ITO_ON_WHITE)
192```
194For Plotly, set categorical colors explicitly and add symbols/dashes:
196```python
197fig = px.scatter(
198 frame,
199 x="x",
200 y="y",
201 color="group",
202 symbol="group",
203 color_discrete_sequence=OKABE_ITO_ON_WHITE,
205```
207Check the static export too. Interactive hover does not replace contrast, labels, keyboard operation, alt text, or a static fallback.

Seaborn and Plotly

Seaborn 0.13.2 accepts palette names, lists, and dictionaries through palette=/set_palette(). Apply a Matplotlib style before Seaborn only if the subsequent sns.set_theme() call will not overwrite the intended rc settings; pass rc= explicitly when needed [SEABORN-PALETTE] [SEABORN-THEME].

import seaborn as sns

sns.set_theme(style="ticks", context="paper")
sns.set_palette(OKABE_ITO_ON_WHITE)

For Plotly, set categorical colors explicitly and add symbols/dashes:

fig = px.scatter(
    frame,
    x="x",
    y="y",
    color="group",
    symbol="group",
    color_discrete_sequence=OKABE_ITO_ON_WHITE,
)

Check the static export too. Interactive hover does not replace contrast, labels, keyboard operation, alt text, or a static fallback.

Raw lines 183 to 208
209## Color management
211- Work in sRGB unless the publisher or calibrated workflow requires another space.
212- Preserve ICC profiles in scientific raster images when relevant.
213- Preview print conversion when the publisher converts RGB to CMYK.
214- Do not alter raw intensity data merely to obtain attractive colors.
215- Record channel mappings, color limits, normalization, and any color-space conversion.
216- Avoid transparency when blending with an unknown background can change contrast.

Color management

  • Work in sRGB unless the publisher or calibrated workflow requires another space.
  • Preserve ICC profiles in scientific raster images when relevant.
  • Preview print conversion when the publisher converts RGB to CMYK.
  • Do not alter raw intensity data merely to obtain attractive colors.
  • Record channel mappings, color limits, normalization, and any color-space conversion.
  • Avoid transparency when blending with an unknown background can change contrast.
Raw lines 209 to 217
218## Review checklist
220- [ ] Palette type matches data semantics.
221- [ ] Center, limits, normalization, and missing-value color are explicit.
222- [ ] Foreground/background contrast was audited at final size.
223- [ ] Color is redundant with shape, line style, hatching, labels, or layout.
224- [ ] Grayscale was inspected without treating it as a complete accessibility test.
225- [ ] Print/profile conversion was reviewed when relevant.
226- [ ] Palette and category mapping are consistent across figures.
227- [ ] Underlying values and an accessible text/table alternative are available.

Review checklist

  • [ ] Palette type matches data semantics.
  • [ ] Center, limits, normalization, and missing-value color are explicit.
  • [ ] Foreground/background contrast was audited at final size.
  • [ ] Color is redundant with shape, line style, hatching, labels, or layout.
  • [ ] Grayscale was inspected without treating it as a complete accessibility test.
  • [ ] Print/profile conversion was reviewed when relevant.
  • [ ] Palette and category mapping are consistent across figures.
  • [ ] Underlying values and an accessible text/table alternative are available.
Raw lines 218 to 227