Skip to content

Commit 8fef999

Browse files
committed
brush
closes #5
1 parent a362a0d commit 8fef999

File tree

4 files changed

+128
-1
lines changed

4 files changed

+128
-1
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"dependencies": {
4646
"d3-array": "^2.8.0",
4747
"d3-axis": "^2.0.0",
48+
"d3-brush": "^2.0.0",
4849
"d3-color": "^2.0.0",
4950
"d3-interpolate": "^2.0.1",
5051
"d3-scale": "^3.2.3",

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export {Mark} from "./mark.js";
33
export {Area, area, areaX, areaY} from "./marks/area.js";
44
export {BarX, BarY, barX, barY} from "./marks/bar.js";
55
export {bin, binX, binY} from "./marks/bin.js";
6+
export {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";

src/marks/brush.js

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import {extent} from "d3-array";
2+
import {brush as brusher, brushX as brusherX, brushY as brusherY} from "d3-brush";
3+
import {create} from "d3-selection";
4+
import {filter} from "../defined.js";
5+
import {Mark, identity, first, second} from "../mark.js";
6+
import {Style} from "../style.js";
7+
const {max, min} = Math;
8+
9+
export class Brush extends Mark {
10+
constructor(
11+
data,
12+
{
13+
x = first,
14+
y = second,
15+
selection,
16+
transform,
17+
...style
18+
} = {}
19+
) {
20+
super(
21+
data,
22+
[
23+
{name: "picker", value: identity},
24+
{name: "x", value: x, scale: "x", optional: true},
25+
{name: "y", value: y, scale: "y", optional: true}
26+
],
27+
transform
28+
);
29+
Style(this, style);
30+
this.initialSelection = selection;
31+
}
32+
render(
33+
I,
34+
{x, y},
35+
{x: X, y: Y, picker: J},
36+
{marginLeft, width, marginRight, marginTop, height, marginBottom}
37+
) {
38+
let svg;
39+
const g = create("svg:g");
40+
const data = this.data;
41+
42+
const bounds = [
43+
[Math.floor(marginLeft), Math.floor(marginTop)],
44+
[Math.ceil(width - marginRight), Math.ceil(height - marginBottom)]
45+
];
46+
const brush = (x && y ? brusher : x ? brusherX : brusherY)()
47+
.extent(bounds)
48+
.on("start brush end", ({type, selection, sourceEvent}) => {
49+
let index = filter(I, X, Y);
50+
if (selection) {
51+
if (x) {
52+
const e = y ? [selection[0][0], selection[1][0]] : selection;
53+
const [x0, x1] = extent(e.map(x.invert));
54+
index = index.filter(i => X[i] >= x0 && X[i] <= x1);
55+
}
56+
if (y) {
57+
const e = x ? [selection[0][1], selection[1][1]] : selection;
58+
const [y0, y1] = extent(e.map(y.invert));
59+
index = index.filter(i => Y[i] >= y0 && Y[i] <= y1);
60+
}
61+
}
62+
const dots = selection ? Array.from(index, i => J[i]) : data;
63+
64+
if (svg) {
65+
svg.value = dots;
66+
svg.dispatchEvent(new CustomEvent('input'));
67+
if (sourceEvent && type === "start") {
68+
for (const {b, g} of svg.__brushes) {
69+
if (b !== brush) g.call(b.clear);
70+
}
71+
}
72+
}
73+
});
74+
75+
g.call(brush);
76+
77+
/* 🌶 async
78+
* wait for the ownerSVGElement to:
79+
* - send the first signal
80+
* - register the multiple brushes (for faceting)
81+
*/
82+
setTimeout(() => {
83+
svg = g.node().ownerSVGElement;
84+
if (!svg.__brushes) svg.__brushes = [];
85+
svg.__brushes.push({b: brush, g});
86+
87+
// initial setup works only on one facet
88+
if (svg.__brushes.length === 1) {
89+
if (this.initialSelection) {
90+
const s = this.initialSelection;
91+
if (x && y) {
92+
const [x0, x1] = extent([x(s[0][0]), x(s[1][0])]);
93+
const [y0, y1] = extent([y(s[0][1]), y(s[1][1])]);
94+
g.call(brush.move, [
95+
[ max(x0, bounds[0][0]), max(y0, bounds[0][1]) ],
96+
[ min(x1, bounds[1][0]), min(y1, bounds[1][1]) ]
97+
]);
98+
} else if (x) {
99+
const [x0, x1] = extent(s.map(x));
100+
g.call(brush.move, [ max(x0, bounds[0][0]), min(x1, bounds[1][0]) ]);
101+
} else if (y) {
102+
const [y0, y1] = extent(s.map(y));
103+
g.call(brush.move, [ max(y0, bounds[0][1]), min(y1, bounds[1][1]) ]);
104+
}
105+
} else {
106+
g.call(brush.clear);
107+
}
108+
}
109+
}, 1);
110+
111+
return g.node();
112+
}
113+
}
114+
115+
export function brush(data, options) {
116+
return new Brush(data, options);
117+
}
118+
119+
export function brushX(data, {x = identity, ...options} = {}) {
120+
return new Brush(data, {...options, x, y: null});
121+
}
122+
123+
export function brushY(data, {y = identity, ...options} = {}) {
124+
return new Brush(data, {...options, x: null, y});
125+
}

yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -706,7 +706,7 @@ d3-axis@2, d3-axis@^2.0.0:
706706
resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-2.0.0.tgz#40aebb65626ffe6d95e9441fbf9194274b328a8b"
707707
integrity sha512-9nzB0uePtb+u9+dWir+HTuEAKJOEUYJoEwbJPsZ1B4K3iZUgzJcSENQ05Nj7S4CIfbZZ8/jQGoUzGKFznBhiiQ==
708708

709-
d3-brush@2:
709+
d3-brush@2, d3-brush@^2.0.0:
710710
version "2.1.0"
711711
resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-2.1.0.tgz#adadfbb104e8937af142e9a6e2028326f0471065"
712712
integrity sha512-cHLLAFatBATyIKqZOkk/mDHUbzne2B3ZwxkzMHvFTCZCmLaXDpZRihQSn8UNXTkGD/3lb/W2sQz0etAftmHMJQ==

0 commit comments

Comments
 (0)