Reusable Legends in D3JS

I am inherently lazy and I hate doing things repeatedly.  One of the task I am sure to need over and over is the ability to create versatile legends.

So I started the task tonight, first tackling ordinal legends.  Now I am relatively new to Javascript so if anyone notices bad programming practices, I am always open to criticism.

Now my first objective is to make the simple case simple, so I created a function which contains reasonable defaults, yet allows the user to override when needed.

The function looks like:

appendOrdinalHorizontalLegend(svg, labels, params)

svg is the node which we will append the legend.

labels is an array of labels for the legend.

params is an object with properties which can override default behavior.

Here is the code in which sets up the svg and labels and creates three legends:

var header =
	[ "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
	  "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" ]

var svg = d3.select("body").append("svg")
  .attr("width", 960)
  .attr("height", 500);

appendOrdinalHorizontalLegend(svg, header, { palette : d3.scale.category10() });
appendOrdinalHorizontalLegend(svg, header, {
  palette : d3.scale.category20(),
  yoffset : 90
});

appendOrdinalHorizontalLegend(svg, header, {
  caption : "Yet another legend...",
  palette : d3.scale.category20c(),
  yoffset : 160
});

Here is the appendOrdinalHorizontalLegend code:

function appendOrdinalHorizontalLegend(node, labels, params)
{
  // Default parameters.
  var p =
  {
    xoffset         : 10,
    yoffset         : 20,
    cellWidth       : 30,
    cellHeight      : 20,
    tickLength      : 25,
    caption         : "Legend",
    palette         : d3.scale.category20c(),
    captionFontSize : 14,
    captionXOffset  : 0,
    captionYOffset  : -6
  };

  // If we have parameters, override the defaults.
  if (params !== 'undefined')
  {
    for (var prop in params)
    {
      p[prop] = params[prop];
    }
  }

  // Create our x scale
  var x = d3.scale.ordinal()
    .domain(labels)
    .range(d3.range(labels.length).map(function(i) { return i * p.cellWidth; }));

  // Create the x axis.
  var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom")
    .tickSize(p.tickLength)
    .tickPadding(10)
    .tickValues(labels)
    .tickFormat(function(d) { return d; });

  // Append a graphics node to the supplied svg node.
  var g = node.append("g")
    .attr("class", "key")
    .attr("transform", "translate(" + p.xoffset + "," + p.yoffset + ")");

  // Draw a colored rectangle for each ordinal range.
  g.selectAll("rect")
    .data(labels)
    .enter().append("rect")
    .attr("height", p.cellHeight)
    .attr("x", function(d, i) { return x(i); })
    .attr("width", function(d) { return p.cellWidth; })
    .style("fill", function(d, i)
    {
      return p.palette(i);
    });

  // Add the caption.
  g.call(xAxis).append("text")
    .attr("class", "caption")
    .attr("y", p.captionYOffset)
    .attr("x", p.captionXOffset)
    .text(p.caption)
    .style("font-size", p.captionFontSize);
}

And here is the code in action:

HorizontalOrdinalLegend

Anyway, this is my thought process early on.  If I seem to be going down the wrong track, I’d love to get input.  I don’t think it’s quite right.  It doesn’t feel object oriented, however, it achieves my main goal of adding Legends with a one liner.  I can always refactor later.

Advertisements

About patmartin

I am a coder and Data Visualization/Machine Learning enthusiast.
This entry was posted in General. Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s