Skip to content

Commit 2f5ca2b

Browse files
committed
performance: hack Z instead of doing several calls to next(); also, prettier
1 parent b6eb54a commit 2f5ca2b

3 files changed

Lines changed: 130 additions & 40 deletions

File tree

src/interactions/brush.js

Lines changed: 36 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from "d3";
1111
import {composeRender, Mark} from "../mark.js";
1212
import {constant, keyword, maybeInterval} from "../options.js";
13+
import {pixelPrecision} from "../precision.js";
1314

1415
export class Brush extends Mark {
1516
constructor({dimension = "xy", interval, sync = false} = {}) {
@@ -32,8 +33,8 @@ export class Brush extends Mark {
3233
const dim = this._dimension;
3334
const interval = this._interval;
3435
if (context.projection && dim !== "xy") throw new Error(`brush${dim.toUpperCase()} does not support projections`);
35-
const invertX = (!context.projection && x?.invert) || ((d) => d);
36-
const invertY = (!context.projection && y?.invert) || ((d) => d);
36+
const invertX = precisionInvert(x, context.projection);
37+
const invertY = precisionInvert(y, context.projection);
3738
const applyX = (this._applyX = (!context.projection && x) || ((d) => d));
3839
const applyY = (this._applyY = (!context.projection && y) || ((d) => d));
3940
context.dispatchValue(null);
@@ -43,7 +44,10 @@ export class Brush extends Mark {
4344
_brush
4445
.extent([
4546
[dimensions.marginLeft - (dim !== "y"), dimensions.marginTop - (dim !== "x")],
46-
[dimensions.width - dimensions.marginRight + (dim !== "y"), dimensions.height - dimensions.marginBottom + (dim !== "x")]
47+
[
48+
dimensions.width - dimensions.marginRight + (dim !== "y"),
49+
dimensions.height - dimensions.marginBottom + (dim !== "x")
50+
]
4751
])
4852
.on("start brush end", function (event) {
4953
if (syncing) return;
@@ -99,40 +103,28 @@ export class Brush extends Mark {
99103
context.dispatchValue(value);
100104
}
101105
} else {
102-
const [[px1, py1], [px2, py2]] =
103-
dim === "xy"
104-
? selection
105-
: dim === "x"
106-
? [
107-
[selection[0], NaN],
108-
[selection[1], NaN]
109-
]
110-
: [
111-
[NaN, selection[0]],
112-
[NaN, selection[1]]
113-
];
114-
115-
const inX = isNaN(px1) ? () => true : (xi) => px1 <= xi && xi < px2;
116-
const inY = isNaN(py1) ? () => true : (yi) => py1 <= yi && yi < py2;
117-
106+
const [[px1, py1], [px2, py2]] = dim === "xy" ? selection
107+
: dim === "x" ? [[selection[0]], [selection[1]]]
108+
: [[, selection[0]], [, selection[1]]]; // prettier-ignore
109+
const inX = dim !== "y" && ((xi) => px1 <= xi && xi < px2);
110+
const inY = dim !== "x" && ((yi) => py1 <= yi && yi < py2);
118111
if (sync) {
119112
syncing = true;
120113
selectAll(_brushNodes.filter((_, i) => i !== currentNode)).call(_brush.move, selection);
121114
syncing = false;
122115
}
123116
for (let i = sync ? 0 : currentNode, n = sync ? _brushNodes.length : currentNode + 1; i < n; ++i) {
124117
inactive.update(false, i);
125-
ctx.update((xi, yi) => !(inX(xi) && inY(yi)), i);
126-
focus.update((xi, yi) => inX(xi) && inY(yi), i);
118+
ctx.update(!inX ? (_, yi) => !inY(yi) : !inY ? (xi) => !inX(xi) : (xi, yi) => !(inX(xi) && inY(yi)), i);
119+
focus.update(!inX ? (_, yi) => inY(yi) : !inY ? inX : (xi, yi) => inX(xi) && inY(yi), i);
127120
}
128121

129122
const [x1, x2] = invertX && [invertX(px1), invertX(px2)].sort(ascending);
130123
const [y1, y2] = invertY && [invertY(py1), invertY(py2)].sort(ascending);
131124

132125
// Snap to interval on end
133-
if (type === "end" && interval && !snapping) {
134-
const s1 = dim === "x" ? x1 : y1;
135-
const s2 = dim === "x" ? x2 : y2;
126+
if (!snapping && type === "end" && interval) {
127+
const [s1, s2] = dim === "x" ? [x1, x2] : [y1, y2];
136128
const r1 = intervalRound(interval, s1);
137129
let r2 = intervalRound(interval, s2);
138130
if (+r1 === +r2) r2 = interval.offset(r1);
@@ -168,10 +160,9 @@ export class Brush extends Mark {
168160
return;
169161
}
170162
const {x1, x2, y1, y2, fx, fy} = value;
171-
const node = this._brushNodes.find((n) => {
172-
const d = n.__data__;
173-
return (fx === undefined || d?.x === fx) && (fy === undefined || d?.y === fy);
174-
});
163+
const node = this._brushNodes.find(
164+
(n) => (fx === undefined || n.__data__?.x === fx) && (fy === undefined || n.__data__?.y === fy)
165+
);
175166
if (!node) return;
176167
const [px1, px2] = [x1, x2].map(this._applyX).sort(ascending);
177168
const [py1, py2] = [y1, y2].map(this._applyY).sort(ascending);
@@ -266,24 +257,22 @@ function renderFilter(initialTest) {
266257
return {
267258
pointerEvents: "none",
268259
...options,
269-
render: composeRender(function (index, scales, values, dimensions, context, next) {
270-
const {x: X, y: Y, x1: X1, x2: X2, y1: Y1, y2: Y2} = values;
260+
render: composeRender((index, scales, values, dimensions, context, next) => {
261+
const {x: X, y: Y, x1: X1, x2: X2, y1: Y1, y2: Y2, z: Z} = values;
271262
const MX = X ?? (X1 && X2 ? Float64Array.from(X1, (v, i) => (v + X2[i]) / 2) : undefined);
272263
const MY = Y ?? (Y1 && Y2 ? Float64Array.from(Y1, (v, i) => (v + Y2[i]) / 2) : undefined);
264+
const G = Array((MX ?? MY).length);
273265
const render = (test) => {
274266
if (typeof test !== "function") test = constant(test);
275-
let run = [];
276-
const runs = [run];
267+
const I = [];
268+
let run = 0;
277269
for (const i of index) {
278-
if (test(MX?.[i], MY?.[i])) run.push(i);
279-
else if (run.length) runs.push((run = []));
270+
if (test(MX?.[i], MY?.[i])) {
271+
I.push(i);
272+
G[i] = `${Z?.[i] ?? ""}/${run}`;
273+
} else ++run;
280274
}
281-
const g = next(runs[0], scales, values, dimensions, context);
282-
for (const run of runs.slice(1)) {
283-
const h = next(run, scales, values, dimensions, context);
284-
while (h.firstChild) g.appendChild(h.firstChild);
285-
}
286-
return g;
275+
return next(I, scales, {...values, z: G}, dimensions, context);
287276
};
288277
let g = render(initialTest);
289278
updatePerFacet.push((test) => {
@@ -302,3 +291,10 @@ function renderFilter(initialTest) {
302291
}
303292
);
304293
}
294+
295+
function precisionInvert(scale, projection) {
296+
if (projection || !scale?.invert) return (d) => d;
297+
const interval = pixelPrecision(scale);
298+
if (interval == null) return scale.invert.bind(scale);
299+
return (p) => interval.floor(scale.invert(p));
300+
}

0 commit comments

Comments
 (0)