How to Implement Brush Selection for Data Filtering with d3-brush
Use d3.brush() to create an interactive SVG selection, listen for brush events to obtain pixel coordinates, then invert your D3 scales to translate those bounds into data domain filters.
The d3-brush module provides a powerful interaction layer for D3 visualizations, enabling users to draw, move, and resize rectangular selections directly on SVG elements. By connecting these visual selections to your data scales, you can implement dynamic filtering for scatter plots, time-series charts, and cross-filter dashboards. This guide walks through the architecture of the d3-brush system—referencing the actual d3/d3 and d3/d3-brush source code—and provides a complete, runnable implementation of brush selection for data filtering.
Understanding the d3-brush Architecture
The brush system is built around a generator pattern that attaches interactive behaviors to SVG groups. According to the source in d3-brush/src/brush.js, the implementation handles pointer events, creates the necessary SVG elements (overlay, selection rectangle, and handles), and emits standardized events.
Brush Generators and Application
The factory functions d3.brush(), d3.brushX(), and d3.brushY() create brush behaviors for two-dimensional, horizontal-only, or vertical-only selections respectively. These are re-exported from the main D3 bundle in src/index.js.
When you call selection.call(brush), d3-brush appends the following SVG structure inside your group:
- An
<rect class="overlay">that captures pointer events across the brushable area - A
<rect class="selection">that visualizes the current selection bounds - Eight
<rect class="handle">elements that enable resizing from edges and corners
Event System and Programmatic Control
The brush emits three event types: start, brush, and end. Listeners registered via brush.on(typenames, listener) receive an event object containing:
selection: The current bounds as[[x0, y0], [x1, y1]]in pixel coordinatestarget: The brush behavior instancetype: The event type stringmode: The interaction mode ("drag","handle", or"center")
For programmatic control, use brush.move(selection, null) to clear the brush or brush.move(selection, [[x0, y0], [x1, y1]]) to set a specific region. The static helper d3.brushSelection(node) retrieves the current selection for any brush group element.
Implementing Brush Selection for Data Filtering
The following complete example demonstrates how to connect a 2D brush to a scatter plot, converting pixel coordinates back to data values using D3 scales to filter the displayed points.
// 1️⃣ Set up SVG, scales, and data
const width = 800, height = 500;
const svg = d3.select("body")
.append("svg")
.attr("viewBox", [0, 0, width, height]);
const x = d3.scaleLinear().domain([0, 100]).range([40, width - 20]);
const y = d3.scaleLinear().domain([0, 100]).range([height - 30, 20]);
// Example data: an array of {x, y, id}
const data = d3.range(500).map(i => ({
id: i,
x: Math.random() * 100,
y: Math.random() * 100
}));
// 2️⃣ Draw all points (initially visible)
const points = svg.append("g")
.attr("class", "points")
.selectAll("circle")
.data(data, d => d.id)
.join("circle")
.attr("cx", d => x(d.x))
.attr("cy", d => y(d.y))
.attr("r", 3)
.attr("fill", "steelblue");
// 3️⃣ Create the brush
const brush = d3.brush()
.extent([[0, 0], [width, height]]) // full SVG area
.on("brush", brushed) // fire while dragging
.on("end", brushEnded); // optional final step
svg.append("g")
.attr("class", "brush")
.call(brush);
// 4️⃣ Brush event handler – filter the points
function brushed(event) {
// `event.selection` is [[x0, y0], [x1, y1]] in pixel space
const sel = event.selection;
if (!sel) {
// No selection → show all points
points.attr("fill", "steelblue");
return;
}
const [[x0, y0], [x1, y1]] = sel;
// Convert pixel bounds back to data domain using the inverse scales
const xDomain = [x.invert(x0), x.invert(x1)].sort(d3.ascending);
const yDomain = [y.invert(y1), y.invert(y0)].sort(d3.ascending); // note SVG y‑axis direction
// Update point styles based on whether they fall inside the brushed area
points.attr("fill", d =>
(d.x >= xDomain[0] && d.x <= xDomain[1] &&
d.y >= yDomain[0] && d.y <= yDomain[1])
? "orange"
: "steelblue"
);
}
// Optional: clear the brush on double‑click
function brushEnded(event) {
if (!event.selection) {
points.attr("fill", "steelblue"); // reset colors
}
}
Key Implementation Details
Scale Inversion for Data Filtering
The critical step in connecting brush selection to data filtering is converting pixel coordinates back to data values. Since D3 scales are bidirectional, use scale.invert() to translate the brush's pixel bounds (event.selection) into your data domain. Remember that SVG y-coordinates increase downward while most data visualizations treat y-values as increasing upward, so you must invert the y-range carefully.
Event Handling Strategy
The brush event fires continuously during interaction, making it ideal for live filtering. For expensive computations, consider debouncing this handler or switching to the end event, which fires only when the user releases the mouse. The start event is useful for initializing temporary UI states.
Selection Persistence
To retrieve the brush state outside of event handlers, use d3.brushSelection(node), passing the brush group element. This returns the current selection in pixel coordinates or null if the brush is empty.
Advanced d3-brush Configuration
Beyond basic rectangular selection, the d3-brush module offers specialized behaviors for specific interaction patterns.
One-Dimensional Brushes
For time-series or single-axis filtering, use d3.brushX() or d3.brushY() instead of d3.brush(). These restrict selection to horizontal or vertical ranges respectively, automatically hiding the handles for the non-brushable axis.
const xBrush = d3.brushX()
.extent([[margin.left, 0], [width - margin.right, height]])
.on("brush", updateTimeRange);
Customizing Interaction Constraints
The default brush accepts all primary pointer events. To restrict brushing to specific conditions—such as requiring the Shift key or ignoring right-clicks—chain .filter():
const brush = d3.brush()
.filter(event => !event.ctrlKey && !event.button) // ignore ctrl+click and right-click
.extent([[0, 0], [width, height]]);
Adjust the visual handle size with .handleSize(size) to make resize targets easier to grab on touch devices.
Programmatic Brush Control
Drive the brush from external controls using brush.move(). This method accepts a transition for animated brushing:
// Programmatically select a specific region
svg.select(".brush")
.call(brush.move, [[100, 100], [300, 300]]);
// Clear the brush
svg.select(".brush")
.call(brush.clear);
Summary
- d3-brush provides SVG-based rectangular selection via
d3.brush(),d3.brushX(), andd3.brushY()factory functions exported fromsrc/index.jsin thed3/d3repository. - Attach a brush to a
<g>element usingselection.call(brush), which automatically creates the overlay, selection rectangle, and resize handles defined ind3-brush/src/brush.js. - Implement data filtering by listening to
brushevents, readingevent.selectionfor pixel bounds, and applyingscale.invert()to translate those coordinates back into your data domain. - Control the brush programmatically with
brush.move(),brush.clear(), or the staticd3.brushSelection()helper to retrieve current state outside of event handlers.
Frequently Asked Questions
How do I clear a brush selection programmatically?
Call brush.move(selection, null) on the brush group to clear the selection. For example: svg.select(".brush").call(brush.move, null). This removes the selection rectangle and resets the brush to its empty state, triggering an end event if listeners are attached.
What is the difference between d3.brush(), d3.brushX(), and d3.brushY()?
d3.brush() creates a two-dimensional rectangular selection with eight resize handles. d3.brushX() restricts interaction to the horizontal axis only, displaying a selection with left and right handles, while d3.brushY() restricts interaction to the vertical axis with top and bottom handles. All three share the same event system and API methods defined in d3-brush/src/brush.js.
How can I prevent the brush from triggering during zoom or other gestures?
Use the .filter() method to specify which input events should initiate a brush. For example, brush.filter(event => !event.ctrlKey && !event.button) ignores Ctrl-clicks and right-clicks. You can also check for specific targets or custom conditions to prevent brushing when users interact with other UI elements overlaid on the chart.
Why does my data filtering invert the Y-axis incorrectly?
SVG coordinates increase downward (y=0 is at the top), while most data visualizations treat Y-values as increasing upward. When inverting the brush selection's Y coordinates with yScale.invert(), you must account for this direction. The brush's y1 pixel coordinate (top of selection) corresponds to the larger data value, while y0 (bottom) corresponds to the smaller value, so you should sort the inverted domain array using d3.ascending to ensure correct filtering logic.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →