Data-Driven Documents
// Select a single element
d3.select("h1").style("font-size", "3em");
// Select multiple elements
d3.selectAll("p").style("color", "white");
// Select SVG elements
d3.selectAll("circle").attr("r", 25);
// Bind data and use it to set each element's background color
d3.selectAll("div")
.data(["red", "#00FF00", "rgb(0,0,255)"])
.style('background-color', function(d) { return d; });
// Let the text content of each element depend on the data
d3.selectAll("p")
.data([1, 2, 3, 4, 5])
.text(function(d) { return "Number: " + d; });
// Bind more complex data, and use different properties
var circles = [
{ radius: 10, center: [5, 20], color: "red" },
{ radius: 5, center: [20, 10], color: "blue" },
{ radius: 15, center: [30, 30], color: "green" }
];
d3.selectAll("circle")
.data(circles)
.attr("r", function(d) { return d.radius; })
.attr("cx", function(d) { return d.center[0]; })
.attr("cy", function(d) { return d.center[1]; })
.attr("fill", function(d) { return d.color; });
function update(data) {
// Join
var p = d3.select("body").selectAll("p").data(data);
// Enter
p.enter().append("p");
// Enter + Update
p.text(function (d) { return "Number: " + d; });
// Exit
p.exit().remove();
}
d3.select("button").on("click", function () {
d3.select("body").transition()
.duration(500)
.style("background-color", "black");
});
var scale = d3.scale.linear()
.domain([0, 600]). // the min/max extent of your data
.range([0, 300]); // the pixel range to map that to
scale(0); // => 0
scale(100); // => 50
Let's use that scale to create an SVG axis...
var axis = d3.svg.axis()
.scale(scale)
.orient("bottom")
.ticks(5);
d3.select("#chart").append("g")
.call(axis);
...and it took wayyy too long to build.
D3 is a library, not a framework
Alex Graul & Irene Ros from...
...joined forces to make data-in-the-browser easier.
This partnership was called...
...which has spawned some great libraries, including...
"...a framework for building reusable charts with d3.js"
// Define a new chart type: a circle chart
d3.chart("CircleChart", {
initialize: function () { /* ... */ }
});
// Create an instance of the chart on a d3 selection
var chart = d3.select("body")
.append("svg")
.attr("height", 30)
.attr("width", 400)
.chart("CircleChart");
// Render it with some data
chart.draw([1,4,6,9,12,13,30]);
initialize: function () {
var chart = this;
chart.w = this.base.attr('width') || 200;
chart.h = this.base.attr('height') || 150;
chart.scale = d3.scale.linear().range([0, chart.w]);
/* ... */
}
initialize: function () {
/* ... */
// Create a 'layer' for the circles
chart.layer("circles", chart.base.append("g"), {
// Select the elements and bind the data to them.
dataBind: function (data) {
chart.data = data;
chart.scale.domain([0, d3.max(data)]);
return this.selectAll("circle")
.data(data);
},
// Insert actual circles
insert: function () {
return this.append("circle");
},
// Define lifecycle events
events: {
// Paint new elements, but set their radius to 0
"enter": function() {
return this.attr("cx", function(d) {
return d * 10;
})
.attr("cy", 10)
.attr("r", 0);
},
// ...then transition them to a radius of 5
"enter:transition": function() {
return this
.delay(500)
.attr("r", 5);
},
// Before removing circles, transition
// their radius back to 0
"exit:transition": function() {
return this.duration(1000)
.attr("r", 0)
.remove();
}
}
});
}
When we call draw
with some data, it gets passed through each layer you've defined (lines, bars, axes, labels, etc.).
Each layer knows how to:
chart.width(200).margin(10).circleRadius(10);
d3.chart('CircleChart', {
initialize: function () { /* ... */ },
width : function(newWidth) {
// Getter
if (!arguments.length) { return this.w; }
// Setter + side effects
this.w = newWidth;
this.base.attr('width', this.w); // svg
this.scale = d3.scale.linear().range([0, this.w]); // scale
this.draw(this.data); // redraw
return this; // chaining
},
/* ... (margin, circleRadius, etc.) ... */
});
So we've got some data to play with. Yay.
(Demo)
chart.areas = {
labelsXTop: chart.base.append('g').classed('OrdinalScatterPlot_labels OrdinalScatterPlot_labels-x OrdinalScatterPlot_labels-x-top', true),
labelsXBottom: chart.base.append('g').classed('OrdinalScatterPlot_labels OrdinalScatterPlot_labels-x OrdinalScatterPlot_labels-x-bottom', true),
labelsY: chart.base.append('g').classed('OrdinalScatterPlot_labels OrdinalScatterPlot_labels-y', true),
background: chart.base.append('g').classed('OrdinalScatterPlot_background', true),
gridX: chart.base.append('g').classed('OrdinalScatterPlot_grid OrdinalScatterPlot_grid-x', true),
gridY: chart.base.append('g').classed('OrdinalScatterPlot_grid OrdinalScatterPlot_grid-y', true),
border: chart.base.append('rect').classed('OrdinalScatterPlot_border', true),
points: chart.base.append('g').classed('OrdinalScatterPlot_points', true),
legend: chart.base.append('g').classed('OrdinalScatterPlot_legend', true),
title: chart.base.append('g').classed('OrdinalScatterPlot_title', true)
};
Each area has a corresponding layer
d3.chart('OrdinalScatterPlot', {
// Expected datum properties
dataAttrs: ['metric', 'value', 'series'],
// Initialisation (+ settings, areas, layers, events)
initialize: function() { /* ... */ }
// Fluent API
// - Chart Size
width: function (width, excludesDecorations) { /* ... */ },
height: function (height, excludesDecorations) { /* ... */ },
// - Metric labels (y-axis)
metricLabelsWidth: function (width, maintainsTotalWidth) { /* ... */ },
// - Control dot shape & colour (based on series)
seriesScales: function (names, colors, symbols) { /* ... */ },
// - Legend
showLegend: function (maintainsTotalHeight) { /* ... */ },
hideLegend: function (maintainsTotalHeight) { /* ... */ },
// - Title
title: function (title, subtitle) { /* ... */ },
showTitle: function (maintainsTotalHeight) { /* ... */ },
hideTitle: function (maintainsTotalHeight) { /* ... */ },
// - Value labels (x-axis)
showLabelsX: function (maintainsTotalHeight) { /* ... */ },
hideLabelsX: function (maintainsTotalHeight) { /* ... */ },
showLabelsXTop: function (maintainsTotalHeight) { /* ... */ },
hideLabelsXTop: function (maintainsTotalHeight) { /* ... */ },
showLabelsXBottom: function (maintainsTotalHeight) { /* ... */ },
hideLabelsXBottom: function (maintainsTotalHeight) { /* ... */ },
formatLabelsX: function (formatFn) { /* ... */ },
valueDomain: function(min, max) { /* ... */ },
// - Tooltips
formatTooltips: function (formatFn) { /* ... */ },
_defaultTooltipFormat: function (d) { /* ... */ },
// - Dot animation toggle
animated: function (isAnimated) { /* ... */ },
// - Linked chart behaviours
linkSymbiote: function(symbiote) { /* ... */ },
// Fluent API internal functions
// - Scales
_updateXScale: function () { /* ... */ },
_updateYScale: function () { /* ... */ },
// - Area size measurement
_effectiveXDecorationsWidth: function () { /* ... */ },
_effectiveLeftDecorationsWidth: function () { /* ... */ },
_effectiveRightDecorationsWidth: function () { /* ... */ },
_effectiveYDecorationsHeight: function () { /* ... */ },
_effectiveTopDecorationsHeight: function () { /* ... */ },
_effectiveBottomDecorationsHeight: function () { /* ... */ },
_effectiveLegendHeight: function () { /* ... */ },
_effectiveTitleHeight: function () { /* ... */ },
_effectiveLabelsXTopHeight: function () { /* ... */ },
_effectiveLabelsXBottomHeight: function () { /* ... */ },
// - Area size/position management
_updateAreasDisplay: function () { /* ... */ }
});
The same chart can be used to display
different sets of data.
We want the x-axis labels to make sense...
chart.formatLabelsX(function (x) { return x + '%'; });
chart
.formatLabelsX(function (x) {
var label = '';
if (x > 0.9) {
label += 'More left-leaning ←';
} else if (-0.9 > x) {
label += '→ More right-leaning';
}
return label;
})
.formatTooltips(function (d) {
return 'Held by ' + d.series;
});
First we define a function that constructs a chart with all the shared properties/defaults
function makeChart() {
return d3.select('#chart')
.append("div")
.append("svg")
.chart("OrdinalScatterPlot")
.width(700)
.height(44, true)
.valueDomain(0, 10)
.hideLegend()
.hideLabelsX()
.animated(true);
}
Then use it as a starting point for each part
var charts = [
makeChart().title('Kevin Rudd', 'Labor').showLegend(), // 1
makeChart().height(88, true).showLabelsX(), // 2
makeChart().title('Tony Abbott', 'Coalition'), // 3
makeChart().height(88, true).showLabelsX(), // 4
makeChart().title('Christine Milne', 'Greens'), // 5
makeChart().height(88, true).showLabelsX() // 6
];
Data can then be filtered by metric and passed to the corresponding chart
You may have noticed we gave every layer and child SVG elements their own distingushing CSS classes.
chart.areas = {
labelsXTop: chart.base.append('g').classed('OrdinalScatterPlot_labels OrdinalScatterPlot_labels-x OrdinalScatterPlot_labels-x-top', true),
labelsXBottom: chart.base.append('g').classed('OrdinalScatterPlot_labels OrdinalScatterPlot_labels-x OrdinalScatterPlot_labels-x-bottom', true),
labelsY: chart.base.append('g').classed('OrdinalScatterPlot_labels OrdinalScatterPlot_labels-y', true),
// ...
title: chart.base.append('g').classed('OrdinalScatterPlot_title', true)
};
This is what gives us flexibility with re-styling
By appending unique classes to some of the charts, we can style them differently:
charts[0].base.classed('ptyred', true);
charts[2].base.classed('ptyblue', true);
charts[4].base.classed('ptylightgreen', true);
.ptyred .OrdinalScatterPlot_labels-y .tick:first-child { font-weight: bold; }
.ptyred .OrdinalScatterPlot_subtitle { fill: #BE4848; }
.ptyred .OrdinalScatterPlot_border { stroke: #E7CACA; }
.ptyred .OrdinalScatterPlot_background .tick { stroke: #EFDCDC; }
.ptyred .OrdinalScatterPlot_background .tick:nth-child(even) { stroke: #F7EEEE; }
/* ...and similar CSS for .ptyblue & .ptylightgreen */
This is applicable not just to per-element style-overrides, but for theming entire charts for your own environment.
// Link charts as symbiotes
charts.forEach(function (a) {
charts.forEach(function (b) {
if (a !== b) {
a.linkSymbiote(b);
}
});
});
// In one of the dot mouseover event handlers...
onActivateDot = function (d) {
function activateDot(chart) { /* ... */ }
activateDot(chart);
chart.symbiotes.forEach(activateDot);
};
window.addEventListener("resize", function () {
var width = $(chart.base.node().parentNode).width();
chart.width(width);
}, false);
function debounce(fn, wait) {
var timeout;
return function () {
var context = this, // preserve context
args = arguments, // preserve arguments
later = function () { // define a function that:
timeout = null; // * nulls the timeout (GC)
fn.apply(context, args); // * calls the original fn
};
// (re)set the timer which delays the function call
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
Using debounce, the handler will now only run once window's resize event stops getting hammered for 250ms:
window.addEventListener("resize", debounce(function () {
var width = $(chart.base.node().parentNode).width();
chart.width(width);
}, 250), false);
if (document.body.requestFullscreen ||
document.body.mozRequestFullScreen ||
document.body.webkitRequestFullscreen
) {
$("")
.on('click', function (e) {
var root = $interactive.get(0);
if (root.requestFullscreen) { root.requestFullscreen(); }
else if (root.mozRequestFullScreen) { root.mozRequestFullScreen(); }
else if (root.webkitRequestFullscreen) { root.webkitRequestFullscreen(); }
e.preventDefault();
})
.appendTo($interactive);
}
.interactive:full-screen {
width: 100%;
height: 100%;
overflow-y: scroll;
}