D3.js Step by Step
From Zero to Data Viz Hero
Kent English
@kentenglish
The deck:
slides.com/zeroviscosity/d3-js-step-by-step
The blog post version:
zeroviscosity.com (or just zevi.co)
Data-Driven Documents

D3 can be daunting
But don't panic

Take it step by step
We'll go from this:

To this (time permitting):
Steps:
-
A Basic Pie Chart
-
A Basic Donut Chart
-
Adding a Legend
-
Loading External Data
-
Adding Tooltips
-
Animating Interactivty
Step 1: A Basic Pie Chart

<div id="chart">
<svg width="360" height="360">
<g transform="translate(180,180)">
<path d="M0,-180A..." fill="#393b79"></path>
<path d="M105.801..." fill="#5254a3"></path>
<path d="M171.190..." fill="#6b6ecf"></path>
<path d="M-105.80..." fill="#9c9ede"></path>
</g>
</svg>
</div>

Our dummy dataset:
var dataset = [
{ label: 'Abulia', count: 10 },
{ label: 'Betelgeuse', count: 20 },
{ label: 'Cantaloupe', count: 30 },
{ label: 'Dijkstra', count: 40 }
];
Some dimensions:
var width = 360;
var height = 360;
var radius = Math.min(width, height) / 2;
Colour Scale:
var color = d3.scale.category20b();
// Alternative
var color = d3.scale.ordinal()
.range(['#A60F2B', '#648C85', '#B3F2C9', '#528C18']);
Setting up the SVG:
var svg = d3.select('#chart')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' +
(width / 2) + ',' + (height / 2) + ')');
var svg = d3.select('#chart')
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' +
(width / 2) + ',' + (height / 2) + ')');
<div id="chart">
<svg width="360" height="360">
<g transform="translate(180,180)">
<path d="M0,-180A..." fill="#393b79"></path>
<path d="M105.801..." fill="#5254a3"></path>
<path d="M171.190..." fill="#6b6ecf"></path>
<path d="M-105.80..." fill="#9c9ede"></path>
</g>
</svg>
</div>
Now for the pie segments
We need two things:
-
The radius
-
The angles
Defining the radius:
var arc = d3.svg.arc()
.outerRadius(radius);
Defining the angles:
var pie = d3.layout.pie()
.value(function(d) { return d.count; })
.sort(null);
And now... drum roll, please...

Adding the path elements:
var path = svg.selectAll('path')
.data(pie(dataset))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d, i) {
return color(d.data.label);
});
This looks harder than it is

// Select 'path' elements (these don't currently exist)
var path = svg.selectAll('path')
// Assign our 'pie' version of the dataset
.data(pie(dataset))
// Create placeholder elements for each data point
.enter()
// Replace placeholders with 'path' elements
.append('path')
// Assign our 'arc' to the 'd' attribute
.attr('d', arc)
// Define how to determine the 'fill'
.attr('fill', function(d, i) {
// Use the colour scale we defined earlier
return color(d.data.label);
});
This is a pattern you'll see again and again in D3 examples.
svg.selectAll(path)
.data(dataset)
.enter()
.append(path)
Step 2: A Basic Donut Chart



How can we do this?
First define a width:
var donutWidth = 75;
Then define an innerWidth():
var arc = d3.svg.arc()
.innerRadius(radius - donutWidth) // NEW
.outerRadius(radius);
Ta da! We're done.
Easy mode, right?

Step 3: Adding a Legend

We want this:

Each entry in the legend looks like this:
<g class="legend" transform="translate(-36,-44)">
<rect width="18" height="18"
style="fill: ... stroke: ..."></rect>
<text x="22" y="14">Abulia</text>
</g>
We haven't used CSS yet...

