I wrote some code that automatically checks visualizations for non-colorblind safe colors. Here's how it works

Earlier this week we released a new feature at Datawrapper that checks colors used in charts or maps for problems with colorblind readers. If problematic colors are found, a warning sign is shown that leads to a colorblind simulation view that helps our users find better colors.

This blog post explains how the code works. Perhaps this can open a discussion for future improvement. I also hope that other tools will follow Darawrapper on this path towards more automated accessibility testing.

To see how the algorithm work we’ll look at an example map showing population growth in the US between 2000 and 2010, based on this map made by Matt Stiles in 2011:

So here’s what we’re going to do

  1. We need to extract a representative color sample
  2. Then look for color-pairs that are distinguishable under normal vision and
  3. check if any of them turn indistinguishable in a color blindness simulated view
  4. Finally we need to decide whether or not to show a warning.

# Extracting a representative color sample

Now, to check if this map is colorblind-friendly or not, we need to look at the colors used in it. In some graphics there might be just a handful of colors, but since this is a continuous US county choropleth map, we have a lot of them (TK, to be exact).

Since the next step will involve checking each combination of these colors, it would result in a lot of permutations (TK, to be exact). So in order to save some work we need to reduce the number of colors.

My first approach was to use random sampling. To illustrate this, let’s look at 20 random colors from the palette above:

This might look like a nice sample at first glance, but due to the is random sampling we might end up with more colors from either spectrum of the palette. Click the button below to try a few more samples.

To get around this problem I tried to find a more deterministic sampling method that gives us a representative collection of colors. Since color blindness affects hue perception I thought it made sense to get a good sample of the entire hue-spectrum. So I sorted all colors by hue…

…and then pick 20 evenly spaced colors along this spectrum. Like taking a sample every 5th percentile. To deal with interpolation (in case a percentile falls exactly between two colors) I used the scale.colors() method in chroma.js.

1
var sample = chroma.scale(sorted_colors).colors(20);

And this is what the sample looks like:

We could experiment with other sampling methods, but for now these look good enough, so let’s move on. Next we want to find out which of the resulting 190 color pairs are actually distinguishable from another. To do that we need to find a method to compute differences between colors.

# How to compute color differences

There are a couple of ways to do this. For instance, a color can be represented as three-dimensional coordinates in a R-G-B color space, so the color difference can be defined as the Euclidean distance between the two points.

distRGB=(R2R1)2+(G2G1)2+(B2B1)2dist_{RGB}=\sqrt{(R_2-R_1)^{2} + (G_2-G_1)^{2} + (B_2-B_1)^{2}}

However, after playing around with RGB distances a bit I noticed a problem: Look at the yellow/green and pink/blue combinations below. From eyeballing the colors I had expected the distance between yellow and green to be smaller than the pink/blue combination.

vs

But it turns out, in RGB the distance is exactly 255 in both cases. This makes sense mathematically, since it takes 255 “steps” from (255,255,0) to (0,255,0) as well as from (255,0,255) to (0,0,255). But it doesn’t make sense visually.

Of course, the same Euclidean distances can be computed in any other color space, so perhaps a perceptual color space like Lab or Lch makes more sense. Sadly, that’s not the case.

RGBLchLabdeltaE
25541.466.326.9
25540.058.034.7

In Lch, like RGB, both color pairs are almost equally distant. In Lab, the distance between yellow and green is even larger (66.3) than pink/blue (58.0).

So I ended up using a more fancy algorithm called deltaE or CMC l:c. It’s based on the Lch color model, but appears to work better.

One minor problem with deltaE is that it’s not symmetrical, meaning that the difference between yellow and green differs slightly from the difference between green and yellow. To get around this problem I am using the mean of the distances in both direction:

dist(c1,c2)=deltaE(c1,c2)+deltaE(c2,c1)2dist(c_{1},c_{2})=\frac{deltaE(c_{1}, c_{2}) + deltaE(c_{2}, c_{1})}{2}

Now let’s move from the difference between two colors to the difference between all the colors!

# Compute the color distance matrix

To do that we compute the color distance matrix. Which is a fancy word for a table with rows and columns for each of our sample colors, and the distance between each color combination shown in the table cells.

We only need to look at the upper half of the matrix – the lower half is just an exact mirror. I highlighted all values above 45 so the combinations with the largest color differences pop out a bit more.

Now we need to compute the same matrix with the colorblind-simulated versions of our colors.

# Simulating color blindness

Color blindness simulation is done by mapping colors from RGB to a reduced color space. It means that you have a function that gets one color as input and returns a new color.

After googling around a bit I settled on a NPM package color-blind, which provides a fairly simple API:

1
2
var blinder = require('color-blind');
blinder.protanopia('#42dead'); // result: "#d1c4a0"

Here is our color sample mapped through three kinds of color blindness:

Now we can just repeat the color difference matrix for the converted sample colors:

You can click through the buttons below to see the color difference matrix for each color blindness simulation:

# Computing difference ratios between normal and colorblind vision

We’re getting closer to the finish line! The only thing that’s left to do now is to compare the differences under normal vision with the differences under a color blindness simulation.

One way to do this is to look at the ratio between the two differences:

ratio(c1,c2)=distnormal(c1,c2)distcolorblind(c1,c2)ratio(c_1,c_2)=\frac{dist_{normal}(c_1,c_2)}{dist_{colorblind}(c_1,c_2)}

To illustrate the ratio, let’s look at the notorious green/red color pair and compute the differences between them after applying different colorblindness simulations.

A ratio of 17.3 means that a color pair is seventeen times more differentiable under normal vision than under this type of color blindness. The higher the ratio the more information is “lost” for a colorblind person. This is what we’ll use to decide what colors are ok or not.

Now, to look at all the ratios at once, let’s make another matrix. The table looks just like the matrices we’ve seen before, except now the values in the cells show the ratio between the normal distance and the colorblind distance for each color pair.

You can use the buttons to cycle through the different simulations.

All that’s left to do now is to decide when to show a warning.

# When to show a color warning

One criteria to check for I came up with is to check how many of the color pairs that were differentiable under normal vision turn into non-differentiable color pairs under colorblind vision. I also found that difference ratios above five signal a significant loss of information. So I ended up using a mix of both to decide when to trigger warnings.

As you can see in the table, it all depends highly on where I set the thresholds. You can tweak the sliders below to change them and see the results changing.

Feel free to scroll back to the top and try out different colors in the map. All the examples and matrices in this post will change accordingly. Let me know if you find bugs along the way.

# Some ideas for future improvements

This algorithm was hacked together in a few days, so obviously there are possible improvements to be made. Here are a few ideas that might be worth exploring:

  • Instead of the hue-percentiles we could try different methods to find the most “representative” colors, such as k-means clustering.
  • One could look out for different color difference metrics, ideally by having (non-colorblind) people “guess” color differences and compare these results with the various color difference formulas.
  • Maybe there are smarter ways to compare the color differences between normal and colorblind vision than looking at the ratios?
  • Maybe there are smarter ways to decide how many “problematic” colors are enough to trigger the color blindness warning.
  • Clearly, it would help testing a larger set of charts and maps with this algorithm to measure its real performance

As always I’m happy to hear what you think. Feel free to drop a comment below or send me an email at gregor@datawrapper.de.

# Comments