Histogram
A frequency histogram using d3-array's bin() to bucket continuous values. Uses a reusable AxisChart for axes and transform() to preprocess raw values into bins before the layers see the data.
Live Preview
Full Source
js
import { select } from 'd3-selection';
import { D3Blueprint } from 'd3-blueprint';
import { scaleLinear } from 'd3-scale';
import { max, bin } from 'd3-array';
import { AxisChart } from './charts/AxisChart.js';
import { tooltipPlugin } from './plugins/Tooltip.js';
const MARGIN = { top: 20, right: 20, bottom: 30, left: 45 };
class Histogram extends D3Blueprint {
initialize() {
this.xScale = scaleLinear();
this.yScale = scaleLinear();
this.chart = this.base
.append('g')
.attr('transform', `translate(${MARGIN.left},${MARGIN.top})`);
this.attach('axes', AxisChart, this.chart);
const innerHeight = 400 - MARGIN.top - MARGIN.bottom;
// Bars layer. Each bar is one bin.
const barsGroup = this.chart.append('g').attr('class', 'bars');
this.layer('bars', barsGroup, {
dataBind: (selection, data) => {
return selection.selectAll('rect').data(data, (d) => d.x0);
},
insert: (selection) => selection.append('rect'),
events: {
enter: (selection) => {
selection
.attr('x', (d) => this.xScale(d.x0) + 1)
.attr('width', (d) => Math.max(0, this.xScale(d.x1) - this.xScale(d.x0) - 2))
.attr('y', innerHeight)
.attr('height', 0)
.attr('fill', 'steelblue')
.attr('rx', 1);
},
'enter:transition': (transition) => {
transition
.duration(600)
.attr('y', (d) => this.yScale(d.length))
.attr('height', (d) => innerHeight - this.yScale(d.length));
},
'merge:transition': (transition) => {
transition
.duration(600)
.attr('x', (d) => this.xScale(d.x0) + 1)
.attr('width', (d) => Math.max(0, this.xScale(d.x1) - this.xScale(d.x0) - 2))
.attr('y', (d) => this.yScale(d.length))
.attr('height', (d) => innerHeight - this.yScale(d.length));
},
'exit:transition': (transition) => {
transition.duration(200).attr('opacity', 0).remove();
},
},
});
const innerWidth = 600 - MARGIN.left - MARGIN.right;
this.tooltip = tooltipPlugin(this.chart);
}
transform(data) {
this.xScale.domain([0, 100]);
return bin().domain(this.xScale.domain()).thresholds(20)(data);
}
preDraw(data) {
const innerWidth = 600 - MARGIN.left - MARGIN.right;
const innerHeight = 400 - MARGIN.top - MARGIN.bottom;
this.xScale.range([0, innerWidth]);
this.yScale.domain([0, max(data, (d) => d.length)]).range([innerHeight, 0]).nice();
this.attached.axes.config({
xScale: this.xScale,
yScale: this.yScale,
innerWidth,
innerHeight,
duration: 600,
xTickCount: 10,
yTickCount: 5,
});
}
postDraw(data) {
const tooltip = this.tooltip;
const xScale = this.xScale;
const yScale = this.yScale;
this.chart.selectAll('.bars rect')
.on('mouseenter', function (event, d) {
select(this).attr('fill-opacity', 0.8);
tooltip.show(
xScale((d.x0 + d.x1) / 2),
yScale(d.length),
`${d.x0}–${d.x1}: ${d.length} values`,
);
})
.on('mouseleave', function () {
select(this).attr('fill-opacity', 1);
tooltip.hide();
});
}
}Usage
js
import { select } from 'd3-selection';
const chart = new Histogram(
select('#chart').append('svg').attr('width', 600).attr('height', 400),
);
// Pass raw continuous values. transform() bins them automatically.
const data = Array.from({ length: 500 }, () =>
Math.round(50 + (Math.random() + Math.random() + Math.random() - 1.5) * 30),
);
await chart.draw(data);Key Takeaways
transform()preprocesses raw data: the raw array of numbers is converted into bins beforepreDraw()and the layers see it. This keeps the layer logic clean, since it only deals with bin objects.bin().domain().thresholds(): D3'sbin()generator controls how values are bucketed.domain([0, 100])sets the range,thresholds(20)creates ~20 evenly spaced bins.- Bins have
x0,x1, andlength: each bin is an array of values withx0(lower bound) andx1(upper bound) properties.d.lengthgives the count. - AxisChart handles axis rendering: the parent configures it with scales in
preDraw(). - Bar width comes from the bin boundaries:
xScale(d.x1) - xScale(d.x0)ensures bars fill their bin exactly, with a small gap between them.