Our first CSS:
.legend {
font-size: 12px;
}
rect {
stroke-width: 2;
}
(And this isn't strictly necessary)
Now for some sizes:
var legendRectSize = 18;
var legendSpacing = 4;
We're going to see this
pattern again:
svg.selectAll(path)
.data(dataset)
.enter()
.append(path)
Adding the g elements:
var legend = svg.selectAll('.legend')
.data(color.domain())
.enter()
.append('g')
.attr('class', 'legend')
.attr('transform', function(d, i) {
// Some code to center the legend
});
Centering the legend:
var legend = svg.selectAll('.legend')
// ...
// More code
// ...
.attr('transform', function(d, i) {
var height = legendRectSize + legendSpacing;
var offset = height * color.domain().length / 2;
var horz = -2 * legendRectSize;
var vert = i * height - offset;
return 'translate(' + horz + ',' + vert + ')';
});
Adding the rect elements:
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', color)
.style('stroke', color);
Adding the text elements:
legend.append('text')
.attr('x', legendRectSize + legendSpacing)
.attr('y', legendRectSize - legendSpacing)
.text(function(d) { return d; });
Step 4: Loading External Data

Our dataset is rather contrived.
Time for something more realistic.
Toronto Parking Ticket Data

Our new dataset:
label,count
Monday,379130
Tuesday,424923
Wednesday,430728
Thursday,432138
Friday,428295
Saturday,368239
Sunday,282701
What we'll end up with:

Just wrap any dataset-dependent code in a callback to d3.csv():
// Code that doesn't depend on dataset
d3.csv('weekdays.csv', function(error, dataset) {
// Code that depends on dataset
// Like the definition of 'path' and 'legend'
});
There is also:
-
d3.tsv()
-
d3.dsv()
One thing: count needs to be a number
// Code that doesn't depend on dataset
d3.csv('weekdays.csv', function(error, dataset) {
dataset.forEach(function(d) {
d.count = +d.count; // Cast to number
});
// Code that depends on dataset
});
And that's it.

Step 5: Adding Tooltips

We'll harness mouse events
We'll need a bit of CSS
First, for the container:
#chart {
height: 360px;
position: relative;
width: 360px;
}
Some tooltip styles:
.tooltip {
background: #eee;
box-shadow: 0 0 5px #999999;
color: #333;
display: none;
font-size: 12px;
left: 130px;
padding: 10px;
position: absolute;
text-align: center;
top: 95px;
width: 80px;
z-index: 10;
}
Create the tooltip:
var tooltip = d3.select('#chart')
.append('div')
.attr('class', 'tooltip');
tooltip.append('div')
.attr('class', 'label');
tooltip.append('div')
.attr('class', 'count');
tooltip.append('div')
.attr('class', 'percent');
Event handlers:
var path = svg.selectAll('path')
// Blah blah blah
path.on('mouseover', function(d) {
// Code
});
path.on('mouseout', function(d) {
// Code
});
path.on('mouseover', function(d) {
var total = d3.sum(dataset.map(function(d) {
return d.count;
}));
var percent = Math.round(1000 * d.data.count / total) / 10;
tooltip.select('.label').html(d.data.label);
tooltip.select('.count').html(d.data.count);
tooltip.select('.percent').html(percent + '%');
tooltip.style('display', 'block');
});
Mouseover:
path.on('mouseout', function() {
tooltip.style('display', 'none');
});
Mouseout:
// OPTIONAL
path.on('mousemove', function(d) {
tooltip.style('top', (d3.event.pageY + 10) + 'px')
.style('left', (d3.event.pageX + 10) + 'px');
});
(Optionally) mousemove:

Our tooltip will follow the cursor
until we leave the segment
Step 6: Animating Interactivity

What we want:

Animation is hard
D3 is here to help
A bit more CSS:
rect {
cursor: pointer;
stroke-width: 2;
}
rect.disabled {
fill: transparent !important;
}
Add an enabled property:
d3.csv('weekdays.csv', function(error, dataset) {
dataset.forEach(function(d) {
d.count = +d.count;
d.enabled = true; // NEW
});
// ...
// More code
// ...
Add a _current property:
var path = svg.selectAll('path')
.data(pie(dataset))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d, i) {
return color(d.data.label);
})
.each(function(d) { this._current = d; }); // NEW
Change our total calculation:
path.on('mouseover', function(d) {
var total = d3.sum(dataset.map(function(d) {
return (d.enabled) ? d.count : 0; // UPDATED
}));
// ...
// More code
// ...
Now that the house
keeping is out of the way

React to click events:
legend.append('rect')
.attr('width', legendRectSize)
.attr('height', legendRectSize)
.style('fill', color)
.style('stroke', color)
.on('click', function(label) { // NEW
// ...
// A bunch of code
// ...
}); // NEW
// ...
// More code
// ...
.on('click', function(label) {
var rect = d3.select(this);
var enabled = true;
var totalEnabled = d3.sum(dataset.map(function(d) {
return (d.enabled) ? 1 : 0;
}));
if (rect.attr('class') === 'disabled') {
rect.attr('class', '');
} else {
if (totalEnabled < 2) return;
rect.attr('class', 'disabled');
enabled = false;
}
// ...
// More code
// ...
// ...
// More code
// ...
pie.value(function(d) {
if (d.label === label) d.enabled = enabled;
return (d.enabled) ? d.count : 0;
});
path = path.data(pie(dataset));
path.transition()
.duration(750)
.attrTween('d', function(d) {
var interpolate = d3.interpolate(this._current, d);
this._current = interpolate(0);
return function(t) {
return arc(interpolate(t));
};
});
});
And we're done!

Thanks!
The deck:
slides.com/zeroviscosity/d3-js-step-by-step
The blog post version:
zeroviscosity.com (or just zevi.co)
D3.js Step by Step From Zero to Data Viz Hero
D3.js Step by Step
By zeroviscosity
D3.js Step by Step
Presented at Web Unleashed 2014 in Toronto
- 7,155