Creating D3 components
a journey of pain, joy, frustration and enlightenment
Chris Price / @100pxls
d3
data join
components
our experience
d3
in 2 minutes
DOM abstraction
var selection = d3.select('#id');
selection.select('div')
.selectAll('.class-name');
- selection
- manipulation
- events
- XHR
- transitions
selection.text('inner text')
.attr('class', 'class-name')
.style({
height: 'auto'
})
.each(function(data) {
var element = this;
// ...
})
.append('span');
selection.on('click.foo', function(data) {
var element = this;
// ...
});
d3.json('/some/url', function(error, data) {
// ...
});
selection.style('background', 'blue')
.transition()
.delay(100)
.ease('linear')
.style('background', 'red');
layout algorithms
var scale = d3.scale.linear()
.domain([-10, 10])
.range([0, 500]);
scale(-5); // 125
scale.invert(125); // -5
scale.ticks(3); // [-10, 0, 10]
- scales
- specialised layouts
- geometric primitives
- geographic projections
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...
// }, ... ]
var polygon = d3.geom.polygon(
[[0,0],[0,1],[1,1],[1,0]]);
polygon.area(); // 1
polygon.centroid(); // [0.5,0.5]
SVG helpers and misc.
- SVG helpers
- time formatting
- and more...
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)"
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>
var scale = d3.linear.scale()
.domain([-10, 10])
.range([0, 100]);
var axis = d3.svg.axis()
.scale(scale);
d3.select('svg')
.call(axis);
d3
- data binding
- DOM abstraction
- layout algorithms
- SVG helpers
- miscellaneous
data join
contains spoilers
data join
var data = [
{ name: 'Adams', rank: 'Captain' },
{ name: 'Krill', rank: 'Commander' },
{ name: 'Ryback', rank: 'Chief Petty Officer' }
];
<h1>USS Missouri Crew List</h1>
<ul>
<li></li>
<li></li>
<li></li>
</ul>
data join
var data = [
{ name: 'Adams', rank: 'Captain' },
{ name: 'Krill', rank: 'Commander' },
{ name: 'Ryback', rank: 'Chief Petty Officer' }
];
function render() {
requestAnimationFrame(render);
}
requestAnimationFrame(render);
<h1>USS Missouri Crew List</h1>
<ul>
<li></li>
<li></li>
<li></li>
</ul>
- render loop
data join
var data = [
{ name: 'Adams', rank: 'Captain' },
{ name: 'Krill', rank: 'Commander' },
{ name: 'Ryback', rank: 'Chief Petty Officer' }
];
function render() {
var container = d3.select('ul');
container.selectAll('li');
requestAnimationFrame(render);
}
requestAnimationFrame(render);
<h1>USS Missouri Crew List</h1>
<ul>
<li></li>
<li></li>
<li></li>
</ul>
- render loop
- selection
data join
var data = [
{ name: 'Adams', rank: 'Captain' },
{ name: 'Krill', rank: 'Commander' },
{ name: 'Ryback', rank: 'Chief Petty Officer' }
];
function render() {
var container = d3.select('ul');
container.selectAll('li')
.data(data);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
<h1>USS Missouri Crew List</h1>
<ul>
<li></li>
<li></li>
<li></li>
</ul>
- render loop
- selection
- data
data join
var data = [
{ name: 'Adams', rank: 'Captain' },
{ name: 'Krill', rank: 'Commander' },
{ name: 'Ryback', rank: 'Chief Petty Officer' }
];
function render() {
var container = d3.select('ul');
var updateSelection = container.selectAll('li')
.data(data);
updateSelection.text(function(d) {
return d.name + ' (' + d.rank + ')';
});
requestAnimationFrame(render);
}
requestAnimationFrame(render);
<h1>USS Missouri Crew List</h1>
<ul>
<li>Adams (Captain)</li>
<li>Krill (Commander)</li>
<li>Ryback (Chief Petty Officer)</li>
</ul>
- render loop
- selection
- data
- update selection
data join
var data = [
{ name: 'Adams', rank: 'Captain' },
{ name: 'Krill', rank: 'Commander' },
{ name: 'Ryback', rank: 'Chief Petty Officer' },
{ name: 'Strannix', rank: 'Civilian' }
];
function render() {
var container = d3.select('ul');
var updateSelection = container.selectAll('li')
.data(data);
updateSelection.text(function(d) {
return d.name + ' (' + d.rank + ')';
});
requestAnimationFrame(render);
}
requestAnimationFrame(render);
<h1>USS Missouri Crew List</h1>
<ul>
<li>Adams (Captain)</li>
<li>Krill (Commander)</li>
<li>Ryback (Chief Petty Officer)</li>
</ul>
- render loop
- selection
- data
- update selection
data join
var data = [
{ name: 'Adams', rank: 'Captain' },
{ name: 'Krill', rank: 'Commander' },
{ name: 'Ryback', rank: 'Chief Petty Officer' },
{ name: 'Strannix', rank: 'Civilian' }
];
function render() {
var container = d3.select('ul');
var updateSelection = container.selectAll('li')
.data(data);
updateSelection.enter()
.append('li');
updateSelection.text(function(d) {
return d.name + ' (' + d.rank + ')';
});
requestAnimationFrame(render);
}
requestAnimationFrame(render);
<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>
- render loop
- selection
- data
- update selection
- enter selection
data join
var data = [
{ name: 'Krill', rank: 'Commander' },
{ name: 'Ryback', rank: 'Chief Petty Officer' },
{ name: 'Strannix', rank: 'Civilian' }
];
function render() {
var container = d3.select('ul');
var updateSelection = container.selectAll('li')
.data(data);
updateSelection.enter()
.append('li');
updateSelection.text(function(d) {
return d.name + ' (' + d.rank + ')';
});
requestAnimationFrame(render);
}
requestAnimationFrame(render);
<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>
- render loop
- selection
- data
- update selection
- enter selection
data join
var data = [
{ name: 'Krill', rank: 'Commander' },
{ name: 'Ryback', rank: 'Chief Petty Officer' },
{ name: 'Strannix', rank: 'Civilian' }
];
function render() {
var container = d3.select('ul');
var updateSelection = container.selectAll('li')
.data(data);
updateSelection.enter()
.append('li');
updateSelection.text(function(d) {
return d.name + ' (' + d.rank + ')';
});
updateSelection.exit()
.remove();
requestAnimationFrame(render);
}
requestAnimationFrame(render);
<h1>USS Missouri Crew List</h1>
<ul>
<li>Krill (Commander)</li>
<li>Ryback (Chief Petty Officer)</li>
<li>Strannix (Civilian)</li>
</ul>
- render loop
- selection
- data
- update selection
- enter selection
- exit selection
data join
var data = [
{ name: 'Ryback', rank: 'Chief Petty Officer' }
];
function render() {
var container = d3.select('ul');
var updateSelection = container.selectAll('li')
.data(data);
updateSelection.enter()
.append('li');
updateSelection.text(function(d) {
return d.name + ' (' + d.rank + ')';
});
updateSelection.exit()
.remove();
requestAnimationFrame(render);
}
requestAnimationFrame(render);
<h1>USS Missouri Crew List</h1>
<ul>
<li>Ryback (Chief Petty Officer)</li>
</ul>
- render loop
- selection
- data
- update selection
- enter selection
- exit selection
data join
- idempotent transformation of data into nodes
- join is configurable
- index-based (default)
- identity-based
- custom
- values stored directly on DOM node
- accessible using selection.datum()
components
are easy
components
function caseyRyback(selection) {
selection.text('Another cold day in Hell.');
}
function dramaticEffect(selection) {
selection.style('fontWeight', 'bold');
}
var selection = d3.select('span');
caseyRyback(selection);
caseyRyback(dramaticEffect(selection));
- just a function
<span style="font-weight: bold">
Another cold day in Hell.
</span>
components
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);
- just a function
- invoked with call
<span style="font-weight: bold">
Another cold day in Hell.
</span>
components
function caseyRybackFactory() {
function caseyRyback(selection) {
selection.text('Another cold day in Hell.');
}
return caseyRyback;
}
var component = caseyRybackFactory();
d3.select('span')
.call(component);
- just a function
- invoked with call
- factory
<span>
Another cold day in Hell.
</span>
components
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;
}
var component = caseyRybackFactory()
.quote('Keep the faith, Strannix.');
d3.select('span')
.call(component);
- just a function
- invoked with call
- factory
- configurable
<span>
Keep the faith, Strannix.
</span>
components
- convention based unit of re-use
- invocation is idempotent
- applicable to multi-node selections
our experience
8 rules
#1
data join in components
- use each for reusability
- consider if enter or update is most appropriate
function componentFactory() {
function component(selection) {
selection.each(function(data) {
var container = d3.select(this);
var circles = container.selectAll('circle')
.data(data);
circles.enter()
.append('circle')
.attr('stroke', 'white');
circles.attr('r', function(d) {
return d;
});
circles.exit()
.remove();
});
}
return component;
}
var component = componentFactory();
d3.select('svg')
.datum([50,30,20,5])
.call(component);
<svg>
<circle stroke="white" r="50"></circle>
<circle stroke="white" r="30"></circle>
<circle stroke="white" r="20"></circle>
<circle stroke="white" r="5"></circle>
</svg>
#2
singleton elements
function componentFactory() {
function component(selection) {
selection.each(function(data) {
var container = d3.select(this);
var circle = container.selectAll('circle')
.data([data]);
circle.enter()
.append('circle')
.attr('r', 50);
});
}
return component;
}
var component = componentFactory();
d3.select('svg')
.call(component);
- default join behaviour is index based - enter selection will run once
- update and exit selections most likely redundant
- why use data as the dummy element?
#3
avoid g soup
function componentFactory() {
function component(selection) {
selection.each(function(data) {
var container = d3.select(this);
var g = container.selectAll('g')
.data([data]);
g.enter()
.append('g');
var circles = g.selectAll('circle')
.data(data);
// ...
});
}
return component;
}
var component = componentFactory();
d3.select('svg')
.datum([50,30,20,5])
.call(component);
- don't create container elements
- callers pass in the container
- container won't be re-used
- components are free to manipulate their container's attributes and events
#4
no state in components
- prevents reusability
function componentFactory() {
var container = null;
function component(selection) {
selection.each(function(data) {
container = d3.select(this);
// ...
});
}
component.container = function() {
return container;
};
return component;
}
var component = componentFactory();
d3.select('svg')
.call(component);
#5
separate concerns
function componentFactory() {
var oddColor = 'black';
var evenColor = 'white';
function component(selection) {
selection.attr('fill', function(d, i) {
return i % 2 === 0 ? evenColor : oddColor;
});
}
component.oddColor = function(value) {
// ...
};
component.evenColor = function(value) {
// ...
};
return component;
}
var component = componentFactory();
d3.select('rect')
.call(component);
- extract algorithmic functionality into a separate component
#6
use 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
function componentFactory() {
var colors = d3.scale.category10();
function component(selection) {
selection.each(function(data) {
d3.select(this)
.attr('fill', colors(data.index))
.on('click.component', function(data) {
data.index++;
d3.select(this)
.call(component);
});
});
}
return component;
}
var component = componentFactory();
d3.select('svg')
.append('circle')
.attr('r', 100)
.datum({index: 0})
.call(component);
- only one handler per type
- reduces conflicts with other components
- use event context
#8
expose your own events
function componentFactory() {
var event = d3.dispatch("customevent");
function component(selection) {
selection.each(function(data) {
d3.select(this)
.on('click.component', click);
});
}
function click(data) {
event.customevent.apply(this, arguments);
}
d3.rebind(component, event, 'on');
return component;
}
var component = componentFactory()
.on('customevent', function() {
console.log(this, arguments);
});
d3.select('svg')
.call(component);
- allow interactions to propagate outside of your component
- increase the flexibility of your 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
"the one"
d3-financial-components
- a toolkit for rapidly developing bespoke charts
- one charting library to rule them all
but...
d3-financial-components
- embraces d3: prioritises simplicity
- a curated and consistent set of examples
Creating D3 components
a journey of pain, joy, frustration and enlightenment
(creating d3-financial-components)
Chris Price / @100pxls
Creating D3 components - ScotlandJS
By Chris Price
Creating D3 components - ScotlandJS
The video of this talk is now available on YouTube - https://www.youtube.com/watch?v=aVps_7Llo-M
- 1,582