Plugins
d3-blueprint charts emit lifecycle events (preDraw, postDraw, postTransition) that external code can listen to via .on(). A plugin is a simple object that hooks into these events to add cross-cutting behavior like tooltips, crosshairs, and hover highlights without modifying the chart class itself.
The Plugin Interface
A plugin is any object with some or all of these methods:
const myPlugin = {
name: 'my-plugin', // used as the event namespace
install(chart) { }, // called once, immediately
preDraw(chart, data) { }, // called on every preDraw event
postDraw(chart, data) { }, // called on every postDraw event
postTransition(chart, data) { }, // called after transitions complete
destroy(chart) { }, // called when the chart is destroyed
};Only install is required. The rest are optional, so implement only the hooks you need.
chart.use(plugin, name?)
The usePlugin method on D3Blueprint wires a plugin into the chart:
class MyChart extends D3Blueprint {
initialize() {
// ... set up layers, scales, etc.
this.use(myPlugin);
}
}What it does:
- Calls
plugin.install(chart)immediately - Registers
plugin.preDraw/plugin.postDraw/plugin.postTransitionas namespaced event listeners (e.g.postDraw.my-plugin), so they don't conflict with other plugins or the chart's own hooks - Wraps
chart.destroy()to callplugin.destroy(chart)before the original teardown
The optional name parameter overrides the namespace (defaults to plugin.name or 'plugin').
tooltipPlugin()
A factory that returns a tooltip plugin. Pass the SVG group (for coordinate conversion) and a bind callback:
import { tooltipPlugin } from './plugins/Tooltip.js';
class BarChart extends D3Blueprint {
initialize() {
this.chart = this.base.append('g')
.attr('transform', `translate(${MARGIN.left},${MARGIN.top})`);
// ... set up layers, axes, etc.
this.use(tooltipPlugin(this.chart, (chart, tooltip) => {
chart.bars.base.selectAll('rect')
.on('mouseenter', function (event, d) {
select(this).attr('opacity', 0.8);
tooltip.show(
chart.xScale(d.label) + chart.xScale.bandwidth(),
chart.yScale(d.value),
`${d.label}: ${d.value}`,
);
})
.on('mouseleave', function () {
select(this).attr('opacity', 1);
tooltip.hide();
});
}));
}
// No postDraw() needed. The plugin handles tooltip wiring.
}Signature
tooltipPlugin(parent, bind)| Argument | Type | Description |
|---|---|---|
parent | d3 selection | The SVG group used for coordinate conversion |
bind | function | bind(chart, tooltip, data), called on every postDraw to wire DOM events |
The bind callback runs after layers draw, giving full access to:
chart: the chart instance (scales, layers, config, etc.)tooltip: theTooltipinstance (call.show()/.hide())data: the current dataset
Automatic Positioning
The tooltip uses floating-ui under the hood. It converts SVG-local coordinates to screen coordinates via getScreenCTM(), then uses computePosition with flip(), shift(), and offset() middleware to ensure the tooltip always stays within the viewport. No manual width/height configuration is needed, even for responsive charts.
Before & After
Before (manual wiring)
class MyChart extends D3Blueprint {
initialize() {
// ... layers, axes, etc.
this.tooltipEl = document.createElement('div');
document.body.appendChild(this.tooltipEl);
}
postDraw(data) {
this.chart.selectAll('.dots circle')
.on('mouseenter', (event, d) => {
// manual positioning, show/hide logic, etc.
})
.on('mouseleave', () => { /* hide tooltip */ });
}
}After (plugin)
import { tooltipPlugin } from './plugins/Tooltip.js';
class MyChart extends D3Blueprint {
initialize() {
// ... layers, axes, etc.
this.use(tooltipPlugin(this.chart, (chart, tooltip) => {
chart.chart.selectAll('.dots circle')
.on('mouseenter', function (event, d) {
tooltip.show(chart.xScale(d.x), chart.yScale(d.value), `Value: ${d.value}`);
})
.on('mouseleave', () => tooltip.hide());
}));
}
// postDraw() is gone. Less boilerplate, same behavior.
}responsivePlugin()
Makes any chart automatically resize when its container dimensions change, using a ResizeObserver under the hood.
import { responsivePlugin } from './plugins/responsivePlugin.js';
const container = document.querySelector('#chart');
const svg = select(container).append('svg');
const chart = new MyChart(svg);
chart.config('width', container.clientWidth);
chart.use(responsivePlugin({
container,
getSize: (el) => ({ width: el.clientWidth }),
}));
chart.draw(data);Options
| Option | Type | Description |
|---|---|---|
container | Element | The DOM element to observe for size changes |
getSize | function | getSize(container), returns an object of config key/value pairs to apply on resize |
How It Works
- On
install, creates aResizeObserverthat watches the container element - On each
postDraw, stores the latest dataset - When the container resizes, calls
getSize(container)to compute new config values, applies them viachart.config(), and redraws with the stored data - On
destroy, disconnects the observer and clears the stored data
The getSize callback gives full control over which config values change on resize. For example, to resize both width and height:
chart.use(responsivePlugin({
container,
getSize: (el) => ({
width: el.clientWidth,
height: el.clientHeight,
}),
}));See the Responsive Bar Chart example for a complete working demo.
Writing a Custom Plugin
Any object that implements install() can be a plugin. Here's a crosshair plugin:
function crosshairPlugin({ parent, height }) {
return {
name: 'crosshair',
install(chart) {
chart.crosshairLine = parent
.append('line')
.attr('y1', 0)
.attr('y2', height)
.attr('stroke', '#999')
.attr('stroke-dasharray', '4,3')
.style('display', 'none');
},
destroy(chart) {
chart.crosshairLine?.remove();
chart.crosshairLine = null;
},
};
}Compose multiple plugins on the same chart:
this.use(crosshairPlugin({ parent: this.chart, height: innerHeight }));
this.use(tooltipPlugin(this.chart, ...));Each plugin gets its own namespaced event listeners, so they don't interfere with each other.