Simulate Colorblindness in R to Test Your Figures

See your plot the way a colorblind reader does — with colorspace

Data Visualization

Learn to simulate colorblindness in R with the colorspace package — transform a ggplot2 figure to show how it looks under deuteranopia, protanopia, and tritanopia, so you can check your colours are accessible before you publish, then fix them with a colourblind-safe palette. Every figure is rendered from real, copy-paste R.

Author
Published

July 8, 2026

Modified

July 9, 2026

TipWhat you’ll learn
  • The three functions that do the work — deutan(), protan(), and tritan() from the colorspace package — and what each type of colour-vision deficiency is.
  • How to simulate a whole ggplot2 figure under each deficiency, so you can see the problem instead of guessing.
  • Why the default ggplot2 palette fails the red-green test — and the one-line viridis fix that passes.
  • No colorblindr needed: colorspace is on CRAN and does everything here.

You picked colours that look great on your screen. But as many as 1 in 12 men (about 8%) and 1 in 200 women have a red-green colour-vision deficiency (NEI) — so a meaningful slice of your readers may not see the distinction your figure depends on. The fix is not to guess: simulate how your plot looks to a colorblind reader, before you publish, and adjust if groups collapse together.

This post does exactly that with the colorspace package (on CRAN — no development packages, no colorblindr). We’ll simulate a ggplot2 figure under all three deficiencies, spot where the default palette breaks, and fix it.

The three functions you need

colorspace ships three functions that take any vector of colours and return what a person with that deficiency perceives:

  • deutan()deuteranopia, the most common form (reduced green sensitivity; red-green confusion).
  • protan()protanopia (reduced red sensitivity; also red-green confusion).
  • tritan()tritanopia, rare (reduced blue sensitivity; blue-yellow confusion).

Start with the palette ggplot2 hands you by default. For three groups it’s a red, green, and blue trio. Pass those colours through each simulation function to see how they transform:

library(colorspace)

pal <- scales::hue_pal()(3)   # ggplot2's default palette for 3 groups
pal
[1] "#F8766D" "#00BA38" "#619CFF"
deutan(pal)                   # how a deuteranope perceives them
[1] "#B6A86A" "#AC9C45" "#5A96FD"

The hex codes change, but numbers are hard to read. Use colorspace::swatchplot() to look at each palette side by side — normal vision on top, then each deficiency below:

Four rows of colour swatches — normal vision, deuteranopia, protanopia, tritanopia — for the default ggplot2 red, green, blue palette; under deuteranopia and protanopia the red and green swatches drift toward the same muddy tone.

swatchplot(
  "Normal vision" = pal,
  "Deuteranopia"  = deutan(pal),
  "Protanopia"    = protan(pal),
  "Tritanopia"    = tritan(pal)
)

Read down each column: under deuteranopia and protanopia the red and green swatches drift toward the same muddy yellow-brown — a red-green reader can barely tell them apart — while the blue stays distinct. Under tritanopia it’s the blue that shifts. That is the exact problem you want to catch before it reaches a figure.

Simulate a whole ggplot2 figure

Swatches are the diagnosis; the real test is your actual plot. The trick without colorblindr is simple: redraw the figure with each group’s fill replaced by its simulated colour. Whatever the deuteranope perceives, we paint literally — so the faceted result is the simulation.

First, a small helper. It takes a palette, computes the deutan/protan/tritan versions, and stacks four copies of the data — one per vision type — with a fill_hex column holding the already-simulated colour for each row:

library(ggplot2)
library(colorspace)

cvd_data <- function(pal) {
  names(pal) <- levels(iris$Species)
  sims <- list(
    "Normal vision" = pal,
    "Deuteranopia"  = deutan(pal),
    "Protanopia"    = protan(pal),
    "Tritanopia"    = tritan(pal)
  )
  do.call(rbind, lapply(names(sims), function(v) {
    d <- iris
    d$vision   <- factor(v, levels = names(sims))
    d$fill_hex <- sims[[v]][as.character(d$Species)]
    d
  }))
}

Now plot it. The key is scale_fill_identity(): we already computed the exact hex per row, so ggplot2 should use those colours verbatim rather than mapping them through a scale. Facet by vision and you get one panel per deficiency:

cvd_facets <- function(pal, title) {
  ggplot(cvd_data(pal), aes(Species, Sepal.Length, fill = fill_hex)) +
    geom_boxplot(colour = "grey30", outlier.size = 0.6) +
    scale_fill_identity() +
    facet_wrap(~ vision, nrow = 1) +
    labs(title = title, x = NULL, y = "Sepal length (cm)") +
    theme_minimal(base_size = 12) +
    theme(legend.position = "none")
}

