Sunday, January 17, 2016

How to make Tufte's line graph sparklines with d3.js

In this blog post, I will cover how to make a sparkline using d3.js . Our final product will look like this:


For those who are unfamiliar with them, sparklines were first discussed by Edward Tufte, who defines them as follows:


    A sparkline is a small intense, simple, word-sized graphic with typographic resolution. Sparklines mean that graphics are no longer cartoonish special occasions with captions and boxes, but rather sparkline graphics can be everywhere a word or number can be: embedded in a sentence, table, headline, map, spreadsheet, graphic. Data graphics should have the resolution of typography.    -- Edward Tufte, Beautiful Evidence


The first step in creating our sparkline is to create our HTML template. Our `d3.js` script will then add elements where necessary, based on our script's specifications. 


 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
<html>
<meta charset='utf-8'>




    <link href='https://fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css'>
    <style>
    text {
        font-family: 'Open Sans', sans-serif;
    }
    .sparkline-container {
        position:absolute;
        top:0px;
        left:0;
        float:left;
        width:300px;
        height:100px;
    }
    .sparkcircle {
      fill: #f00;
      stroke: none;
    }
    path {
        stroke: #000;
        stroke-width: 0.35px;
        fill: none;
    }
    </style>



<body>


    <div id="graph" class="aGraph sparkline-container" style=""></div>


</body>


<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="
https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.12/d3.js" charset='utf-8'></script>

<script src='https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js'></script>
<script>

$(document).ready(function() {



});
</script>

</html>

We will add our `d3.js` code at line 50, within the `$(document).ready(function() {});`. This function simply ensures that our DOM is completely ready before the `d3.js` script runs. 

First, let's define some test data. I'll go ahead and use the currency fluctuations of the Brazilian Real vs. the USD over the past month, which I have lying around from the sparkline dashboard project:


1
2
3
4
5
        var data = { 'rates.BRL': [3.7629, 3.853, 3.853, 3.853, 3.9045,
        3.8771, 3.9369, 3.877, 3.9004, 3.9004, 3.9004, 3.976,
        3.9827, 3.9677, 3.941, 3.941, 3.941, 3.941, 3.9257,
        3.852, 3.898, 3.9604, 3.9604, 3.9604, 3.9604, 4.0395,
        4.0036, 4.0338, 4.0487, 4.0487]}

Next, we define the dimensions of the box that contains our sparkline. While this may seem trivial, it is the essential element that sets a sparkline aside from a regular line graph -- it is a small visual representation of data. We will use this variable in a bit:


Now, we create a skeleton of our sparkline object. We will have the side text which corresponds to the sparkline label, an svg element that will show the sparkline graph, and a little red circle that will highlight the sparkline's endpoint. Feel free to play around with including or not including the end circle -- I thought that it added to the visualization, but depending on where exactly you are using your sparkline, it might make sense to leave it out. 


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
        d3.select('body')
          .selectAll('g')
          .data(d3.keys(data))
            .enter()
            .append('g')
            .classed('sparkline-container', function(d) {
                return true;
            })
            .style('top', function(d, i) { return (i + 1) * 35 + 'px';})
            .style('left', '500px')

          .append('text')
            // get last 3 characters since format is like rates.JPY
            .text(function(d) {return d.slice(-3);})
          .append('svg')
            .attr('id', function(d) { return 'graph' + d.slice(-3);})
          .append('circle')
            .attr('id', function(d) { return 'circle' + d.slice(-3);})
            .attr('r', 1.5)
            .classed('sparkcircle', true);

Let's break down that last chunk of code.  In line 1, we select our body element, to which we will be appending the rest of our visualization. Then, we choose to append as many 'g' elements as we have data points. In this case, we only have one time series, but we could easily re-use this code to add numerous sparklines. In line 6, we make sure that our sparkline g elements have the `sparkline-container` class. Based on our styles that we defined, this will give them a certain width, height, and alignment. In line 9, we give it a distance from the top that depends on our i, or incrementing, value -- this will make sure that our sparklines will not be on top of each other if we have more than one. 


You'll notice that so far, the svg doesn't have any line elements within it, and the circle doesn't have any `cx` nor `cy` attributes, so neither one of these elements will render a visual element. Let's make a function that will actually draw our sparklines and end circles for us!




 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
         function drawSparkline(svgElemId, circleElemId, data) {

            // create an SVG element inside the #elemId div that fills 100% of the div
        var graph = d3.select(svgElemId)
            .append("svg:svg")
            .attr("width", "100%")
            .attr("height", "100%");


        // X scale will fit values from 0-60 within pixels 0-width
        var x = d3.scale.linear()
            .domain([0, 60])
            .range([0, boxDimensions.width]);
        // Y scale will fit values from [the extent of our currency fluctations
        // dataset] within pixels 0-100
        var y = d3.scale.linear()
            .domain(d3.extent(data, function(d) { return d; }))
            .range([0, boxDimensions.height]);

        // create a line object that represents the SVG line we're creating
        var line = d3.svg.line()
            // Makes our plots smooth
            .interpolate('monotone')
            // assign the X function to plot our line as we wish
            .x(function(d,i) {
                return x(i);
            })
            .y(function(d) {
                return y(d);
            });

            // display the line by appending an svg:path element with the data line we created above
            graph.append("svg:path").attr("d", line(data));

        var circle = d3.select(circleElemId)
            .attr('cx', function(d, i) {return x(29); })
            .attr('cy', function(d) {
                /* And this is really helpful for figuring out what is going on */
                return y(data.slice(-1)); })

        }

The key part of this chunk of code is lines 21 - 30 -- these actually create a line object that represents the SVG line that we are creating. We use the x and y scale functions that we have defined previously, and then actually display the line by appending an svg:path. Finally, lines 35-39 give our circles `cx` and `cy` attributes, which will define where the center of the circle should be. Our `cx` is fixed, since we always want for the circle to be drawn at the last data point, and our `cy` consists of the last element in our data set.

Now that we have this handy sparkline function, let's call it for each data element!


1
2
3
4
5
 
        $.each(data, function(country_name, country_series){

            drawSparkline('#graph' + country_name.slice(-3), '#circle' + country_name.slice(-3),data[country_name]);
        });

And we do a tiny bit of visual cleanup to ensure that the labels and the SVGs are not on top of each other:



1
        $('svg').css({left: 45, position: 'absolute'});

Which gives us a working sparkline.



Check out the full example on Github. If you're interested in sparklines, you can read about creating a dashboard that shows numerous sparklines for comparative purposes here. You can learn more about sparklines by reading Tufte's Beautiful Evidence.

3 comments:

Sign up to receive data viz talk tips and updates via email. We're low traffic and take privacy very seriously.