Skip to content

Commit e468497

Browse files
committed
cross-facet brushX with stocks
1 parent bd83a84 commit e468497

4 files changed

Lines changed: 295 additions & 460 deletions

File tree

docs/interactions/brush.md

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,24 @@ import * as topojson from "topojson-client";
66
import {shallowRef, computed, onMounted} from "vue";
77
import penguins from "../data/penguins.ts";
88

9+
const stocks = shallowRef([]);
910
const world = shallowRef(null);
1011
const land = computed(() => world.value && topojson.feature(world.value, world.value.objects.land));
1112
const allCities = shallowRef([]);
1213
const cities = computed(() => allCities.value.filter((d) => d.population > 500000));
1314

1415
onMounted(() => {
16+
Promise.all([
17+
d3.csv("../data/aapl.csv", d3.autoType),
18+
d3.csv("../data/amzn.csv", d3.autoType),
19+
d3.csv("../data/goog.csv", d3.autoType)
20+
]).then(([aapl, amzn, goog]) => {
21+
stocks.value = [
22+
...aapl.map((d) => ({Symbol: "AAPL", ...d})),
23+
...amzn.map((d) => ({Symbol: "AMZN", ...d})),
24+
...goog.map((d) => ({Symbol: "GOOG", ...d}))
25+
];
26+
});
1527
d3.json("../data/countries-110m.json").then((data) => (world.value = data));
1628
d3.csv("../data/cities-10k.csv", d3.autoType).then((data) => (allCities.value = data));
1729
});
@@ -221,31 +233,37 @@ plot.addEventListener("input", () => {
221233
222234
The brush mark supports [faceting](../features/facets.md). When the plot uses **fx** or **fy** facets, each facet gets its own brush. 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.
223235
236+
<div v-if="stocks.length">
237+
224238
:::plot hidden
225239
```js
226240
Plot.plot({
227-
height: 270,
228-
grid: true,
229-
marks: ((brush) => (d3.timeout(() => brush.move({x1: 45, x2: 55, y1: 15, y2: 20, fx: "Gentoo"})), [
241+
height: 350,
242+
y: {type: "log", grid: true},
243+
fy: {label: null},
244+
marks: ((brush) => (d3.timeout(() => brush.move({x1: new Date("2015-01-01"), x2: new Date("2016-06-01"), fy: "AAPL"})), [
230245
Plot.frame(),
231246
brush,
232-
Plot.dot(penguins, brush.inactive({x: "culmen_length_mm", y: "culmen_depth_mm", fx: "species", fill: "sex", r: 2})),
233-
Plot.dot(penguins, brush.context({x: "culmen_length_mm", y: "culmen_depth_mm", fx: "species", fill: "#ccc", r: 2})),
234-
Plot.dot(penguins, brush.focus({x: "culmen_length_mm", y: "culmen_depth_mm", fx: "species", fill: "sex", r: 3}))
235-
]))(Plot.brush())
247+
Plot.lineY(stocks, {x: "Date", y: "Close", fy: "Symbol", stroke: "#ccc", strokeWidth: 1}),
248+
Plot.lineY(stocks, brush.inactive({x: "Date", y: "Close", fy: "Symbol", stroke: "Symbol"})),
249+
Plot.lineY(stocks, brush.focus({x: "Date", y: "Close", fy: "Symbol", stroke: "Symbol", strokeWidth: 2}))
250+
]))(Plot.brushX())
236251
})
237252
```
238253
:::
239254
240255
```js
241-
const brush = Plot.brush();
256+
const brush = Plot.brushX();
242257
Plot.plot({
258+
height: 350,
259+
y: {type: "log", grid: true},
260+
fy: {label: null},
243261
marks: [
244262
Plot.frame(),
245263
brush,
246-
Plot.dot(penguins, brush.inactive({x: "culmen_length_mm", y: "culmen_depth_mm", fx: "species", fill: "sex", r: 2})),
247-
Plot.dot(penguins, brush.context({x: "culmen_length_mm", y: "culmen_depth_mm", fx: "species", fill: "#ccc", r: 2})),
248-
Plot.dot(penguins, brush.focus({x: "culmen_length_mm", y: "culmen_depth_mm", fx: "species", fill: "sex", r: 3}))
264+
Plot.lineY(stocks, {x: "Date", y: "Close", fy: "Symbol", stroke: "#ccc"}),
265+
Plot.lineY(stocks, brush.inactive({x: "Date", y: "Close", fy: "Symbol", stroke: "Symbol"})),
266+
Plot.lineY(stocks, brush.focus({x: "Date", y: "Close", fy: "Symbol", stroke: "Symbol", strokeWidth: 2}))
249267
]
250268
})
251269
```
@@ -255,32 +273,38 @@ By default, starting a brush in one facet clears any selection in other facets.
255273
:::plot hidden
256274
```js
257275
Plot.plot({
258-
height: 270,
259-
grid: true,
260-
marks: ((brush) => (d3.timeout(() => brush.move({x1: 43, x2: 50, y1: 17, y2: 19})), [
276+
height: 350,
277+
y: {type: "log", grid: true},
278+
fy: {label: null},
279+
marks: ((brush) => (d3.timeout(() => brush.move({x1: new Date("2015-01-01"), x2: new Date("2016-06-01"), fy: "AAPL"})), [
261280
Plot.frame(),
262281
brush,
263-
Plot.dot(penguins, brush.inactive({x: "culmen_length_mm", y: "culmen_depth_mm", fx: "species", fill: "sex", r: 2})),
264-
Plot.dot(penguins, brush.context({x: "culmen_length_mm", y: "culmen_depth_mm", fx: "species", fill: "#ccc", r: 2})),
265-
Plot.dot(penguins, brush.focus({x: "culmen_length_mm", y: "culmen_depth_mm", fx: "species", fill: "sex", r: 3}))
266-
]))(Plot.brush({sync: true}))
282+
Plot.lineY(stocks, {x: "Date", y: "Close", fy: "Symbol", stroke: "#ccc", strokeWidth: 1}),
283+
Plot.lineY(stocks, brush.inactive({x: "Date", y: "Close", fy: "Symbol", stroke: "Symbol"})),
284+
Plot.lineY(stocks, brush.focus({x: "Date", y: "Close", fy: "Symbol", stroke: "Symbol", strokeWidth: 2}))
285+
]))(Plot.brushX({sync: true}))
267286
})
268287
```
269288
:::
270289
271290
```js
272-
const brush = Plot.brush({sync: true});
291+
const brush = Plot.brushX({sync: true});
273292
Plot.plot({
293+
height: 350,
294+
y: {type: "log", grid: true},
295+
fy: {label: null},
274296
marks: [
275297
Plot.frame(),
276298
brush,
277-
Plot.dot(penguins, brush.inactive({x: "culmen_length_mm", y: "culmen_depth_mm", fx: "species", fill: "sex", r: 2})),
278-
Plot.dot(penguins, brush.context({x: "culmen_length_mm", y: "culmen_depth_mm", fx: "species", fill: "#ccc", r: 2})),
279-
Plot.dot(penguins, brush.focus({x: "culmen_length_mm", y: "culmen_depth_mm", fx: "species", fill: "sex", r: 3}))
299+
Plot.lineY(stocks, {x: "Date", y: "Close", fy: "Symbol", stroke: "#ccc"}),
300+
Plot.lineY(stocks, brush.inactive({x: "Date", y: "Close", fy: "Symbol", stroke: "Symbol"})),
301+
Plot.lineY(stocks, brush.focus({x: "Date", y: "Close", fy: "Symbol", stroke: "Symbol", strokeWidth: 2}))
280302
]
281303
})
282304
```
283305
306+
</div>
307+
284308
The dispatched value still includes **fx** (and **fy**), indicating the facet where the interaction originated.
285309
286310
## Projections

test/brush-test.ts

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -187,30 +187,33 @@ it("brush faceted filter without fx selects across all facets", async () => {
187187
});
188188

189189
it("brush cross-facet filter selects across all facets", async () => {
190-
const penguins = await d3.csv<any>("data/penguins.csv", d3.autoType);
191-
const b = new Plot.Brush({sync: true});
192-
const xy = {x: "culmen_length_mm", y: "culmen_depth_mm", fx: "species"};
190+
const stocks = [
191+
...(await d3.csv<any>("data/aapl.csv", d3.autoType)).map((d: any) => ({...d, Symbol: "AAPL"})),
192+
...(await d3.csv<any>("data/amzn.csv", d3.autoType)).map((d: any) => ({...d, Symbol: "AMZN"})),
193+
...(await d3.csv<any>("data/goog.csv", d3.autoType)).map((d: any) => ({...d, Symbol: "GOOG"}))
194+
];
195+
const b = Plot.brushX({sync: true});
193196
const plot = Plot.plot({
194-
marks: [Plot.dot(penguins, b.inactive({...xy, r: 2})), b]
197+
marks: [Plot.lineY(stocks, b.inactive({x: "Date", y: "Close", fy: "Symbol"})), b]
195198
});
196199

197200
let lastValue: any;
198201
plot.addEventListener("input", () => (lastValue = plot.value));
199-
b.move({x1: 35, x2: 50, y1: 14, y2: 20, fx: "Adelie"});
202+
b.move({x1: new Date("2015-01-01"), x2: new Date("2016-06-01"), fy: "AAPL"});
200203

201204
assert.ok(lastValue, "should have a value");
202-
assert.ok(lastValue.fx !== undefined, "value should include fx (origin facet)");
203-
204-
// Without fx: selects across all facets
205-
const withoutFx = penguins.filter((d: any) => lastValue.filter(d.culmen_length_mm, d.culmen_depth_mm));
206-
const species = new Set(withoutFx.map((d: any) => d.species));
207-
assert.ok(species.size > 1, `should select multiple species, got: ${[...species]}`);
208-
209-
// With fx: restricts to the origin facet
210-
const withFx = penguins.filter((d: any) => lastValue.filter(d.culmen_length_mm, d.culmen_depth_mm, d.species));
211-
const fxSpecies = new Set(withFx.map((d: any) => d.species));
212-
assert.equal(fxSpecies.size, 1, "with fx should restrict to origin facet");
213-
assert.ok(withFx.length < withoutFx.length, "with fx should select fewer points");
205+
assert.ok(lastValue.fy !== undefined, "value should include fy (origin facet)");
206+
207+
// Without fy: selects across all facets
208+
const withoutFy = stocks.filter((d: any) => lastValue.filter(d.Date));
209+
const symbols = new Set(withoutFy.map((d: any) => d.Symbol));
210+
assert.ok(symbols.size > 1, `should select multiple symbols, got: ${[...symbols]}`);
211+
212+
// With fy: restricts to the origin facet
213+
const withFy = stocks.filter((d: any) => lastValue.filter(d.Date, d.Symbol));
214+
const fySymbols = new Set(withFy.map((d: any) => d.Symbol));
215+
assert.equal(fySymbols.size, 1, "with fy should restrict to origin facet");
216+
assert.ok(withFy.length < withoutFy.length, "with fy should select fewer points");
214217
});
215218

216219
it("brush faceted filter with fx and fy supports partial facet args", async () => {

0 commit comments

Comments
 (0)