Writing the paper
Can every reader tell your figure colours apart?
Build a palette from one colour and test it, in Python or R.
Figure 2 has six groups, coloured from the plotting defaults. About 1 in 12 men and 1 in 200 women have a colour-vision deficiency, so at least one likely reader of your paper cannot tell some of those six apart. The greyscale print version loses a few more. You can test this before submission, in two minutes, from one colour of your choice.
The problemPlotting defaults are chosen for looks, not for readers
This is not carelessness. Nothing in a normal figure workflow checks whether two colours stay distinguishable for a reader with deuteranopia, or after the journal’s PDF is printed in greyscale. The defaults in matplotlib and ggplot2 are tuned to look pleasant on a screen, and a palette tool that hands you eight hex codes tells you nothing about what happens to them in other eyes. Crameri, Shephard and Heron showed in Nature Communications (2020) how widespread the damage is: rainbow maps and unchecked colour choices distort published data across whole fields.
A palette without its audit is like a lab value without its reference range. The number may be fine; you cannot tell by looking.
Common mistakesFive ways figure colour goes wrong
- Red paired with green. The most common deficiency is red-green. If your two most important groups are red and green, the reader most likely to miss the difference is 1 in 12.
- The rainbow map. “Jet” and friends create bright bands that make some values look more important than the data say, and they collapse under colour-vision deficiency.
- Colour as the only encoding. If groups differ only by colour, one reader’s deficiency erases your finding. Shapes, line styles, and direct labels cost nothing.
- Palettes that die in print. Two colours can be clearly different in hue and identical in lightness; greyscale keeps only the lightness.
- More categories than colours can carry. Past about eight, no palette keeps every pair apart. Merge groups or use small multiples.
The methodWhat makes a palette scientific
The fix is a short list of properties you can test, from the same paper. A palette for ordered data should take even perceptual steps (measured as colour distance, ΔE, in a perceptual colour space, not as equal jumps in RGB), and its lightness should run in one direction so the order survives printing. Any palette should keep its colours apart under simulated protanopia, deuteranopia and tritanopia (the three main colour-vision deficiencies), and keep enough contrast against the background for its job. Categorical palettes for unordered groups are the opposite case: lightness should stay roughly level, because a light-to-dark run falsely implies an order.
The free shortcut is to use an established palette that already has these properties: Okabe-Ito for up to eight categories, viridis or batlow for sequential data. Our figure-requirements guide has Okabe-Ito ready to paste. If those cover your case, you are done, and you never need a generator.
The gap appears when you want your own colours: a lab colour scheme, a university template, a journal style, or simply a figure family that matches the rest of the paper. Then the five checks above still apply, and doing them by hand is the step everyone skips.
Create and save your ownOne colour in, an audited palette out
We built a small open-source tool for exactly this step: palettecore (free, Apache-2.0, runs locally in Python or R). You give it one anchor colour and a palette kind, and it returns the colours together with the audit: colour distances under normal vision and all three simulated deficiencies, the greyscale result, gamut and background contrast, and a warning for every check that misses its threshold. The same algorithm runs in both languages and produces identical hex codes, so a Python collaborator and an R collaborator can share one palette spec.
Python: create, check, save
pip install palettecore
from palettecore import generate_palette
r = generate_palette("#8B6FC9", n=8, kind="categorical",
background="#FFFFFF", use="data_fill")
print(r.hexes) # your palette
print(r.warnings) # what to fix, if anything
with open("palette.css", "w") as f: # save for reuse
f.write(r.to_css())
with open("palette.json", "w") as f: # palette + full audit
f.write(r.to_json())
R: create, check, save
install.packages("palettecore",
repos = "https://heidihelena.r-universe.dev")
library(palettecore)
r <- generate_palette("#8B6FC9", n = 8, kind = "categorical")
r$hexes
writeLines(r$hexes, "palette.txt")
# or straight into a plot; audit warnings surface, never vanish
ggplot(df, aes(x, y, colour = group)) +
geom_point() +
scale_colour_accessible("#8B6FC9")
Use kind = "sequential" for ordered data (low to high), "diverging" for data with a meaningful centre, and "categorical" for unordered groups. Declare the background and the use (data_fill, text, line), because a colour that works as an area fill can fail as text.
Working with an AI agent? Paste this prompt
If Claude Code, Cursor, or another coding agent writes your figures, hand it this and fill in the blanks. The prompt makes the agent keep the audit honest instead of hiding it.
Build me a colour palette for my figures with the open-source
palettecore package (Python: pip install palettecore · R:
install.packages("palettecore",
repos = "https://heidihelena.r-universe.dev")).
My anchor colour: #______
Number of colours: ____
My data is: [ordered low-to-high / has a meaningful centre /
unordered groups] -> kind = sequential / diverging / categorical
(ask me if you cannot tell from my data)
Background: #FFFFFF Use: data_fill (or text / line)
Then:
1. Run it (CLI: python3 -m palettecore "#..." -n 8
--kind ... --format json) and show me the hex codes AND every
warning from the audit, unchanged.
2. If a warning appears, give me the trade-offs (fewer colours,
shapes or direct labels alongside colour, or a threshold change
as my recorded decision). Never silently change thresholds or
drop a warning.
3. Save palette.css and palette.json next to my analysis code, and
record the exact call so the palette is reproducible.
Full agent instructions:
https://github.com/heidihelena/palettecore/blob/main/skill/SKILL.md
Worked exampleOur own brand colour, audited
We ran the tool on Vahtian’s violet, #8B6FC9. The categorical palette of eight passed every check: the smallest distance between any two colours is ΔE 13.8 in normal vision, and it stays at 8.0 or above under all three simulated deficiencies.
The sequential palette did not pass clean. Seven of its checks were comfortable, and one was not: under simulated tritanopia the two palest steps sit at ΔE 5.7, just under the tool’s threshold of 6. So the report says exactly that, and the options are the standard ones: use fewer steps, accept the near miss at the pale end, or add labels. The first palette we generated from our own colour earned a warning, which is the tool doing its job.
Five checks that travel with any palette
- Even perceptual steps (ΔE), not equal RGB jumps.
- Lightness runs one way for ordered data; stays level for categories.
- Colours stay apart under simulated protanopia, deuteranopia, tritanopia.
- The order survives greyscale printing.
- Enough contrast against the background for the declared use.
Related guidesRead next
palettecore checks a palette against stated thresholds under simulated complete colour-vision deficiency, and the milder, more common partial deficiencies are outside its model. It is free and open source: github.com/heidihelena/palettecore, alongside our other open-source methods and software.
Further readingSources worth your time
- Crameri, Shephard & Heron (2020), The misuse of colour in science communication. Nature Communications (open access).
- Machado, Oliveira & Fernandes (2009), A physiologically-based model for simulation of color vision deficiency. IEEE TVCG.