Run it on the default palette and read the four panels:

cvd_facets(pal, "Default ggplot2 palette")

An iris Sepal.Length boxplot coloured by species with the default ggplot2 palette, faceted into normal vision, deuteranopia, protanopia and tritanopia; under deuteranopia and protanopia the setosa and versicolor boxes take on nearly the same muddy tone.

In the Normal vision panel the three species are clearly red, green, blue. Slide right to Deuteranopia and Protanopia and two of the three boxes collapse toward the same dull tone — a red-green reader now has to fall back on box position to tell the groups apart, which defeats the point of colouring by group. The default palette fails the test.

The fix: a colourblind-safe palette

The reliable fix is a palette engineered to stay distinguishable across deficiencies. viridis (bundled with ggplot2) is the go-to: it varies colours by lightness as well as hue, so groups stay separable even when hue collapses. Swap one scale line:

ggplot(iris, aes(Species, Sepal.Length, fill = Species)) +
  geom_boxplot(colour = "grey30", outlier.size = 0.6) +
  scale_fill_viridis_d() +
  labs(x = NULL, y = "Sepal length (cm)") +
  theme_minimal(base_size = 12) +
  theme(legend.position = "none")

An iris Sepal.Length boxplot coloured by species using the viridis discrete palette — dark purple, teal, and yellow — under normal vision.

Now put viridis through the same simulation gauntlet — reuse the helper, feeding it the viridis colours instead:

pal_viridis <- substr(viridisLite::viridis(3), 1, 7)   # drop the alpha byte
cvd_facets(pal_viridis, "Viridis palette")

An iris boxplot in the viridis palette faceted into normal vision, deuteranopia, protanopia and tritanopia; the dark-purple, teal and yellow boxes stay clearly distinct in every panel because they differ in lightness.

Across all four panels the three boxes stay distinct — dark, medium, light — because viridis separates them by lightness, not just hue. That difference survives every deficiency. This is why viridis (and its jco/npg journal-palette cousins) is the safe default for grouped figures.

Note

Colour is a helpful cue, not the only cue. For print, small multiples, or maximum robustness, add a second channel — distinct shapes, line types, direct labels, or facets — so the figure still reads in greyscale.

A three-step workflow

Bake the check into how you finish a figure:

  1. Draw the figure with your intended palette.
  2. Simulate it — deutan(), protan(), tritan() on the palette (or the faceted redraw above) — and look for groups that collapse.
  3. Iterate the colours until every group stays distinguishable in all conditions; reach for viridis or a journal palette when in doubt.

Frequently asked questions

They are the three dichromacy types, each from a missing cone. Deuteranopia (missing green cone) and protanopia (missing red cone) both cause red-green confusion and are by far the most common. Tritanopia (missing blue cone) causes blue-yellow confusion and is rare. In colorspace they map to deutan(), protan(), and tritan().

Roughly 8% of men (about 1 in 12) and 0.5% of women have some form of red-green colour-vision deficiency (National Eye Institute). At that rate almost any figure with a general audience has colorblind readers, which is why simulating before you publish is worth the two extra lines.

No. The older colorblindr package is not on CRAN and depends on development versions of other packages. colorspace is on CRAN and provides deutan(), protan(), and tritan() — everything in this post uses only colorspace plus ggplot2.

Largely, yes. viridis varies lightness monotonically as well as hue, so categories stay separable even when hue perception collapses — as the simulation panels above show. It is not magic for fine categorical distinctions, so still simulate your specific figure, and add a non-colour cue (shape, label, facet) for the highest-stakes plots.

colorspace’s deutan()/protan()/tritan() operate on colour values, so the cleanest path in R is to simulate the palette and redraw the figure, as shown here. To transform an existing raster image, read its pixels (e.g. with the magick package) and pass the pixel colours through the same functions.

Going deeper in /learn

This post is the focused recipe. For the full lessons on choosing and applying colours in ggplot2 — scales, manual palettes, and named colours — see:

Citation

BibTeX citation:
@online{kassambara2026,
  author = {Kassambara, Alboukadel},
  title = {Simulate {Colorblindness} in {R} to {Test} {Your} {Figures}},
  date = {2026-07-08},
  url = {https://www.datanovia.com/blog/simulate-colorblindness-in-r},
  langid = {en}
}
For attribution, please cite this work as:
Kassambara, Alboukadel. 2026. “Simulate Colorblindness in R to Test Your Figures.” July 8. https://www.datanovia.com/blog/simulate-colorblindness-in-r.