Some declarative, React-like logic for your Observable/d3 toolkit
Earlier this week, as I was looking into idiomatic ways of updating a chart, I came across Kaho Cheung’s d3-render – and boy did I get excited.
d3-render
packages up a bunch of d3
commands in a declarative shell, inspired by a eather functional and reactive approach. @unkleho has got a lovely tutorial introducing the library, but here’s another one from yours truly anyway.
Implementation
It’s going to work this way:
- Our chart is just an
<svg>
element with a timer calling… - A
draw
function, which draws on an DOM element from an array of objects-like variable. More precisely, itmap
s over each and renders… - A component-like DOM element for each item in the
map
.
Our components obviously need to be defined and crafted, but we do so at a higher, more declarative level, while d3-render
looks after the update pattern and the details.
“Our chart is just an SVG with a timer which calls…”
chart = {
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);
let i = 0;
while (true) {
await Promises.tick(900);
yield svg.node();
drawData(
svg,
data.map(e => ({ ...e, value: Math.random() * e.value }))
);
}
return svg.node();
}
“A draw function that maps over data to render…”
drawData = (selection, data) => {
const root = pack(data);
const arrayOfBubbles = root
.descendants()
.filter(d => d.depth > 0)
.map((e, i) => {
const { x, y, r } = e;
return CircleComponent({
key: e.data.title,
cx: x,
cy: y,
r: r,
fill: e.data.group
});
});
render(selection, arrayOfBubbles);
}
“A component-like DOM element for each map item”
CircleComponent = ({ key, r, fill, cx, cy }) => ({
append: 'circle',
key,
r,
fill,
cx,
cy,
fill: color(fill),
duration: 1000,
delay: Math.random() * 50
})
Background
The General Update Pattern is a commonly-referred to implementation of d3’s updating and reactive abilities.
In Observable world, it’s Michael Freeman’s implementation of the new d3 .join()
that I was referred to. Here’s what the same chart would have looked like in this world – and bear in mind that you’d have to write a lot of this imperative code for each group of elements in a more complex chart.
oldchart = {
const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);
svg.node().drawData = function(data) {
const root = pack(data);
const circles = svg
.selectAll('circle')
.data(root.descendants().filter(d => d.depth > 0));
circles.join(
enter =>
enter
.append('circle')
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('fill', d => color(d.data.group))
.attr('r', d => d.r),
update =>
update
.transition()
.duration(1000)
.delay(Math.random() * 50)
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('fill', d => color(d.data.group))
.attr('r', d => d.r)
);
};
return svg.node();
}
{
let i = 0;
while (true) {
await Promises.tick(900);
i++;
oldchart.drawData(
data.map(e => ({ ...e, value: Math.random() * e.value }))
);
yield oldchart.nodeName;
}
}