Introducing D3.js

D3 is Not a Charting Library

$('#container').highcharts({
  title: {  text: 'Monthly Average Temperature', x: -20 },
  subtitle: { text: 'Source: WorldClimate.com', x: -20 },
  xAxis: { categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', //…
  yAxis: { title: { text: 'Temperature (°C)' },
       plotLines: [{value: 0, width: 1, color: '#808080'}] },
  legend: { layout: 'vertical', align: 'right',
        verticalAlign: 'middle', borderWidth: 0 },
  series: [{ name: 'Tokyo',    data: [ 7.0, 6.9, 9.5, 14.5, //…
           { name: 'New York', data: [-0.2, 0.8, 5.7, 11.3, //…
           { name: 'Berlin',   data: [-0.9, 0.6, 3.5,  8.4, //…
           { name: 'London',   data: [ 3.9, 4.2, 5.7,  8.5, //…
});

Where One Statement = A Chart

Tokyo New York Berlin London Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec -5 0 5 10 15 20 25 30 Temperature (°C) Monthly Average Temperature Source: WorldClimate.com

Charting Library - Buying a Home

D3 - Buying a Home at Home Depot

D3 Philosophy

D3 Components

Let’s Build a Chart

  1. Setup and Scaffolding: HTML, JSON, and AJAX
  2. D3 Scales: Map data ⇒ DOM
  3. Draw with SVG

HTML Scaffolding

<!DOCTYPE html>
<html>
<head>
  <meta charset='utf-8'>
  <title>Basic line demo</title>
</head>
<body>
  <script src='http://d3js.org/d3.v3.min.js'></script>
</body>
</html>

Data in JSON Format

[{
  "name": "Tokyo",
  "data": [ 7.0, 6.9, 9.5, 14.5, 18.2, 21.5, 25.2, 26.5, //…
 },{
  "name": "New York",
  "data": [-0.2, 0.8, 5.7, 11.3, 17.0, 22.0, 24.8, 24.1, //…
 },{
  "name": "Berlin",
  "data": [-0.9, 0.6, 3.5,  8.4, 13.5, 17.0, 18.6, 17.9, //…
 },{
  "name": "London",
  "data": [ 3.9, 4.2, 5.7,  8.5, 11.9, 15.2, 17.0, 16.6, //…
}]

Retrieve the Data

1
2
3
4
5
6
7
8
9
10
11
12
d3.json('data.json', function(error, datasets) {
  datasets.forEach(function(dataset) {
    dataset.data = dataset.data.map(function(d,i) {
      return {
        "date": d3.time.month.offset(
                  new Date(2013,0,1), i),
        "temp": d
      };
  });

  // Continue...
})

Use a Scale to Map Data ⇒ DOM

var y = d3.scale.linear()
          .range([height, 0])
          .domain(d3.extent(dataset, dataset.temp))
          .nice();

Scales Don’t Have to be Linear

var x = d3.time.scale()
          .range([0, width])
          .domain(
            d3.extent(dataset, dataset.date)
              .map(function(d, i) {
                d3.time.day.offset(d, i ? 15 : -16)
            })
        );

Create the SVG Container

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

Graph the Data Points

1
2
3
4
5
6
7
8
9
10
11
svg.selectAll(".point")
      .data(dataset.data)
    .enter().append("path")
      .attr("class", "point")
      .attr("fill", d3.scale.category10(idx))
      .attr("stroke", d3.scale.category10(idx))
      .attr("d", d3.svg.symbol(idx));
      .attr("transform", function(d) {
        return "translate(" + x(d.date) +
                        "," + y(d.temp) + ")";
      });

Associate DOM and Data

<path class= “point” d= “…” transform= “translate(…)” /> 7.0° <path class= “point” d= “…” transform= “translate(…)” /> 6.9° <path class= “point” d= “…” transform= “translate(…)” /> 9.5°

Graph the Data Points (cont’d)

1
2
3
4
5
6
7
8
9
10
11
svg.selectAll(".point")
      .data(dataset.data)
    .enter().append("path")
      .attr("class", "point")
      .attr("fill", d3.scale.category10(idx))
      .attr("stroke", d3.scale.category10(idx))
      .attr("d", d3.svg.symbol(idx));
      .attr("transform", function(d) {
        return "translate(" + x(d.date) +
                        "," + y(d.temp) + ")";
      });

Add the Connecting Lines

1
2
3
4
5
6
7
8
9
10
svg.append("path")
    .datum(dataset.data)
    .attr("fill", "none")
    .attr("stroke", color(i))
    .attr("stroke-width", "3")
    .attr("d",
        d3.svg.line()
            .x(function(d) { return x(d.date); })
            .y(function(d) { return y(d.temp); })
    );

Add the Axes

1
2
3
4
5
6
svg.append("g")
    .attr("transform", "translate(0," + height + ")")
    .call(d3.svg.axis()
              .scale(x)
              .tickFormat(d3.time.format("%b"))
              .orient("bottom"));

The D3 Version

JanFebMarAprMayJunJulAugSepOctNovDec0510152025Temperature (°C)TokyoNew YorkBerlinLondonMonthly Average TemperatureSource: WorldClimate.com

That’s a Lot of Code

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
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
// Convenience functions that provide parameters for
// the chart. In most cases these could be defined as
// CSS rules, but for this particular implementation
// we're avoiding CSS so that we can easily extract
// the SVG into a presentation.

// What colors are we going to use for the different
// datasets.
var color = function(i) {
  var colors = ["#CA0000", "#A2005C",
                "#7EBD00", "#007979"];
  return colors[i % colors.length]
};

// What symbols are we going to use for the different
// datasets.
var symbol = function(i) {
  var symbols = ["circle", "diamond",
                 "square", "triangle-up",
                 "triangle-down", "cross"];
  return d3.svg.symbol()
           .size(100)
           .type(symbols[i % symbols.length]);
};

// Define the dimensions of the visualization.
var margin = {top: 60, right: 160, bottom: 30, left: 50},
    width = 840 - margin.left - margin.right,
    height = 520 - margin.top - margin.bottom;

// Since this is a line chart, it graphs x- and y-values.
// Define scales for each. Both scales span the size of
// the chart. The x-scale is time-based (assuming months)
// and the y-scale is linear. Note that the y-scale
// ranges from `height` to 0 (opposite of what might be
// expected) because the SVG coordinate system places a
// y-value of `0` at the _top_ of the container.

// At this point we don't know the domain for either of
// the x- or y-values since that depends on the data
// itself (which we'll retrieve in a moment) so we only
// define the type of each scale and its range. We'll
// add a definition of the domain after we retrieve the
// actual data.
var x = d3.time.scale()
          .range([0, width]),
    y = d3.scale.linear()
          .range([height, 0]);

// Define the axes for both x- and y-values. For the
// x-axis, we specify a format for the tick labels
// (just the month abbreviation) since we only have
// the month value for the data. (The year is unknown.)
// Without the override, D3 will try to display an
// actual date (e.g. with a year).
var xAxis = d3.svg.axis()
  .scale(x)
  .tickSize(0, 0, 0)
  .tickPadding(10)
  .tickFormat(d3.time.format("%b"))
  .orient("bottom");

// For the y-axis we add grid lines by specifying a
// negative value for the major tick mark size. We
// set the size of the grid lines to be the entire
// width of the graph.
var yAxis = d3.svg.axis()
  .scale(y)
  .tickSize(-width, 0, 0)
  .tickPadding(10)
  .orient("left");

// Define a convenience function to create a line on
// the chart. The line's x-values are dates and the
// y-values are the temperature values. The result
// of this statement is that `line` will be a
// function that, when passed an array of data
// points, returns an SVG path whose coordinates
// match the x- and y-scales of the chart.
var line = d3.svg.line()
  .x(function(d) { return x(d.date); })
  .y(function(d) { return y(d.temp); });

// Create the SVG container for the visualization and
// define its dimensions. Within that container, add a
// group element (`<g>`) that can be transformed via
// a translation to account for the margins.
var svg = d3.select("body").append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .attr("transform", "translate(" + margin.left +
    "," + margin.top + ")");

// Retrieve the data. Even though this particular
// data file is small enough to embed directly, it's
// generally a good idea to keep the data separate.
d3.json('data.json', function(error, datasets) {

  // We're not bothering to check for error returns
  // for this simple demonstration.

  // Convert the data into a more "understandable"
  // JavaScript object. Instead of just an array
  // of numbers, make it an array of objects with
  // appropriate properties.
  datasets.forEach(function(dataset) {
    dataset.data = dataset.data.map(function(d,i) {

      // Although no year is given for the data
      // (so we won't display one), we can simply
      // pick an abitrary year (2013, in this case)
      // to use for our dates. We'll start in January
      // and increment by the index of the data value.
      // The data value itself is the temperature.
      return {
        "date": d3.time.month.offset(
          new Date(2013,0,1), i),
        "temp": d
      };
    });
  })

  // Now that we have the data, we can calculate
  // the domains for our x- and y-values. The x-values
  // are a little tricky because we want to add additional
  // space before and after the data. We start by getting
  // the extent of the data, and then extending that range
  // 16 days before the first date and 15 days after the
  // last date. To account for datasets of differing
  // lengths, we get the maximum length from among all
  // datasets.
  var xMin = new Date(2013,0,1),
      xMax = d3.time.month.offset(xMin,
               d3.max(datasets, function(dataset) {
                 return dataset.data.length-1;
               }));
  x.domain([d3.time.day.offset(xMin,-16),
            d3.time.day.offset(xMax,15)]);

  // For the y-values, we want the chart to show the
  // minimum and maximum values from all the datasets.
  var yMin = d3.min(datasets, function(dataset) {
               return d3.min(dataset.data, function(d) {
                 return d.temp;
               });
             });
  var yMax = d3.max(datasets, function(dataset) {
               return d3.max(dataset.data, function(d) {
                 return d.temp;
               });
             });

  // The `.nice()` function gives the domain nice
  // rounded limits.
  y.domain([yMin, yMax]).nice();

  // With the domains defined, we now have enough
  // information to complete the axes. We position
  // the x-axis by translating it below the chart.
  svg.append("g")
       .attr("class", "x axis")
       .attr("transform", "translate(0," + height + ")")
       .call(xAxis);

  // For the y-axis, we add a label.
  svg.append("g")
       .attr("class", "y axis")
       .call(yAxis)
     .append("text")
       .attr("font-size", "18")
       .attr("transform", "rotate(-90)")
       .attr("y", 9)
       .attr("dy", ".71em")
       .attr("text-anchor", "end")
       .text("Temperature (°C)");

  // Style the axes. As with other styles, these
  // could be more easily defined in CSS. For this
  // particular code, though, we're avoiding CSS
  // to make it easy to extract the resulting SVG
  // and paste it into a presentation.
  svg.selectAll(".axis line, .axis path")
       .attr("fill", "none")
       .attr("stroke", "#bbbbbb")
       .attr("stroke-width", "2px")
       .attr("shape-rendering", "crispEdges");

  svg.selectAll(".axis text")
       .attr("font-size", "18");

  svg.selectAll(".axis .tick line")
       .attr("stroke", "#d0d0d0")
       .attr("stroke-width", "1");

  // Plot the data and the legend
  datasets.forEach(function(dataset, i) {

      // Individual points
      svg.selectAll(".point.dataset-" + i)
           .data(dataset.data)
         .enter().append("path")
           .attr("class", "point dataset-" + i)
           .attr("fill", color(i))
           .attr("stroke", color(i))
           .attr("d", symbol(i))
           .attr("transform", function(d) {
             return "translate(" + x(d.date) +
                         "," + y(d.temp) + ")";
           });

      // Connect the points with lines
      svg.append("path")
         .datum(dataset.data)
         .attr("class", "line dataset-" + i)
         .attr("fill", "none")
         .attr("stroke", color(i))
         .attr("stroke-width", "3")
         .attr("d", line);

      // Legend. In general, it would be cleaner
      // to create an SVG group for the legend,
      // position that group, and then position
      // the individual elements of the legend
      // relative to the group. We're not doing
      // it in this case because we want to do
      // some fancy animation tricks with the
      // resulting SVG within the presentation.
      d3.select("svg").append("path")
         .attr("class", "point dataset-" + i)
         .attr("fill", color(i))
         .attr("stroke", color(i))
         .attr("d", symbol(i))
         .attr("transform", "translate(" +
           (margin.left + width + 40) + "," +
           (24*i + margin.top + height/2 -
            24*datasets.length/2 - 6) + ")")

      d3.select("svg").append("line")
         .attr("class", "line dataset-" + i)
         .attr("stroke", color(i))
         .attr("stroke-width", "3")
         .attr("x1", margin.left + width + 30)
         .attr("x2", margin.left + width + 50)
         .attr("y1", 24*i + margin.top + height/2 -
                     24*datasets.length/2 - 6)
         .attr("y2", 24*i + margin.top + height/2 -
                     24*datasets.length/2 - 6);

      d3.select("svg").append("text")
         .attr("transform", "translate(" +
           (margin.left + width + 60) + "," +
           (24*i + margin.top + height/2 -
            24*datasets.length/2) + ")")
         .attr("class", "legend")
         .attr("font-size", "18")
         .attr("text-anchor", "left")
         .text(dataset.name);

  });

  // Chart decoration. Once more we're avoiding
  // CSS for styling, but usually that would be
  // a better approach.
  d3.select("svg").append("text")
       .attr("transform", "translate(" +
         (margin.left + width/2 + 20) + ",20)")
       .attr("class", "title")
       .attr("font-size", "24")
       .attr("text-anchor", "middle")
       .text("Monthly Average Temperature");

  d3.select("svg").append("text")
       .attr("transform", "translate(" +
         (margin.left + width/2 + 20) + ",48)")
       .attr("class", "subtitle")
       .attr("font-size", "18")
       .attr("text-anchor", "middle")
       .text("Source: WorldClimate.com");
});

But with D3 We Can Do More

JanFebMarAprMayJunJulAugSepOctNovDec0510152025Temperature (°C)TokyoNew YorkBerlinLondonMonthly Average TemperatureSource: WorldClimate.com

(That Animation was D3)

d3.selectAll(".point")
  .transition()
  .duration(2000)
  .ease("bounce")
  .attr("transform", function(d) {
    return "translate(" + x(d.date) + "," +
      (height - margin.top - margin.bottom - 10) + ")";
  })
  .remove();

But with D3 We Can Do More

JanFebMarAprMayJunJulAugSepOctNovDec0510152025Temperature (°C)TokyoNew YorkBerlinLondonMonthly Average TemperatureSource: WorldClimate.com

Small Addition to the Line Function

1
2
3
4
5
6
7
8
9
10
11
svg.append("path")
    .datum(dataset.data)
    .attr("fill", "none")
    .attr("stroke", color(i))
    .attr("stroke-width", "3")
    .attr("d",
        d3.svg.line()
            .interpolate("basis")
            .x(function(d) { return x(d.date); })
            .y(function(d) { return y(d.temp); })
    );

Why Bother with Monthly Values?

JanFebMarAprMayJunJulAugSepOctNovDec2030405060708090Temperature (°F)Average Daily Temperature - AtlantaSource: www.noaa.gov

Maps are Useful Also

Conventions are not Constraints

More Information