10 May 2015

Posted by: Duncan
Tags: JavaScript | D3 | Bootstrap

Today at ExtraGravity HQ I've been encouraging D3 to cooperate with Bootstrap. In fact I've been trying to encourage D3 to cooperate with the world outside of Mike Bostock's head, a head that obviously contains a truly colossal brain but while (frustratingly) exhibiting the genius's usual disregard for the prosaic (in this case, object-orientation and responsiveness).

We know that JavaScript doesn't support classes; that D3 is based on a procedural, chained and theatrically-derived notation that bears no relation to modern O-O; and that there's no point in using a responsive framework like Bootstrap if client components use hard-coded pixel values because the results will be hideous; fixed-size charts overflowing into surrounding elements or scrollbars appearing when the window was shrunk would have me kicked out of the Somerset Computer Club for gross amateurism. How would I live that down? I got to work...

JavaScript has a nice pattern that emulates classes;

function class(args)
{
   this.a = args.a || false;
   this.b = args.b || false;
}

var myclass = class({a:true,b:true});

I used this pattern to rig up a D3 wrapper pseudo-class for rendering a multiline chart showing Met Office data. My class exposed a simple API (the args argument above); "class methods" were emulated using JavaScript closures;

this.draw_chart = function (obj)
{
   return function()
   {
      obj.chart = d3.select("#"+obj.container)
         .attr("width", obj.width + obj.margin*2)
         .attr("height", obj.height + obj.margin*2)
         .append("g")
         .attr("transform", 
            "translate(" + obj.margin + "," + obj.margin + ")");
      
      // X Axis >>>
      obj.chart.append("g")
         .attr("class", "x axis")
         .attr("transform", "translate(0," + obj.height + ")")
         .call(obj.x_axis);
      
      // Y Axis >>>
      obj.chart.append("g")
         .attr("class", "y axis")
         .call(obj.y_axis)
         .append("text")
            .attr("transform", "rotate(-90)")
            .attr("y", 6)
            .attr("dy", ".71em")
            .style("text-anchor", "end")
            .text("Temp");
   }
}(this);

I soon had a reusable D3 line chart class. The next step was to get it to work with Bootstrap and to react properly to dynamic window resizing and deployment to any device.

This bit wasn't straightforward at all. There's a way to implement responsiveness with external JavaScript but I wanted a more elegant solution. I found it in a modification of the so-called padding-bottom hack, in which SVG's support for an aspect ratio is invoked by the percentage dimensions of a nested pair of container elements. In fact, the hack was eventually discarded as a solution due to unexpected and uncontrollable margin issues on some browsers but it did lead me to the realisation that an aspect ratio could be provided to D3 in chart construction and maintained throughout the lifecycle of the component using some minor internal JavaScript.

I'll put the code on Github once I've finished with interactivity but, for now, the results are;



Just to show that this works within a fluid Bootstrap framework, here are a pair of side-by-side single-line charts that remain responsive when the browser is resized;



Here's the wrapper class in use; it's not complete as it has yet to include interactivity but it shows a working D3 reusable line chart based on a user-defined aspect ratio which works with Boostrap;

var line_chart = new D3LineChart({
      container      : 'responsive-container',
      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   : ASPECT_RATIO || 1.667
   });

Links