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 coordinates
  • target: The brush behavior instance
  • type: The event type string
  • mode: 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(), and d3.brushY() factory functions exported from src/index.js in the d3/d3 repository.
  • Attach a brush to a <g> element using selection.call(brush), which automatically creates the overlay, selection rectangle, and resize handles defined in d3-brush/src/brush.js.
  • Implement data filtering by listening to brush events, reading event.selection for pixel bounds, and applying scale.invert() to translate those coordinates back into your data domain.
  • Control the brush programmatically with brush.move(), brush.clear(), or the static d3.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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →