color_palettes.md
227 lines · skills/scientific-visualization/references/color_palettes.md at 5f2bc03
Most marks come from matching words in the file, so treat them as pointers.
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.
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].
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 56Paul 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 96ColorBrewer
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 104Perceptually 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 114WCAG 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 graphicalThe 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 143Redundant 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 166Missing 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 182Seaborn 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 208Color 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.
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.