Donut Chart
A donut chart with animated arc transitions. No axes are needed because this chart uses d3-shape's pie() and arc() generators directly, with attrTween for smooth angular interpolation between data updates.
Live Preview
Full Source
js
import { select } from 'd3-selection';
import { D3Blueprint } from 'd3-blueprint';
import { scaleOrdinal } from 'd3-scale';
import { pie, arc } from 'd3-shape';
import { tooltipPlugin } from './plugins/Tooltip.js';
const WIDTH = 400;
const HEIGHT = 400;
const RADIUS = Math.min(WIDTH, HEIGHT) / 2;
const INNER_RADIUS = RADIUS * 0.55;
const COLORS = ['steelblue', '#e45858', '#50a060', '#e8a838', '#7c6bbf'];
class DonutChart extends D3Blueprint {
initialize() {
this.colorScale = scaleOrdinal().range(COLORS);
this.pieFn = pie().value((d) => d.value).sort(null);
this.arcFn = arc().innerRadius(INNER_RADIUS).outerRadius(RADIUS - 10);
this.hoverArc = arc().innerRadius(INNER_RADIUS).outerRadius(RADIUS - 4);
this.chart = this.base
.append('g')
.attr('transform', `translate(${WIDTH / 2},${HEIGHT / 2})`);
const slicesGroup = this.chart.append('g').attr('class', 'slices');
this.layer('slices', slicesGroup, {
dataBind: (selection, data) => {
return selection.selectAll('path').data(this.pieFn(data), (d) => d.data.label);
},
insert: (selection) => selection.append('path'),
events: {
enter: (selection) => {
selection
.attr('fill', (d) => this.colorScale(d.data.label))
.attr('stroke', '#fff')
.attr('stroke-width', 2)
.each(function (d) { this._current = { startAngle: d.endAngle, endAngle: d.endAngle }; });
},
'enter:transition': (transition) => {
const arcFn = this.arcFn;
transition.duration(600).attrTween('d', function (d) {
const start = this._current;
const interpolate = (t) => ({
startAngle: start.startAngle + (d.startAngle - start.startAngle) * t,
endAngle: start.endAngle + (d.endAngle - start.endAngle) * t,
});
this._current = d;
return (t) => arcFn(interpolate(t));
});
},
'merge:transition': (transition) => {
const arcFn = this.arcFn;
transition
.duration(600)
.attr('fill', (d) => this.colorScale(d.data.label))
.attrTween('d', function (d) {
const start = this._current || d;
const interpolate = (t) => ({
startAngle: start.startAngle + (d.startAngle - start.startAngle) * t,
endAngle: start.endAngle + (d.endAngle - start.endAngle) * t,
});
this._current = d;
return (t) => arcFn(interpolate(t));
});
},
'exit:transition': (transition) => {
transition.duration(200).attr('opacity', 0).remove();
},
},
});
// Center label
this.centerLabel = this.chart
.append('text')
.attr('text-anchor', 'middle')
.attr('dy', '0.35em')
.attr('font-size', '14px');
this.tooltip = tooltipPlugin(this.chart);
}
preDraw(data) {
this.colorScale.domain(data.map((d) => d.label));
const total = data.reduce((sum, d) => sum + d.value, 0);
this.centerLabel.text(`Total: ${total}`);
}
postDraw(data) {
const tooltip = this.tooltip;
const arcFn = this.arcFn;
const hoverArc = this.hoverArc;
const total = data.reduce((sum, d) => sum + d.value, 0);
this.chart.selectAll('.slices path')
.on('mouseenter', function (event, d) {
select(this).transition().duration(100).attr('d', hoverArc);
const pct = ((d.data.value / total) * 100).toFixed(1);
const [cx, cy] = arcFn.centroid(d);
tooltip.show(cx, cy, `${d.data.label}: ${d.data.value} (${pct}%)`);
})
.on('mouseleave', function () {
select(this).transition().duration(100).attr('d', arcFn);
tooltip.hide();
});
}
}Usage
js
import { select } from 'd3-selection';
const chart = new DonutChart(
select('#chart').append('svg').attr('width', 400).attr('height', 400),
);
await chart.draw([
{ label: 'React', value: 42 },
{ label: 'Vue', value: 28 },
{ label: 'Angular', value: 18 },
{ label: 'Svelte', value: 8 },
{ label: 'Other', value: 4 },
]);
// Update: arcs animate smoothly to new proportions
await chart.draw([
{ label: 'React', value: 38 },
{ label: 'Vue', value: 32 },
{ label: 'Angular', value: 14 },
{ label: 'Svelte', value: 12 },
{ label: 'Other', value: 4 },
]);Key Takeaways
- No AxisChart needed: donut charts have no axes, so this chart uses
D3Blueprintdirectly without attaching an AxisChart. pie()computes angles: the pie layout transforms{ label, value }data into objects withstartAngleandendAngleproperties.arc()draws paths: the arc generator converts angle data into SVG path strings for the<path>elements.attrTweeninterpolates arcs smoothly: instead of interpolating the path string directly (which would distort),attrTweeninterpolates the angle values and recomputes the path at each frame.- Hover effect uses a larger arc:
hoverArchas a slightly larger outer radius, creating a subtle "pop" effect on mouseenter. _currentstores previous angles: each DOM element remembers its last arc data so the next transition can interpolate from the correct starting position.