~ 2 min read

Scaling a Linear Domain to an Ordinal Range with d3

I’ve had the opportunity of using d3 quite a lot over the past few months for a number of clients. It offers some amazing flexibility for chart generation and much more.

Anyway, I thought I’d share a quick tip I developed for mapping a linear set of values onto an ordinal scale. For those who’re seasoned pros at d3, this probably seems trivial, but had me stumped for some time today.

I’d picked out a colour palette I wanted to use for a particular graph, as per below:

var colours = ["#B8D0DE", "#9FC2D6", "#86B4CF", "#73A2BD", "#6792AB"];

The only examples I’ve seen similar to this are where it is assumed you want to vary darkness of colours based on value or vary the domain based on the number of colours you want. Not a good fit.

I wanted to pick one of my values based on a linear value from my data set. My first thought was to make use of the ordinal scale function provided by d3. Something like this:

var colour = d3.scale.ordinal()
  .domain(mySortedDataValues)
  .range(colours);

In doing this, I got something that *looked* a bit like it was working, but not the way I expected. In fact, the way an ordinal scale works is that it provides a 1-to-1 mapping of domain values to the range, rather than any kind of interpolation between them. In this case, it was expecting only 5 distinct data values (to match up against the colours) and for everything over and above that, it wrapped them round to the beginning of the domain again. The solution then is fairly simple once you’ve got your head around that.

What I did next, was to create a scale that gave the index of the colour we were going to be mapping to. This works well, because the indices are linear and d3 has the ability to do the dirty work in that respect.

var colourIndex = d3.scale.linear()
  .domain([0, d3.max(data)])
  .range([0, colours.length - 1]);

svg.selectAll("circle")
  .data(data)
  .enter()
  .append("circle")
  .attr("fill", function(d){
    return colours[Math.ceil(colourIndex(d))];
   });

Here we end up with an index ranging across all the indices of the colour array, and a colour appropriately selected from the palette as expected. You can see the resultant effect in the graph linked to below: