« Screen resolutions of today’s web users
The status of policy implementations of Open Government Data »

Designing accessible color spectrums

As a web developer who likes to produce info graphics, I’ve often run into the problem of choosing good color palettes for charts and, in the harder case, a smooth color spectrum. Colors should be aesthetically pleasing and able to convey differences in accordance with our perceptual abilities.

UPDATE 1: Jan 9, 2011: I made a number of mistakes the first time around, including having the diagrams flipped.

There are a number of types of color blindness (see Wikipedia) but the most common are the absence or dysfunction of the “red”, “blue”, and “green” cones in the retina. The “red” and “green” cones affect up to 10% of men, the “blue” cones and all cones in women must more rarely. When a cone is absent, the individual can’t make distinctions among colors that vary only in the quantity of that color, if you think about colors as being a mix of red, green, and blue. (Actually the cones don’t respond to prototypical red, green, and blue but instead have a distribution of response over a range of color wavelengths not necessarily particularly close to the prototypical wavelengths.)

Until today I didn’t really understand the mechanics of color blindness and so it was difficult to understand how to choose good color spectra. Worse, the only simple guide I could find through Googling gave some examples of good color palettes to use without explanation and without relation to the various types of color blindness.

Here’s what I’ve learned this afternoon.

The CIE 1931 color space is a mathematical model for our perception of color based on the activity of the three types of cones. As with “normal” seeers who have three types of cones, the CIE 1931 model has three dimensions of color. And it has two powerful implications: first, it gives us a coordinate system that covers the gamut of colors that can be perceived. Second, it gives a mathematical model that can be used to understand what happens for color blind people. In particular, in the CIE 1931 flattened two-dimensional color space, color blindless is represented by radial lines emanating from a red, green, or blue “copunctal point”. Color blind individuals (of each type) cannot distinguish two colors if they fall on the same radial line. These are called confusion lines. (Also, this is an interesting way of understanding the reduction of one dimension of perception.)

spacer

CIE 1931 Color Space and Confusion Lines for "Red" Color Blindness, from www.colblindor.com

I found this fascinating.

Now let’s make this practical. For a web designer concerned about accessibility, avoid following radial lines! More on this in a moment.

Backing up from color blindness, it’s important that the choice of colors on a spectrum are spaced to correspond with our ability to distinguish nearby colors. One drawback of the CIE 1931 model is that, for instance, green gets an unfairly large region compared to other colors — that whole area up top just looks like the same green to me. A newer alternative color space called CIE 1976 (L*, a*, b*) (a.k.a. CIE LAB) is a transformation of the older CIE 1931 space so that equal distances in the color space represent equal amounts of perceptual distance. A color spectrum for a chart should be taken by drawing a line or curve through this type of color space. (In the images below, the color space is larger than the colored region but the black areas cannot be represented on computer screens because they do not fall in the RGB color space.)

Now we have to put these two together. The CIE 1931 color space gives a model for how to choose accessible colors: avoid the confusion lines. The best way to avoid a confusion line is to go perpendicular to confusion lines. But we want to go perpendicularly in CIE LAB space so that we make the most perceptually distinct step. (I’m assuming that perceptual distinctiveness between two points in CIE LAB space is unaffected by color blindness. It’s probably wrong but good enough.)

Let’s start with protanopia, the lack of “red” cones, as an example. The image below plots in the dark dotted lines the confusion lines for protanopia on the CIE LAB color space. Note that because CIE LAB space is a distortion of the CIE 1931 space, the confusion lines no longer appear to radiate from a point.  (This color space has a third dimension for lightness, L in 0-100, not shown. Here I’m choosing L=50.) For a “normal”-sighted individual, any path through the color space will be perceptually useful for a chart. For a protanopic, only paths that go perpendicular to the confusion lines will have maximal perceptible differences. If you follow confusion lines, the protanopic will not be able to tell the difference. As you can tell, going from red to green is not a good idea since it follows a confusion line. The perpendiculars are indicated by white arrows. Good gradients follow the perpendiculars.

spacer

Protan Spectrum Lines in CIE LAB L=50

Here’s the full set of images for protanopia (red, left), deuteranopia (green, middle), and tritanopia (blue, right), for different values of lightness:

spacer spacer spacer
spacer spacer spacer
spacer spacer spacer

As you can see, “red” and “green” cone color blindness is similar. “Blue” cone color blindness is totally different, in fact it’s practically a 90-degree rotation of the other two making it impossible to follow a line that is maximally perceptible by everyone.

Since the first two are similar and the prevalence of tritanopia and tritanomaly are considerably rarer compared to the other two, if we put tritanopia and tritanomaly aside (for now!) and design for the other two, we might be able to choose a single color spectrum that at least works reasonably well for those cases. A good color spectrum to use will be a vertical line that stays within the RGB boundary, either orange to blue or red to purple. That said, if we vary from the perpendiculars a little bit we might be able to satisfy everyone a little. Orange to turquoise and green to pink go diagonally across the color space and so might cover everyone.

That said, this is all theoretical. I’m not color blind so I don’t have any intuitions about whether this is right. Also, this is my first time getting into the math of colors so… maybe I got it wrong somewhere. In fact, in my first version of this I had numbers backwards and perpendicular lines that weren’t. Hopefully this is closer to the truth now. (And I appreciate the great explanations given by Daniel Flück at his blog.)

Finally, apparently everyone can see lightness, so the most accessible spectrum is just varying the lightness (and the color doesn’t matter).

These images were created with a Python script and the grapefruit, numpy, and matplotlib libraries. Here is the code:

# Usage: python plot.py L protan|deutan|tritan
#
# e.g. python plot.py 50 protan
# e.g. python plot.py 50 protan
#
# To generate all of the images in at a bash shell:
#    for L in {25,50,75}; do for b in {protan,deutan,tritan}; do echo $L $b; python plot.py $L $b; done; done

###########################################

import sys
from math import sqrt, atan2
from grapefruit import Color
import matplotlib.pyplot as plt
import numpy

w, h = (480, 480)
L = float(sys.argv[1])
bt = sys.argv[2] # blindness type

# According to www.colblindor.com/2009/01/19/colorblind-colors-of-confusion/
# These are points for each type of color blindness around which the dimensionality
# of the color space is reduced, in CIE 1931 color space.
copunctal_points = {
	"protan": (0.7635, 0.2365),
	"deutan": (1.4000, -0.4000),
	"tritan": (0.1748	, 0.0000)
	}

# Draw the color space.
colorspace = [[(0.0,0.0,0.0) for x in xrange(0, w)] for y in xrange(0, h)]
for x in xrange(0, w):
	for y in xrange(0, h):
		# Compute the CIE L*, a*, b* coordinates (easy, since our x,y coordinates
		# are just a translation and scaling of the LAB coordinates).
		a = 2.0*x/(w-1) - 1.0
		b = 1.0 - 2.0*y/(h-1)

		# Convert this into sRGB so we can plot the color, and plot it.
		clr = Color.NewFromLab(L, a, b)
		r, g, b = clr.rgb
		if r < 0 or g < 0 or b < 0 or r > 1 or g > 1 or b > 1:
			continue

		colorspace[y][x] = (r,g,b)

# Draw the confusion line or spectrum line gradient.
csegs = 15
contourpoints = {
	"x": [[0 for x in xrange(0, csegs)] for y in xrange(0, csegs)],
	"y": [[0 for x in xrange(0, csegs)] for y in xrange(0, csegs)],
	"spectrum": [[0 for x in xrange(0, csegs)] for y in xrange(0, csegs)],
	"confusion": [[0 for x in xrange(0, csegs)] for y in xrange(0, csegs)]
	}
for xi in xrange(0, csegs):
	for yi in xrange(0, csegs):
		# Compute pixel coordinate from grid coordinate.
		x = xi/float(csegs-1) * (w-1)
		y = yi/float(csegs-1) * (h-1)

		# Compute the CIE L*, a*, b* coordinates (easy).
		a = 2.0*x/(w-1) - 1.0
		b = 2.0*y/(h-1) - 1.0

		# Compute the corresponding CIE 1931 X, Y, Z coordinates.
		X, Y, Z = Color.LabToXyz(L, a, b)

		# Convert CIE 1931 X, Y, Z to CIE 1931 x, y (but we'll keep capital
		# letters for the variable names). The copuntual point is in CIE 1931
		# x, y coordinates.
		X, Y = (X / (X + Y + Z), Y / (X + Y + Z))

		contourpoints["x"][yi][xi] = x
		contourpoints["y"][yi][xi] = y

		# To compute the confusion lines, we plot a contour diagram where
		# the value at each point is the point's angle relative to the copunctal
		# point. Two points on the same confusion line will have the same angle,
		# and contour plots connect points of the same value.
		dY, dX = Y - copunctal_points[bt][1], X - copunctal_points[bt][0]
		contourpoints["confusion"][yi][xi] = atan2(dY, dX) # yields confusion lines

# To compute the spectrum lines, we want lines perpendicular to the
# confusion lines. In my first attempt at this, I computed perpendiculars
# in the CIE 1931 space by choosing the contour plot value at a point to
# be the *distance* from the point to the copunctal point. This plotted
# the concentric circles around the copunctal point, transformed to
# CIE LAB space.
#
# However this is wrong, because the perpendiculars should be computed
# in CIE LAB space, which is the perceptual space. To compute the
# perpendiculars, we compute the gradient of the matrix that underlies
# the confusion lines. Then the gradient is plotted with the quiver plot type.
contourpoints["spectrum"] = numpy.gradient(numpy.array(contourpoints["confusion"], dtype=numpy.float))
for xi in xrange(0, csegs): # normalize!
	for yi in xrange(0, csegs):
		d = sqrt(contourpoints["spectrum"][0][yi][xi]**2 + contourpoints["spectrum"][1][yi][xi]**2)
		contourpoints["spectrum"][0][yi][xi] /= d
		contourpoints["spectrum"][1][yi][xi] /= d

# Draw it.
plt.figure()
plt.axes(frame_on=False)
plt.xticks([])
plt.yticks([])
plt.imshow(colorspace, extent=(0, w, 0, h))
plt.quiver(contourpoints["x"], contourpoints["y"], contourpoints["spectrum"][1], contourpoints["spectrum"][0],, alpha=.5)
plt.contour(contourpoints["x"], contourpoints["y"], contourpoints["confusion"], w/15, colors="black", linestyles="dotted", alpha=.25)
plt.text(0, 0, "~".join(sys.argv[2:]) + "; L=" + sys.argv[1],)
plt.savefig("colorspace_" + "_".join(sys.argv[1:]) + ".png", bbox_inches="tight", pad_inches=0)

This entry was posted on Saturday, January 8th, 2011 at 8:13 pm and is filed under Uncategorized. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

7 Responses to “Designing accessible color spectrums”

  1. spacer Joshua Gay says:
    January 9, 2011 at 1:08 pm

    It would be neat to write a JavaScript or CSS (maybe sizzler) library that made it really easy for a colorblind person to change the colorscheme through which they were viewing a web site so that it became optimal for them. That is, regardless of the original colorscheme chosen by the web designer, there would be a mathematical “shift” that would take place for a color-blind scheme that would leave the resulting site remaining as “true to form” as possible, while also being visually pleasing to the color-blind individual viewing the site.

  2. spacer Joshua Tauberer says:
    January 9, 2011 at 1:29 pm

    That seems like a theoretical nightmare. I started to read some papers about that while I was learning about this yesterday. And for now I think I will leave it to the researchers!

    But I’m hoping that for the current visualization I’m working on that it will have a color palette selector so it will be understandable for at least the three major types of color blindness.

  3. spacer Daniel Flück says:
    January 12, 2011 at 5:28 pm

    Well done! Good explanations everyone can follow.

    It’s nice to see how you put the confusion lines into another color space. And I can tell you from a red-blind point of view, the arrows are pointing into the correct direction :-)

  4. spacer Michael Hagerty says:
    April 7, 2011 at 12:05 pm

    You mentioned you are working on something to make the color palette understandable for at least the three major types of color blindness. I am a protanope and had been using the CBext add-in to Firefox until no longer kept up with Firefox updates and am now back to having to suffer through bad color choices by web designers, e.g., “respond to the questions in red {where the font is extremely narrow and thus identical to the black thin font}”…

    Since CBext is no longer supported, would you be considering something along those lines?

  5. spacer Joshua Tauberer says:
    April 13, 2011 at 7:46 am

    Hi, Michael. What I’m interested in actually is developing tools that website developers can use to choose original color pallets that are more accessible. (Creating a browser extension to revise an existing page is a considerably harder problem.) But I have a lot on my plate right now and I probably won’t get back to this for a while.

  6. spacer Neda Milic says:
    August 2, 2011 at 2:05 pm

    Hi, Josua. Nice work.
    My master thesis was “Development of plug in for simulating and helping people with Protanopia and Deuteranopia”, and I had the same idea from the beginning – that the best contrast for dichromats is obtained if the colours are on the perpendicular line to confusion line. I had some optimizing recolouring options based on Lab (or LCH) in my plug in, but I didn’t developed any further the idea of perpendicularity because that great thing if you want to choose the optimal colour combination, but it’s not good for photographs because if when you make better contrast between some two coloured surfaces, there are large chance that you will lower contrast between those newly coloured surfaces and the rest of their neighborhood. I have one question- perpendicular lines looks like they are in right direction, but the conversion between xyY and Lab is not so simple as you wrote it in a code. See here:
    xyY to XYZ
    www.easyrgb.com/index.php?X=MATH&H=04#text4
    XYZ to Lab
    www.easyrgb.com/index.php?X=MATH&H=07#text7

  7. spacer Joshua Tauberer says:
    August 2, 2011 at 4:51 pm

    Hi, Neda! I’m not sure, but I think you may be misunderstanding the lowercase x/y variables which are image coordinates, rather than xyY. But my head explodes when I try to keep all of these coordinate systems separate and it’s been months since I’ve tried to remember how all that works.

    One thing that I’ve thought about since writing this post is that actually the perpendicular lines themselves may not be optimal. What counts is the perpendicular distance. Two colors on a perpendicular will be differentiable, but if you move the second point along a confusion line they ought to remain differentiable (because the perpendicular distance will remain unchanged) but they should *gain* in distinctiveness for normal-signed viewers, which in principle is better.

    Changing colors of existing things seems… really hard. To say the least. If all three dimensions of color were used in the original, it would be impossible to squeeze them into two dimensions without losing something. If I had to give it a shot, I would try changing three dimensions of color into two dimensions of color plus one dimension of something else (maybe a spatial distortion). Fun.

    I find all this incredibly fascinating.

Leave a Reply

Click here to cancel reply.