Tuesday, March 21, 2017

D3.js - The search for the working responsive demo.

Had some fun recently where I needed to add some graphing to a web page.  The purpose was to give customers an idea of how much and when they were consuming data and how much of their bandwidth was being utilized.  The idea was to help them understand their patterns so that they could better understand why their internet was slowing down.  Coupled with the knowledge of what devices were on their network during these times provided a nice debugging and education tool for our customers and technical support team.

But during the development I found maze of twisty passages on how to use the D3 graphics library and how to use that library within a responsive web page.  Most of it sadly either didn't work (old version of D3) or was badly documented so that it was essentially unusable.  What I came up with likely won't win awards for good code design.  But I hope to provide an easier approach to this problem that hopefully someone stumbles across.  For this demonstration I used a simple bar chart that would raise up from the bottom of the <svg> element.  It used a simple array of numbers as its data source which was stored in a JSON file on the web server (and could be easily replaced with REST call to obtain the same data!).

Really there are only two major components.  The first is to setup of a number of helper methods that obtain the height and width of the element containing the <svg> element where your graphic has been placed.  I ended up with eight methods - getBarHeight, getBarWidth, getXPosition, getYPosition, getTextXPosition, getTextYPosition, getHeight, getWidth.  Most of these should be understandable.  Below is a full listing of these methods.  Earlier in the javascript I defined a variable called containerElement which stored the element in which the <svg> was contained.  Additionally another variable - dataItems - was populated with my data set - in this case it was a simple array of numbers.  However, you can substitute an array of objects just as easily.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function getBarHeight(dataItem) {
        return containerElement.clientHeight * (dataItem/100);
    }

    function getBarWidth() {
        return (containerElement.clientWidth / dataItems.length) - padding;
    }

    function getXPosition(dataItemPosition) {
        return dataItemPosition * ( containerElement.clientWidth / dataItems.length) + padding;
    }

    function getYPosition(dataItem) {
        return containerElement.clientHeight - getBarHeight(dataItem);
    }

    function getTextXPosition(dataIemPostion) {
        return getBarWidth()/2 + (dataIemPostion * ( containerElement.clientWidth / dataItems.length) - padding);
    }

    function getTextYPosition(dataItem) {
        return containerElement.clientHeight - getBarHeight(dataItem, containerElement.clientHeight) - padding;
    }

    function colorPicker(dataItem) {
        if ( dataItem &gt;= containerElement.clientHeight/2 ) {
            return "red";
        }
        return "blue";
    }

    function setWidth() {
        return containerElement.clientWidth;
    }

    function setHeight() {
        return containerElement.clientHeight;
    }

Essentially the magic was to find the clientHeight/clientWidth of the container of the <svg> element and perform some simple math to determine the width, height and relative positions for the bar chart items.

The second piece to this mystery was actually registering a resize method to the widow resize event to a method that would, this in case, redraw the bar graph after when the event fired.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
var BarChart = (function(window, d3) {

    /* TODO - set for max/min height/width*/

    var dataItems = [];
    var padding = 3;
    var svgElement = [];
    var containerElement = [];

    function createBarGraph(elementName, svgName, urlAction) {

        containerElement = $("#" + elementName)[0];

        svg = d3.select("#" + elementName)
            .append("svg")
            .attr("id", svgName)
            .attr("width", setWidth())
            .attr("height", setHeight());

        svgElement = $("#" + svgName);

        d3.json(urlAction, initData);

        d3.select(window).on('resize', resize);
    }

    function getBarHeight(dataItem) {
        return containerElement.clientHeight * (dataItem/100);
    }

    function getBarWidth() {
        return (containerElement.clientWidth / dataItems.length) - padding;
    }

    function getXPosition(dataItemPosition) {
        return dataItemPosition * ( containerElement.clientWidth / dataItems.length) + padding;
    }

    function getYPosition(dataItem) {
        return containerElement.clientHeight - getBarHeight(dataItem);
    }

    function getTextXPosition(dataIemPostion) {
        return getBarWidth()/2 + (dataIemPostion * ( containerElement.clientWidth / dataItems.length) - padding);
    }

    function getTextYPosition(dataItem) {
        return containerElement.clientHeight - getBarHeight(dataItem, containerElement.clientHeight) - padding;
    }

    function colorPicker(dataItem) {
        if ( dataItem &gt;= containerElement.clientHeight/2 ) {
            return "red";
        }
        return "blue";
    }

    function setWidth() {
        return containerElement.clientWidth;
    }

    function setHeight() {
        return containerElement.clientHeight;
    }

    function setGraph() {
        svg.selectAll("rect")
            .data(dataItems)
            .enter()
            .append("rect")
            .attr( "x", function(d, i) { return getXPosition(i); })
            .attr( "y", function(d, i) { return getYPosition(d); })
            .attr( "width", function(d, i) { return getBarWidth()})
            .attr( "height", function(d, i) { return getBarHeight(d); })
            .attr( "fill",function(d, i) { return colorPicker(getBarHeight(d)); })
        ;

        svg.selectAll("text")
            .data(dataItems)
            .enter()
            .append("text")
            .text( function(d) { return d;})
            .attr( "x", function(d, i) { return getTextXPosition(i) })
            .attr( "y", function(d, i) { return getTextYPosition(d) })
        ;
    }

    function initData(data) {
        dataItems = data;
        setGraph();
    }

    function resize() {
        svg.selectAll("rect").remove();
        svg.selectAll("text").remove();
        svgElement.attr("width", setWidth()).attr("height", setHeight());
        setGraph();
    }

    return {
        resize : resize,
        createBarGraph : createBarGraph
    }

})(window, d3);

I encapsulated all the behavior into an object I called BarChart and placed it in a separate .JS file to reuse.  The full listing is below.  There are only two public methods - resize and createBarGraph.  And honestly only the createBarGraph is necessary as the resize event handler is defined within the object during initialization.  The BarChart object takes three parameters in the createBarGraph method.  The first is elemementName, which is the Id attribute given to the element which will contain the <svg> element.  The second is the svgName, which will be used as the Id attribute when the <svg> element is created.  And finally the urlAction which is where the data for the graph can be obtained.  I'll admit the parameter names are a bit wonky - but since you have access to the source code you are welcome to change them.