Continuous color scale from discrete domain of strings?

amergin picture amergin · Aug 31, 2012 · Viewed 12.4k times · Source

I'm implementing a heatmap in which the cell background color is determined by a d3 color scale. Some of the values are categorical; their value can be of N different arbitrary string-type categories like ["6TH", "7TH", "5TH", "4TH"].

Given a start color d3.rgb("blue") and an end color d3.rgb("red"), how can I construct color scale that maps a discrete domain of strings into a continuous color range?

I tried

var scale = d3.scale.ordinal()
    .domain(["6TH", "7TH", "5TH", "4TH"])
    .rangeBands( [ d3.rgb("blue"), d3.rgb("red") ] );

which obviously doesn't work.

Answer

mbostock picture mbostock · Aug 31, 2012

First, I would consider using one of the readily-available Colorbrewer scales; see colorbrewer2.org. These are also available as JavaScript and CSS files in D3's git repository; see lib/colorbrewer. For example, if you have four discrete values in your domain, and you want a red-blue diverging scale, you could say:

var color = d3.scale.ordinal()
    .domain(["6TH", "7TH", "5TH", "4TH"])
    .range(colorbrewer.RdBu[4]);

(You'll need a <script src="colorbrewer.js"></script> somewhere before this, too.) Colorbrewer has a variety of well-designed sequential, diverging and categorical color scales.

If you insist on rolling your own color scale, I strongly recommend interpolating in L*a*b* or HCL color space for accurate perception. You can do this using d3.interpolateLab or d3.interpolateHcl. For example, d3.interpolateLab("red", "blue")(.5) returns a color halfway between red and blue.

To compute the colors for your ordinal scale's range, you can use an interpolator, or you might find a temporary linear scale more convenient. For example:

var categories = ["6TH", "7TH", "5TH", "4TH"];

var color = d3.scale.ordinal()
    .domain(categories)
    .range(d3.range(categories.length).map(d3.scale.linear()
      .domain([0, categories.length - 1])
      .range(["red", "blue"])
      .interpolate(d3.interpolateLab)));