Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 70 additions & 9 deletions docs/interactions/brush.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ onMounted(() => {

# Brush mark

The **brush mark** renders a two-dimensional [brush](https://d3js.org/d3-brush) that allows the user to select a rectangular region by clicking and dragging. It is typically used to highlight a subset of data, or to filter data for display in a linked view.
The **brush mark** renders a [brush](https://d3js.org/d3-brush) that allows the user to select a region by clicking and dragging. It is typically used to highlight a subset of data, or to filter data for display in a linked view.

## 2-D brushing

:::plot hidden
```js
Expand All @@ -42,8 +44,7 @@ Plot.plot({
})
```

The brush mark does not require data. When added to a plot, it renders a [brush](https://d3js.org/d3-brush) overlay covering the frame. The user can click and drag to create a rectangular selection, drag the selection to reposition it, or drag an edge or corner to resize it. Clicking outside the selection clears it.

The user can click and drag to create a rectangular selection, drag the selection to reposition it, or drag an edge or corner to resize it. Clicking outside the selection clears it.

## 1-D brushing

Expand Down Expand Up @@ -148,6 +149,65 @@ Plot.plot({
To achieve higher contrast, you can place the brush before the reactive marks; reactive marks default to using **pointerEvents** *none* to ensure they don't obstruct pointer events.
:::

## Data and options

The brush accepts optional *data* and *options*. When the options specify **x**, **y**, **fx**, or **fy** channels, these become defaults for the associated reactive marks.

:::plot defer hidden
```js
Plot.plot({
marks: ((brush) => [
brush,
Plot.dot(penguins, brush.inactive({fill: "species", r: 2})),
Plot.dot(penguins, brush.context({fill: "#ccc", r: 2})),
Plot.dot(penguins, brush.focus({fill: "species", r: 3}))
])(Plot.brush(penguins, {x: "culmen_length_mm", y: "culmen_depth_mm"}))
})
```
:::

```js
const brush = Plot.brush(penguins, {x: "culmen_length_mm", y: "culmen_depth_mm"});
Plot.plot({
marks: [
brush,
Plot.dot(penguins, brush.inactive({fill: "species", r: 2})),
Plot.dot(penguins, brush.context({fill: "#ccc", r: 2})),
Plot.dot(penguins, brush.focus({fill: "species", r: 3}))
]
})
```

If neither **x** nor **y** is specified, *data* is assumed to be an array of values, such as [*x₀*, *x₁*, …] for 1-dimensional brushes, or an array of pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], …] for 2-dimensional brushes.

```js
const brush = Plot.brush(points);
```

### Selection styling

The **fill**, **fillOpacity**, **stroke**, **strokeWidth**, and **strokeOpacity** options style the brush selection rectangle, overriding D3's defaults.

```js
const brush = Plot.brush(penguins, {
x: "culmen_length_mm",
y: "culmen_depth_mm",
stroke: "currentColor",
strokeWidth: 1.5
});
```

### Filtered data

When the brush has *data*, the [BrushValue](#brushvalue) includes a **data** property containing the subset filtered by the selection.

```js
plot.addEventListener("input", () => {
console.log(plot.value?.data); // filtered subset of the brush's data
const selected = otherData.filter((d) => plot.value?.filter(d.x, d.y)); // filter a different dataset
});
```

## Faceting

The brush mark supports [faceting](../features/facets.md). When the plot uses **fx** or **fy** facets, each facet gets its own brush. Starting a brush in one facet clears any selection in other facets. The dispatched value includes the **fx** and **fy** facet values of the brushed facet, and the **filter** function also filters on the relevant facet values.
Expand Down Expand Up @@ -231,6 +291,7 @@ The brush value dispatched on [_input_ events](#input-events). When the brush is
- **fx** - the *fx* facet value, if applicable
- **fy** - the *fy* facet value, if applicable
- **filter** - a function to test whether a point is inside the selection
- **data** - when the brush has data, the filtered subset
- **pending** - `true` during interaction; absent when committed

By convention, *x1* < *x2* and *y1* < *y2*. The brushX value does not include *y1* and *y2*; similarly, the brushY value does not include *x1* and *x2*.
Expand All @@ -244,13 +305,13 @@ plot.addEventListener("input", () => {
});
```

## brush() {#brush}
## brush(*data*, *options*) {#brush}

```js
const brush = Plot.brush()
```

Returns a new brush. The mark exposes the **inactive**, **context**, and **focus** methods for creating reactive marks that respond to the brush state.
Returns a new brush with the given *data* and *options*. Both *data* and *options* are optional. If *data* is specified but the neither **x** nor **y** is specified in the *options*, *data* is assumed to be an array of pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], …] such that **x** = [*x₀*, *x₁*, …] and **y** = [*y₀*, *y₁*, …].

## *brush*.inactive(*options*) {#brush.inactive}

Expand Down Expand Up @@ -298,13 +359,13 @@ brush.move(null)

For projected plots, the coordinates are in pixels (consistent with the [BrushValue](#brushvalue)), so you need to project the two corners of the brush beforehand. In the future Plot might expose its *projection* to facilitate this. Please upvote [this issue](https://github.com/observablehq/plot/issues/1191) to help prioritize this feature.

## brushX(*options*) {#brushX}
## brushX(*data*, *options*) {#brushX}

```js
const brush = Plot.brushX()
```

Returns a new horizontal brush mark that selects along the *x* axis. The available *options* are:
Returns a new horizontal brush mark that selects along the *x* axis. If *data* is specified without an **x** channel, each datum is used as the *x* value directly. In addition to the [brush options](#data-and-options), the *interval* option is supported:

- **interval** - an interval to snap the brush to on release; a number for quantitative scales (_e.g._, `100`), a time interval name for temporal scales (_e.g._, `"month"`), or an object with *floor* and *offset* methods

Expand Down Expand Up @@ -337,10 +398,10 @@ Plot.plot({

The brushX mark does not support projections.

## brushY(*options*) {#brushY}
## brushY(*data*, *options*) {#brushY}

```js
const brush = Plot.brushY()
```

Returns a new vertical brush mark that selects along the *y* axis. Accepts the same *options* as [brushX](#brushX).
Returns a new vertical brush mark that selects along the *y* axis. If *data* is specified without a **y** channel, each datum is used as the *y* value directly. For the other options, see [brushX](#brushX).
75 changes: 64 additions & 11 deletions src/interactions/brush.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {ChannelValueSpec} from "../channel.js";
import type {Interval} from "../interval.js";
import type {RenderableMark} from "../mark.js";
import type {Data, MarkOptions, RenderableMark} from "../mark.js";
import type {Rendered} from "../transforms/basic.js";

/**
Expand All @@ -25,16 +26,45 @@ export interface BrushValue {
* A function to test whether a point falls inside the brush selection.
* The signature depends on the dimensions and active facets: for brushX
* and brushY, filter on the value *v* with *(v)*, *(v, fx)*, *(v, fy)*,
* or *(v, fx, fy)* *(x, y)*; for a 2D brush, use *(x, y)*, *(x, y, fx)*,
* or *(v, fx, fy)*; for a 2D brush, use *(x, y)*, *(x, y, fx)*,
* *(x, y, fy)*, or *(x, y, fx, fy)*. When faceted, returns true only for
* points in the brushed facet. For projected plots, *x* and *y* are
* typically longitude and latitude.
*/
filter: (...args: any[]) => boolean;
/** When the brush has data, the subset of data matching the selection. */
data?: any[];
/** True during interaction, absent when committed. */
pending?: true;
}

/** Options for the brush mark. */
export interface BrushOptions extends MarkOptions {
/**
* The horizontal position channel, typically bound to the *x* scale. When
* specified, inherited by reactive marks as a default.
*/
x?: ChannelValueSpec;

/**
* The vertical position channel, typically bound to the *y* scale. When
* specified, inherited by reactive marks as a default.
*/
y?: ChannelValueSpec;

/**
* The horizontal facet channel, bound to the *fx* scale. When specified,
* inherited by reactive marks as a default.
*/
fx?: MarkOptions["fx"];

/**
* The vertical facet channel, bound to the *fy* scale. When specified,
* inherited by reactive marks as a default.
*/
fy?: MarkOptions["fy"];
}

/**
* A mark that renders a [brush](https://d3js.org/d3-brush) allowing the user to
* select a region. The brush coordinates across facets, clearing previous
Expand All @@ -46,6 +76,14 @@ export interface BrushValue {
* reactive marks that respond to the brush state.
*/
export class Brush extends RenderableMark {
/**
* Creates a new brush mark with the given *data* and *options*. If *data* and
* *options* specify **x** and **y** channels, these become defaults for
* reactive marks (**inactive**, **context**, **focus**). The **fill**,
* **fillOpacity**, **stroke**, **strokeWidth**, and **strokeOpacity** options
* style the brush selection rectangle.
*/
constructor(data?: Data, options?: BrushOptions);
/**
* Returns mark options that show the mark when no brush selection is active,
* and hide it during brushing. Use this for the default appearance.
Expand Down Expand Up @@ -76,23 +114,38 @@ export class Brush extends RenderableMark {
): void;
}

/** Creates a new two-dimensional brush mark. */
export function brush(): Brush;
/**
* Creates a new brush mark with the given *data* and *options*. If neither
* **x** nor **y** is specified, they default to the first and second
* element of each datum, assuming [*x*, *y*] pairs.
*/
export function brush(options?: BrushOptions): Brush;
export function brush(data?: Data, options?: BrushOptions): Brush;

/** Options for brush marks. */
export interface BrushOptions {
/** Options for 1-dimensional brush marks. */
export interface Brush1DOptions extends BrushOptions {
/**
* An interval to snap the brush to, such as a number for quantitative scales
* or a time interval name like *month* for temporal scales. On brush end, the
* selection is rounded to the nearest interval boundaries; the dispatched
* filter function floors values before testing, for consistency with binned
* marks. Supported by the 1-dimensional marks brushX and brushY.
* marks.
*/
interval?: Interval;
}

/** Creates a one-dimensional brush mark along the *x* axis. Not supported with projections. */
export function brushX(options?: BrushOptions): Brush;
/**
* Creates a one-dimensional brush mark along the *x* axis. If *data* is
* specified without an **x** channel, each datum is used as the *x* value
* directly. Not supported with projections.
*/
export function brushX(options?: Brush1DOptions): Brush;
export function brushX(data?: Data, options?: Brush1DOptions): Brush;

/** Creates a one-dimensional brush mark along the *y* axis. Not supported with projections. */
export function brushY(options?: BrushOptions): Brush;
/**
* Creates a one-dimensional brush mark along the *y* axis. If *data* is
* specified without a **y** channel, each datum is used as the *y* value
* directly. Not supported with projections.
*/
export function brushY(options?: Brush1DOptions): Brush;
export function brushY(data?: Data, options?: Brush1DOptions): Brush;
Loading