Improving D3 Sequence Sunburst Example

VividD picture VividD · Jul 5, 2014 · Viewed 10.9k times · Source

This D3 example served as my starting point:

http://bl.ocks.org/kerryrodden/7090426

enter image description here


I wanted to change data that feeds the diagram, and I made following new example:

http://jsfiddle.net/ZGVK3/

enter image description here

One can notice at least two problems:

  1. Legend is wrong. This is because it still contains 'hardcoded' names from original example.
  2. All nodes are colored black. This is because the color scheme is also 'hardcoded' only for node names from original example.

How to improve the original example (or my jsfiddle, it doesn't matter) so that legend and coloring are self-adjusted to the data that feeds the diagram?

Answer

jshanley picture jshanley · Jul 6, 2014

You can use an ordinal scale to map colors to the different node names. Implementing it would only require a few minor changes to your existing code.

Step 1. Create an ordinal scale for the colors

Instead of having colors be simply a list of color names, hard-coded to specific names, use d3.scale.ordinal(), and set the .range() to be an array of the colors you want to use. For example:

var colors = d3.scale.ordinal()
  .range(["#5687d1","#7b615c","#de783b","#6ab975","#a173d1","#bbbbbb"]);

This would create an ordinal scale that uses the same colors as the original visualization. Since your data would require more colors, you would want to add a few more to your range, otherwise colors will be repeated.

As a shortcut, you can use d3.scale.category20() to let d3 choose a range 20 categorical colors for you.

Now when setting the fill colors for your path element arcs and also your breadcrumbs, you would simply use colors(d.name) instead of colors[d.name].

Step 2. Use your data to construct the domain of the scale

The .domain() of this scale will be set once we have the data, since it will depend on a list of the unique names contained in the data. To do this, we can loop through the data, and create an array of the unique names. There are probably several ways to do this, but here's one that works well:

var uniqueNames = (function(a) {
  var output = [];
  a.forEach(function(d) {
    if (output.indexOf(d.name) === -1) {
      output.push(d.name);
    }
  });
  return output;
})(nodes);

This creates an empty array, then loops through each element of the nodes array and if the node's name doesn't already exist in the new array, it is added.

Then you can simply set the new array to be the domain of the color scale:

colors.domain(uniqueNames);

Step 3. Use the scale's domain to build the legend

Since the legend is going to depend on the domain, make sure the drawLegend() function is called after the domain is set.

You can find the number of elements in the domain (for setting the height of the legend) by calling colors.domain().length. Then for the legend's .data(), you can use the domain itself. Finally, to set the fill color for the legend boxes, you call the color scale on d since each element in the domain is a name. Here's what those three changes to the legend look like in practice:

var legend = d3.select("#legend").append("svg:svg")
  .attr("width", li.w)
  .attr("height", colors.domain().length * (li.h + li.s));

var g = legend.selectAll("g")
  .data(colors.domain())
  .enter().append("svg:g")
  .attr("transform", function(d, i) {
     return "translate(0," + i * (li.h + li.s) + ")";
  });

g.append("svg:rect")
  .attr("rx", li.r)
  .attr("ry", li.r)
  .attr("width", li.w)
  .attr("height", li.h)
  .style("fill", function(d) { return colors(d); });

And that's about it. Hope that helps.

Here's the updated JSFiddle.