Function: brushFilter()
brushFilter<
R>(figure,store,o):BrushFilterHandle
Defined in: packages/sdvplot/src/interact/brush.ts:215
Overlay a d3-brush on a rendered Plot figure, or on a d3-drawn <svg> given its own scales. Each brush move sets
the store's predicate (the region as a row test, for linked tables) and selected (the ids of data inside it,
for highlighting) in ONE notification. Pass x and y for a rectangle, or one of them for a 1-D brush across the
whole svg (a date window on a timeline). Clearing the brush, or a click on the empty chart, clears both, but only
while the store still holds this brush's region. A selection made elsewhere since (a table row, a toggled mark)
survives, and so does one made before that holds exactly the brushed ids: the store kept that set, so the brush
wrote none of its own. The drawn brush follows the store: when its region is cleared or replaced elsewhere, the
rectangle is removed with no second write. empty says what a brush holding no row means. The overlay is inserted
BEHIND the marks, so hovering a mark still reaches it. On a figure with a Plot tip, the press that starts a brush
also pins the tip showing at that moment (Plot's pointer toggles a sticky tip on pointerdown); the brush still
works. Throws InputError without x or y, or unless each brushed axis has a continuous (invertible) scale, in
Node too. Returns a HANDLE, not a teardown function, because a brush has more to do than tear down: move drives it
and destroy removes it (linkSelection and linkCursor, whose teardown is all they have, return a function). A
no-op handle without a DOM.
Type Parameters
R
R
Parameters
figure
store
o
Returns
Examples
import * as Plot from "@observablehq/plot";
import { createSelection } from "@sportsdataverse/sdvplot";
import { brushFilter } from "@sportsdataverse/sdvplot/interact";
import { linkIds } from "@sportsdataverse/sdvplot/plot";
const rows = [
{ team: "KC", wins: 15, net_epa: 0.063 },
{ team: "LAC", wins: 11, net_epa: 0.101 },
{ team: "MIA", wins: 8, net_epa: -0.019 },
];
const svg = Plot.plot({ marks: [Plot.dot(rows, { x: "wins", y: "net_epa", render: linkIds(rows, "team") })] });
const store = createSelection<(typeof rows)[number]>();
const brush = brushFilter(svg, store, { data: rows, x: "wins", y: "net_epa", id: "team" });
brush.move({ x: [10, 16], y: [0, 0.15] }); // drag over the 10+ win teams with a positive net EPA
[...store.getState().selected]; // ["KC", "LAC"]
A 1-D date window that clears when it holds no game
import * as Plot from "@observablehq/plot";
import { createSelection } from "@sportsdataverse/sdvplot";
import { brushFilter } from "@sportsdataverse/sdvplot/interact";
import { linkIds } from "@sportsdataverse/sdvplot/plot";
// five of Brooklyn's 2025-26 games
const games = [
{ game_id: "0022500130", game_date: "2025-10-29", matchup: "BKN vs. ATL" },
{ game_id: "0022500149", game_date: "2025-11-02", matchup: "BKN vs. PHI" },
{ game_id: "0022500156", game_date: "2025-11-03", matchup: "BKN vs. MIN" },
{ game_id: "0022500173", game_date: "2025-11-05", matchup: "BKN @ IND" },
{ game_id: "0022500031", game_date: "2025-11-07", matchup: "BKN vs. DET" },
];
const day = (g: (typeof games)[number]): Date => new Date(`${g.game_date}T00:00:00Z`);
const svg = Plot.plot({
height: 60,
x: { type: "utc" },
marks: [Plot.tickX(games, { x: day, render: linkIds(games, "game_id") })],
});
const store = createSelection<(typeof games)[number]>();
// x alone: a brush along the dates; "clear": a window holding no game is no filter and is not left drawn
const brush = brushFilter(svg, store, { data: games, x: day, id: "game_id", empty: "clear" });
brush.move({ x: [new Date("2025-11-01"), new Date("2025-11-06")] });
[...store.getState().selected]; // ["0022500149", "0022500156", "0022500173"]
A d3-drawn chart brushed through its own d3 scale
import { scaleUtc, select } from "d3";
import { createSelection } from "@sportsdataverse/sdvplot";
import { brushFilter } from "@sportsdataverse/sdvplot/interact";
// five of Brooklyn's 2025-26 games
const games = [
{ game_id: "0022500130", game_date: "2025-10-29", matchup: "BKN vs. ATL" },
{ game_id: "0022500149", game_date: "2025-11-02", matchup: "BKN vs. PHI" },
{ game_id: "0022500156", game_date: "2025-11-03", matchup: "BKN vs. MIN" },
{ game_id: "0022500173", game_date: "2025-11-05", matchup: "BKN @ IND" },
{ game_id: "0022500031", game_date: "2025-11-07", matchup: "BKN vs. DET" },
];
const day = (g: (typeof games)[number]): Date => new Date(`${g.game_date}T00:00:00Z`);
const x = scaleUtc([new Date("2025-10-28"), new Date("2025-11-08")], [10, 630]);
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("viewBox", "0 0 640 60");
select(svg)
.selectAll("rect")
.data(games)
.join("rect")
.attr("x", (g) => x(day(g)) - 1.5)
.attr("y", 6)
.attr("width", 3)
.attr("height", 40)
.attr("data-sdv-id", (g) => g.game_id); // the stamp linkIds would write
const store = createSelection<(typeof games)[number]>();
// no figure.scale on a d3 chart: pass the d3 scale itself (its range() is a method; brushFilter reads it)
brushFilter(svg, store, { data: games, x: day, id: "game_id", scales: { x } }).move({
x: [new Date("2025-11-04"), new Date("2025-11-08")],
});
[...store.getState().selected]; // ["0022500173", "0022500031"]