How to automatically choose a label color to contrast with background

Print More

What would data viz be without labels? Just viz, that’s what.

One common labeling challenge is picking a text color that has enough contrast with the element on which it sits. Here we’ll present one method of picking a label color and how to implement it using a library we’ve built.

Difficulty: Medium. This requires some experience with web front-end coding, but readers with a basic understanding of HTML, Javascript and the jQuery library shouldn’t find this concept challenging.

The approach

You could go with black labels, and use only bright color backgrounds, or go with white and use only dark color backgrounds, but that is limiting. What if you want to reuse the code with a new color scheme? What if you need more colors, or you want to use a gradient of colors from dark to bright to indicate a value?

A better way is to choose a label color value programmatically, a feature we’ve added to our catch-all library Trendy.js (Github).

The formula

Fortunately, the World Wide Web Consortium (W3C) offers a standard formula for calculating the perceived brightness of a color:

((Red value X 299) + (Green value X 587) + (Blue value X 114)) / 1000

Given 8-bit (0 to 255) red, green and blue values, the above formula returns a value from 0 to 255 that indicates brightness, with zero being the darkest and 255 the brightest value.

The following code from Trendy.js represents the above formula in javascript:

Trendy.brightness = function(r, g, b){
    return (r * 299 + g * 587 + b * 114) / 1000
Picking a label color

Now that we can determine the brightness of a background, we want to determine whether it’s bright enough to need or a dark label, or dark enough to need a bright label. We built that functionality into Trendy in two more functions.

This next function (which depends upon Trendy.brightness()) returns true depending on whether the brightness value of color [r, g, b] is greater than x.

Trendy.brighterThan = function(r, g, b, x) {
    return (this.brightness(r,g,b) > x);

We chose an arbitrary brightness value of 123, above which we consider a color to be “bright.” This last function (which depends upon Trendy.brighterThan()) returns true if color [r, g, b] is bright by this definition.

Putting it all together

Here’s an example of randomly generated colors (go ahead, refresh and see them change) with black or white labels based on their brightness. Note that it makes of jQuery and Trendy.js:

Finally, here’s the code used to generate that test page. Note that when you download and include and the Trendy.js library in your page, Trendy functions the variable Trendy, containing all of the library functions, is created:

for (var i = 0; i < 255; i += 5){

	var r = Math.floor(Math.random() * 255);
	var g = Math.floor(Math.random() * 255);
	var b = Math.floor(Math.random() * 255);

	var text_color = "rgb(0,0,0)";

	var brightness = Trendy.brightness(r,g,b);

	if (brightness < 123) {
		text_color = "rgb(255,255,255)";

rgb(" + r + "," + g + "," + b + "); brightness: " + brightness + "
"); }
Another implementation

Wall Street Journal’s Squaire library, which we use to make equal-area cartograms (like this one comparing bridge condition by state), uses a very similar approach to choose label color:

//return color for box labels based on how dark background color is
Squaire.prototype.overlayColor = function(color){
	//if only first half of color is defined, repeat it
	if(color.length < 5) {
		color += color.slice(1);
	return (color.replace('#','0x')) > (0xffffff/2) ? '#333' : '#fff';

The above code works the same way as ours, except it uses hexadecimal values to represent color, rather than RGB (we think RGB is a little more readable, but as you’ll see, the hex representation has an advantage as well). The main difference here is how Squaire calculates brightness and what color value it defines as “bright.”

Hex represents color with one number, with the lowest value (0x000000) as black and the highest value (0xffffff) as white. The higher the number, the brighter the color (basically), so Squaire simply uses white (0xffffff) divided by two as the cutoff, above which all values are considered “bright.”

The line that starts with “return” chooses either #333 (a dark gray) or #fff (white) depending on whether the specified color is above or below the cutoff.

Something we learned the hard way

It doesn’t pay to be overly clever. We found it’s best to pre-define dark and bright label colors (say black and white for simplicity), then use a formula to pick one.

Calculating the label color on a continuous spectrum from #000 to #fff as an inverse of brightness (formula: #fff – brightness) value is a bad idea, even though it sounds nifty. While it works OK for extreme bright and dark background colors, it will result in less contrast for backgrounds with brightness values closer to the middle.

What do you think?