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
By zeroviscosity
D3.js Step by Step
Presented at Web Unleashed 2014 in Toronto
- 7,010