Creating D3 components
a journey of pain, joy, frustration and enlightenment
Chris Price / @100pxls
d3
data join
components
our experience
d3
birds-eye view
DOM abstraction
selection
var selection = d3.select('#id');
selection.select('div')
.selectAll('.class-name');
DOM abstraction
manipulation
selection.text('inner text')
.attr('class', 'class-name')
.style({
height: 'auto'
})
.each(function(data) {
var element = this;
// ...
})
.append('span');
DOM abstraction
events
selection.on('click.foo',
function(data) {
var element = this;
// ...
});
DOM abstraction
XHR
d3.json('/some/url',
function(error, data) {
// ...
});
DOM abstraction
animation
selection.style('background', 'blue')
.transition()
.delay(100)
.ease('linear')
.style('background', 'red');
layout algorithms
scales
var scale = d3.scale.linear()
.domain([-10, 10])
.range([0, 500]);
scale(-5); // 125
scale.invert(125); // -5
scale.ticks(3); // [-10, 0, 10]
layout algorithms
specialised layouts
var layout = d3.layout.pie()
.padAngle(Math.PI/10);
layout([3,1,2]);
// [ {
// "data":3,
// "value":3,
// "padAngle":0.314...,
// "startAngle":0,
// "endAngle":2.984...
// }, ... ]
layout algorithms
geometric primitives
var polygon = d3.geom.polygon(
[[0,0],[0,1],[1,1],[1,0]]);
polygon.area(); // 1
polygon.centroid(); // [0.5,0.5]
layout algorithms
geographic projections
var mercator = d3.geo.projection(
function(λ, φ) {
return [
λ,
Math.log(Math.tan(π / 4 + φ / 2))
];
});
SVG helpers
axis renderers
var scale = d3.linear.scale()
.domain([-10, 10])
.range([0, 100]);
var axis = d3.svg.axis()
.scale(scale);
d3.select('svg')
.call(axis);
SVG helpers
path data generators
var line = d3.svg.line();
line([[0,0],[50,50]]);
// "M0,0L50,50"
d3.select('svg')
.append('path')
.datum([[0,0],[50,50]])
.attr('d', line);
// <path d="M0,0L50,50"></path>
miscellaneous
data/time formatting
var format = d3.time.format("%a, %e %B %Y");
format(new Date(2015, 4, 7));
// "Thu, 7 May 2015"
var d = format.parse("Fri, 8 May 2015");
d.toString();
// "Fri May 08 2015 00:00:00 GMT+0000 (GMT)"
d3.time.year.ceil(d, 1);
// "Fri Jan 01 2016 00:00:00 GMT+0000 (GMT)"
examples
d3
- DOM abstraction
- layout algorithms
- SVG helpers
- miscellaneous
- data binding
data join
contains spoilers
data join
the HTML
<h1>USS Missouri Crew List</h1>
<ul>
<li></li>
<li></li>
<li></li>
</ul>
data join
the data
var data = [
{
name: 'Adams', rank: 'Captain'
},
{
name: 'Krill', rank: 'Commander'
},
{
name: 'Ryback', rank: 'Chief Petty Officer'
}
];
data join
render loop
// ...
function render() {
requestAnimationFrame(render);
}
requestAnimationFrame(render);
data join
selection
// ...
function render() {
var container = d3.select('ul');
container.selectAll('li');
requestAnimationFrame(render);
}
// ...
data join
the join
// ...
var container = d3.select('ul');
container.selectAll('li')
.data(data);
requestAnimationFrame(render);
// ...
data join
update selection
// ...
var updateSelection = container.selectAll('li')
.data(data);
updateSelection.text(function(d) {
return d.name + ' (' + d.rank + ')';
});
// ...
data join
update selection
<h1>USS Missouri Crew List</h1>
<ul>
<li>Adams (Captain)</li>
<li>Krill (Commander)</li>
<li>Ryback (Chief Petty Officer)</li>
</ul>
data join
add an item to the data
var data = [
{
name: 'Adams', rank: 'Captain'
},
{
name: 'Krill', rank: 'Commander'
},
{
name: 'Ryback', rank: 'Chief Petty Officer'
},
{
name: 'Strannix', rank: 'Civilian'
}
];
data join
...no change
<h1>USS Missouri Crew List</h1>
<ul>
<li>Adams (Captain)</li>
<li>Krill (Commander)</li>
<li>Ryback (Chief Petty Officer)</li>
</ul>
data join
enter selection
// ...
var updateSelection = container.selectAll('li')
.data(data);
updateSelection.enter()
.append('li');
updateSelection.text(function(d) {
return d.name + ' (' + d.rank + ')';
});
// ...
data join
a change!
<h1>USS Missouri Crew List</h1>
<ul>
<li>Adams (Captain)</li>
<li>Krill (Commander)</li>
<li>Ryback (Chief Petty Officer)</li>
<li>Strannix (Civilian)</li>
</ul>
data join
remove an item from the data
var data = [
{
name: 'Krill', rank: 'Commander'
},
{
name: 'Ryback', rank: 'Chief Petty Officer'
},
{
name: 'Strannix', rank: 'Civilian'
}
];
data join
a change...
<h1>USS Missouri Crew List</h1>
<ul>
<li>Krill (Commander)</li>
<li>Ryback (Chief Petty Officer)</li>
<li>Strannix (Civilian)</li>
<li>Strannix (Civilian)</li>
</ul>
data join
exit selection
// ...
updateSelection.enter()
.append('li');
updateSelection.text(function(d) {
return d.name + ' (' + d.rank + ')';
});
updateSelection.exit()
.remove();
// ...
data join
a change!
<h1>USS Missouri Crew List</h1>
<ul>
<li>Krill (Commander)</li>
<li>Ryback (Chief Petty Officer)</li>
<li>Strannix (Civilian)</li>
</ul>
data join
spoilers
<h1>USS Missouri Crew List</h1>
<ul>
<li>Ryback (Chief Petty Officer)</li>
</ul>
var data = [
{
name: 'Ryback', rank: 'Chief Petty Officer'
}
];
data join
- join is configurable
- index-based (default)
- identity-based
- custom
- values stored directly on DOM node
- accessible using selection.datum()
- idempotent transformation of data into nodes
demo
components
(functions)
components
just functions
function caseyRyback(selection) {
selection.text('Another cold day in Hell.');
}
function dramaticEffect(selection) {
selection.style('fontWeight', 'bold');
}
var selection = d3.select('span');
caseyRyback(selection);
dramaticEffect(selection);
components
invoked with call
function caseyRyback(selection) {
selection.text('Another cold day in Hell.');
}
function dramaticEffect(selection) {
selection.style('fontWeight', 'bold');
}
var selection = d3.select('span')
.call(caseyRyback)
.call(dramaticEffect);
components
factories
function caseyRybackFactory() {
function caseyRyback(selection) {
selection.text('Another cold day in Hell.');
}
return caseyRyback;
}
var component = caseyRybackFactory();
d3.select('span')
.call(component);
components
configurable
function caseyRybackFactory() {
var quote = 'Another cold day in Hell.';
function caseyRyback(selection) {
selection.text(quote);
}
caseyRyback.quote = function(value) {
if (!arguments.length) { return quote; }
quote = value;
return caseyRyback;
};
return caseyRyback;
}
components
configurable
var component = caseyRybackFactory()
.quote('Keep the faith, Strannix.');
d3.select('span')
.call(component);
components
- convention based unit of re-use
- invocation is idempotent
- applicable to multi-node selections
demo
our experience
8 rules
#1
use data join in components
- use each for re-usability
- is enter or update most appropriate?
#2
singleton elements
- default join behaviour is index based
- wrap data in an array
var circle = container.selectAll('circle')
.data([data]);
circle.enter()
.append('circle')
.attr('r', 50);
#3
avoid g soup
- don't create container elements
- callers pass in the container
- container won't be re-used
#4
no state in components
- prevents re-usability
#5
separate concerns
- extract algorithmic complexity into separate components
#6
use d3.rebind
- selectively expose configuration of nested components
function componentFactory() {
function component(selection) {
// ...
}
d3.rebind(component, strober,
'oddColor', 'evenColor');
return component;
}
var component = componentFactory();
d3.select('rect')
.oddColor('pink')
.evenColor('white')
.call(component);
#7
namespace your events
- only one handler per type
- idempotency!
#8
expose your own events
- allow interactions to propagate outside of your component
function componentFactory() {
var event = d3.dispatch("customevent");
function component(selection) {
selection.each(function(data) {
d3.select(this)
.on('click.component', function() {
event.customevent.apply(
this, arguments);
});
});
}
d3.rebind(component, event, 'on');
return component;
}
our experience
- keep it small and simple
- refer back to how d3 itself works
- check you're not re-creating d3/SVG functionality
our library
d3fc
d3fc
- a toolkit for rapidly developing bespoke charts
- embraces d3
- small and composable
- prioritises simplicity
- a curated and consistent set of examples
Creating D3 components
Chris Price / @100pxls
d3fc.io
slides.com/chrisprice/creating-d3-components-1
Creating D3 components - TechStock
By Chris Price
Creating D3 components - TechStock
The examples that went along with this deck are available on GitHub - https://github.com/chrisprice/creating-d3-components
- 1,364