Labeled Bubble Chart
A bubble chart with text labels inside each circle. Builds on the basic Bubble Chart by using <g> groups that contain both a <circle> and <text> elements. Labels are conditionally shown based on circle radius to avoid visual clutter.
Live Preview
Full Source
js
import { select } from 'd3-selection';
import { scaleOrdinal } from 'd3-scale';
import { pack, hierarchy } from 'd3-hierarchy';
import { format } from 'd3-format';
import { D3Blueprint } from 'd3-blueprint';
import { tooltipPlugin } from './plugins/tooltipPlugin.js';
const WIDTH = 500;
const HEIGHT = 500;
const COLORS = ['steelblue', '#e45858', '#50a060', '#e8a838'];
const GROUPS = ['Web', 'Data', 'Enterprise', 'Systems'];
const fmt = format(',d');
class LabeledBubbleChart extends D3Blueprint {
initialize() {
this.colorScale = scaleOrdinal().domain(GROUPS).range(COLORS);
this.packLayout = pack().size([WIDTH - 2, HEIGHT - 2]).padding(3);
this.chart = this.base.append('g').attr('transform', 'translate(1,1)');
const nodesGroup = this.chart.append('g').attr('class', 'nodes');
this.layer('nodes', nodesGroup, {
dataBind: (selection, data) => {
const root = this.packLayout(
hierarchy({ children: data }).sum((d) => d.value),
);
return selection.selectAll('g').data(root.leaves(), (d) => d.data.name);
},
insert: (selection) => {
const g = selection.append('g');
g.append('circle');
g.append('text')
.attr('text-anchor', 'middle')
.attr('dy', '-0.2em')
.attr('font-size', '10px')
.attr('pointer-events', 'none');
g.append('text')
.attr('class', 'value-label')
.attr('text-anchor', 'middle')
.attr('dy', '1em')
.attr('font-size', '9px')
.attr('pointer-events', 'none');
return g;
},
events: {
merge: (selection) => {
selection
.select('text:not(.value-label)')
.text((d) => (d.r > 20 ? d.data.name : ''));
selection
.select('.value-label')
.text((d) => (d.r > 25 ? fmt(d.data.value) : ''));
},
'merge:transition': (transition) => {
transition.duration(800)
.attr('transform', (d) => `translate(${d.x},${d.y})`);
transition.select('circle')
.attr('r', (d) => d.r)
.attr('fill', (d) => this.colorScale(d.data.group))
.attr('fill-opacity', 0.7);
},
'exit:transition': (transition) => {
transition.duration(300).attr('opacity', 0).remove();
},
},
});
this.use(tooltipPlugin(this.chart, (chart, tooltip) => {
chart.chart.selectAll('.nodes g')
.on('mouseenter', function (event, d) {
select(this).select('circle').attr('fill-opacity', 1);
tooltip.show(d.x, d.y, `${d.data.name}: ${fmt(d.data.value)}`);
})
.on('mouseleave', function () {
select(this).select('circle').attr('fill-opacity', 0.7);
tooltip.hide();
});
}));
}
}Usage
js
import { select } from 'd3-selection';
const chart = new LabeledBubbleChart(
select('#chart').append('svg').attr('width', 500).attr('height', 500),
);
await chart.draw([
{ name: 'JavaScript', group: 'Web', value: 92 },
{ name: 'Python', group: 'Data', value: 78 },
{ name: 'Rust', group: 'Systems', value: 34 },
]);Key Takeaways
- Composite
<g>elements: each data point is a group containing a circle and text labels, bound as a single layer item. - Conditional labels: labels are only shown when the circle radius exceeds a threshold (
d.r > 20), preventing text from overflowing small bubbles. - Two-line labels: the name and value are separate
<text>elements offset withdy, giving a clean stacked layout inside each circle. pointer-events: noneon text: ensures hover events pass through labels to the circle beneath, so tooltips work consistently.- Transition on
transform: moving the entire<g>group viatranslate()keeps circle and text in sync during animations.