D3 line chart interactivity

Sunday, 02 August 2015 16:04

D3, without doubt the visualisation library for the cognoscenti, can be a major brain-ache at times. Getting D3 to cooperate with Bootstrap was not exactly straightforward but line chart interactivity takes things to another level entirely.

No doubt due to limitations in my own so-called "brain", I find the D3 examples brilliant but largely unusable. Brilliant because they look so cool; unusable because they ignore reusability, rely on fixed data files, and assume the reader is Stephen Hawking. Throw in the complete absence of commentary or explanation and the developer has his or her work cut out. So what follows is a summary of my explorations into encouraging my D3 line chart to interact with the user while retaining its core reusabilty and cooperation with Bootstrap.

Voronoi tessellations

The D3 code examples tend to use a Voronoi tessellation for line chart feedback, in which regions around each point catch mouse events. D3 provides a voronoi() function that takes a set of predefined points and generates paths that represent the Voronoi tessellation (or diagram). This is shown in the chart below (the Voronoi regions would normally be hidden);

The Voronoi tessellation is built in to an updated D3LineChart object, as is the animated feedback (being a D3 transition applied to the point itself). The code takes the actual points from an interpolated line (and not the actual data points) and introduces new data for each index; this new data is used to provide feedback info to the user;

for ( var i=0; i<paths.length; i++ )
   var v = obj.sample ( paths[i].path.node(), paths[i].y_key);
   for ( var j=0; j<v.length; j++ )
      point_data.push( { year  : parseInt(obj.x_scale.invert(v[j][0])),
                         value : obj.y_scale.invert(v[j][1]),
                         x     : v[j][0],
                         y     : v[j][1],
                         y_key : paths[i].y_key,
                         stroke: paths[i].stroke
var v_data = obj.voronoi(point_data);

   .attr("d", function(d) { return "M" + d.join("L") + "Z"; })
   .style({"fill"         : "#ffffff", 
           "stroke"       : "#9c9c9c", 
           "stroke-width" : "1px",
           "opacity"      :"0.1" })
   .datum(function(d) { return d.point; })
   .on("mouseover", obj.mouseover )
   .on("mouseout", obj.mouseout);
var points = obj.chart.selectAll("circle").data(point_data);
   .attr("cx", function (d) { return d.x; })
   .attr("cy", function (d) { return d.y; })
   .attr("id", function(d,i) { return "point-" + i.toString(); })
   .style("stroke", function (d) { return d.stroke; } )
   .style({"fill": "none"})
   .attr("r", 0);

This is a lot of work just to implement interactivity but, in this case, it's done once and can be reused easily. The code needed to get a line chart into your web page can be as simple as;

function content_ready ()
      url         : "/charts/?limit=240",
      type        : "get",
      dataType    : "json",
      success     : function(data)
         var max_y = d3.max(data.data, function(d) { return parseFloat(d.tmax); });
            var line_chart_adv = new D3LineChartAdv({
               container      : 'responsive-container-2',
               margin         : {top: 10, right: 20, bottom: 60, left: 50},
               max_y          : Math.round(max_y + Math.ceil(2)),
               min_y          : 0,
               x_key          : 'year',
               aspect_ratio   : 1.667
            line_chart_adv.draw_lines ([
               { y_key : 'tmin', class_name: 'line-tmin' }, 
               { y_key : 'tmax', class_name: 'line-tmax' }
            console.log(e.name + ", " + e.message);

Whether this is useful or not I have no idea but at least this approach lets me deploy interactive data to mobile devices in a Bootstrap framework. There's a bit more to do yet; the simple API exposed by the D3LineChartAdv class needs to be extended to include other stuff like a legend, a title and a grid. Once I have some time I'll get it done.