Skip to content

Commit f106ee4

Browse files
committed
brush, pointer
1 parent dcd9ec4 commit f106ee4

24 files changed

+12315
-7
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Observable Plot - Changelog
22

3+
## 0.4.2
4+
5+
*Not yet released. These are forthcoming changes in the main branch.*
6+
7+
Plot now supports [interaction marks](./README.md#interactions)! An interaction mark defines an interactive selection represented as a subset of the mark’s data. For example, the [brush mark](./README.md#brush) allows rectangular selection by clicking and dragging; you can use a brush to select points of interest from a scatterplot and show them in a table. The interactive selection is exposed as *plot*.value. When the selection changes during interaction, the plot emits *input* events. This allows plots to be [Observable views](https://observablehq.com/@observablehq/introduction-to-views), but you can also [listen to *input* events](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) directly.
8+
39
## 0.4.1
410

511
Released February 17, 2022.

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1227,6 +1227,38 @@ Plot.vector(wind, {x: "longitude", y: "latitude", length: "speed", rotate: "dire
12271227

12281228
Returns a new vector with the given *data* and *options*. If neither the **x** nor **y** options are specified, *data* is assumed to be an array of pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] such that **x** = [*x₀*, *x₁*, *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …].
12291229

1230+
## Interactions
1231+
1232+
Interactions are special marks that handle user input and define interactive selections. When a plot has an interaction mark, the returned *plot*.value represents the current selection as an array subset of the interaction mark’s data. As the user modifies the selection through interaction with the plot, *input* events are emitted. This design is compatible with [Observable’s viewof operator](https://observablehq.com/@observablehq/introduction-to-views), but you can also listen to *input* events directly via the [EventTarget interface](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget).
1233+
1234+
### Brush
1235+
1236+
[Source](./src/marks/brush.js) · [Examples](https://observablehq.com/@observablehq/plot-brush) · Selects points within a single contiguous rectangular region, such as nearby dots in a scatterplot.
1237+
1238+
#### Plot.brush(*data*, *options*)
1239+
1240+
```js
1241+
Plot.brush(penguins, {x: "culmen_depth_mm", y: "culmen_length_mm"})
1242+
```
1243+
1244+
Returns a new brush with the given *data* and *options*. If neither the **x** nor **y** options are specified, *data* is assumed to be an array of pairs [[*x₀*, *y₀*], [*x₁*, *y₁*], [*x₂*, *y₂*], …] such that **x** = [*x₀*, *x₁*, *x₂*, …] and **y** = [*y₀*, *y₁*, *y₂*, …].
1245+
1246+
#### Plot.brushX(*data*, *options*)
1247+
1248+
```js
1249+
Plot.brushX(penguins, {x: "culmen_depth_mm"})
1250+
```
1251+
1252+
Equivalent to [Plot.brush](#plotbrushdata-options) except that if the **x** option is not specified, it defaults to the identity function and assumes that *data* = [*x₀*, *x₁*, *x₂*, …].
1253+
1254+
#### Plot.brushY(*data*, *options*)
1255+
1256+
```js
1257+
Plot.brushY(penguins, {y: "culmen_length_mm"})
1258+
```
1259+
1260+
Equivalent to [Plot.brush](#plotbrushdata-options) except that if the **y** option is not specified, it defaults to the identity function and assumes that *data* = [*y₀*, *y₁*, *y₂*, …].
1261+
12301262
## Decorations
12311263

12321264
Decorations are static marks that do not represent data. Currently this includes only [Plot.frame](#frame), although internally Plot’s axes are implemented as decorations and may in the future be exposed here for more flexible configuration.

src/index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@ export {Area, area, areaX, areaY} from "./marks/area.js";
33
export {Arrow, arrow} from "./marks/arrow.js";
44
export {BarX, BarY, barX, barY} from "./marks/bar.js";
55
export {boxX, boxY} from "./marks/box.js";
6+
export {Brush, brush, brushX, brushY} from "./marks/brush.js";
67
export {Cell, cell, cellX, cellY} from "./marks/cell.js";
78
export {Dot, dot, dotX, dotY} from "./marks/dot.js";
89
export {Frame, frame} from "./marks/frame.js";
910
export {Image, image} from "./marks/image.js";
1011
export {Line, line, lineX, lineY} from "./marks/line.js";
1112
export {Link, link} from "./marks/link.js";
13+
export {Pointer, pointer, pointerX, pointerY} from "./marks/pointer.js";
1214
export {Rect, rect, rectX, rectY} from "./marks/rect.js";
1315
export {RuleX, RuleY, ruleX, ruleY} from "./marks/rule.js";
1416
export {Text, text, textX, textY} from "./marks/text.js";
1517
export {TickX, TickY, tickX, tickY} from "./marks/tick.js";
1618
export {Vector, vector} from "./marks/vector.js";
19+
export {selection} from "./selection.js";
1720
export {valueof} from "./options.js";
1821
export {filter, reverse, sort, shuffle} from "./transforms/basic.js";
1922
export {bin, binX, binY} from "./transforms/bin.js";

src/marks/brush.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import {brush as brusher, brushX as brusherX, brushY as brusherY, create, select} from "d3";
2+
import {identity, maybeTuple} from "../options.js";
3+
import {Mark} from "../plot.js";
4+
import {selection, selectionEquals} from "../selection.js";
5+
import {applyDirectStyles, applyIndirectStyles} from "../style.js";
6+
7+
const defaults = {
8+
ariaLabel: "brush",
9+
fill: "#777",
10+
fillOpacity: 0.3,
11+
stroke: "#fff"
12+
};
13+
14+
export class Brush extends Mark {
15+
constructor(data, {x, y, ...options} = {}) {
16+
super(
17+
data,
18+
[
19+
{name: "x", value: x, scale: "x", optional: true},
20+
{name: "y", value: y, scale: "y", optional: true}
21+
],
22+
options,
23+
defaults
24+
);
25+
this.activeElement = null;
26+
}
27+
render(index, {x, y}, {x: X, y: Y}, dimensions) {
28+
const {ariaLabel, ariaDescription, ariaHidden, ...options} = this;
29+
const {marginLeft, width, marginRight, marginTop, height, marginBottom} = dimensions;
30+
const brush = this;
31+
const g = create("svg:g")
32+
.call(applyIndirectStyles, {ariaLabel, ariaDescription, ariaHidden}, dimensions)
33+
.call((X && Y ? brusher : X ? brusherX : brusherY)()
34+
.extent([[marginLeft, marginTop], [width - marginRight, height - marginBottom]])
35+
.on("start brush end", function(event) {
36+
const {type, selection: extent} = event;
37+
// For faceting, when starting a brush in a new facet, clear the
38+
// brush and selection on the old facet. In the future, we might
39+
// allow independent brushes across facets by disabling this?
40+
if (type === "start" && brush.activeElement !== this) {
41+
if (brush.activeElement !== null) {
42+
select(brush.activeElement).call(event.target.clear, event);
43+
brush.activeElement[selection] = null;
44+
}
45+
brush.activeElement = this;
46+
}
47+
let S = null;
48+
if (extent) {
49+
S = index;
50+
if (X) {
51+
let [x0, x1] = Y ? [extent[0][0], extent[1][0]] : extent;
52+
if (x.bandwidth) x0 -= x.bandwidth();
53+
S = S.filter(i => x0 <= X[i] && X[i] <= x1);
54+
}
55+
if (Y) {
56+
let [y0, y1] = X ? [extent[0][1], extent[1][1]] : extent;
57+
if (y.bandwidth) y0 -= y.bandwidth();
58+
S = S.filter(i => y0 <= Y[i] && Y[i] <= y1);
59+
}
60+
}
61+
if (!selectionEquals(this[selection], S)) {
62+
this[selection] = S;
63+
this.dispatchEvent(new Event("input", {bubbles: true}));
64+
}
65+
}))
66+
.call(g => g.selectAll(".selection")
67+
.attr("shape-rendering", null) // reset d3-brush
68+
.call(applyIndirectStyles, options, dimensions)
69+
.call(applyDirectStyles, options))
70+
.node();
71+
g[selection] = null;
72+
return g;
73+
}
74+
}
75+
76+
export function brush(data, {x, y, ...options} = {}) {
77+
([x, y] = maybeTuple(x, y));
78+
return new Brush(data, {...options, x, y});
79+
}
80+
81+
export function brushX(data, {x = identity, ...options} = {}) {
82+
return new Brush(data, {...options, x});
83+
}
84+
85+
export function brushY(data, {y = identity, ...options} = {}) {
86+
return new Brush(data, {...options, y});
87+
}

0 commit comments

Comments
 (0)