Skip to content

Commit 662ad4a

Browse files
committed
Merge branch 'fil/brush-x' into fil/brush-data-options
# Conflicts: # src/interactions/brush.js
2 parents 3139166 + 76474e8 commit 662ad4a

File tree

8 files changed

+382
-81
lines changed

8 files changed

+382
-81
lines changed

src/interactions/brush.js

Lines changed: 35 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, dataify, identity, isIterable, keyword, maybeInterval, maybeTuple, take} from "../options.js";
13+
import {pixelRound} from "../precision.js";
1314
import {applyAttr} from "../style.js";
1415

1516
const defaults = {ariaLabel: "brush", fill: "#777", fillOpacity: 0.3, stroke: "#fff"};
@@ -49,8 +50,8 @@ export class Brush extends Mark {
4950
const dim = this._dimension;
5051
const interval = this._interval;
5152
if (context.projection && dim !== "xy") throw new Error(`brush${dim.toUpperCase()} does not support projections`);
52-
const invertX = (!context.projection && x?.invert) || ((d) => d);
53-
const invertY = (!context.projection && y?.invert) || ((d) => d);
53+
const invertX = precisionInvert(x, context.projection);
54+
const invertY = precisionInvert(y, context.projection);
5455
const applyX = (this._applyX = (!context.projection && x) || ((d) => d));
5556
const applyY = (this._applyY = (!context.projection && y) || ((d) => d));
5657
context.dispatchValue(null);
@@ -72,7 +73,10 @@ export class Brush extends Mark {
7273
_brush
7374
.extent([
7475
[dimensions.marginLeft - (dim !== "y"), dimensions.marginTop - (dim !== "x")],
75-
[dimensions.width - dimensions.marginRight + (dim !== "y"), dimensions.height - dimensions.marginBottom + (dim !== "x")]
76+
[
77+
dimensions.width - dimensions.marginRight + (dim !== "y"),
78+
dimensions.height - dimensions.marginBottom + (dim !== "x")
79+
]
7680
])
7781
.on("start brush end", function (event) {
7882
if (syncing) return;
@@ -129,40 +133,28 @@ export class Brush extends Mark {
129133
context.dispatchValue(value);
130134
}
131135
} else {
132-
const [[px1, py1], [px2, py2]] =
133-
dim === "xy"
134-
? selection
135-
: dim === "x"
136-
? [
137-
[selection[0], NaN],
138-
[selection[1], NaN]
139-
]
140-
: [
141-
[NaN, selection[0]],
142-
[NaN, selection[1]]
143-
];
144-
145-
const inX = isNaN(px1) ? () => true : (xi) => px1 <= xi && xi < px2;
146-
const inY = isNaN(py1) ? () => true : (yi) => py1 <= yi && yi < py2;
147-
136+
const [[px1, py1], [px2, py2]] = dim === "xy" ? selection
137+
: dim === "x" ? [[selection[0]], [selection[1]]]
138+
: [[, selection[0]], [, selection[1]]]; // prettier-ignore
139+
const inX = dim !== "y" && ((xi) => px1 <= xi && xi < px2);
140+
const inY = dim !== "x" && ((yi) => py1 <= yi && yi < py2);
148141
if (sync) {
149142
syncing = true;
150143
selectAll(_brushNodes.filter((_, i) => i !== currentNode)).call(_brush.move, selection);
151144
syncing = false;
152145
}
153146
for (let i = sync ? 0 : currentNode, n = sync ? _brushNodes.length : currentNode + 1; i < n; ++i) {
154147
inactive.update(false, i);
155-
ctx.update((xi, yi) => !(inX(xi) && inY(yi)), i);
156-
focus.update((xi, yi) => inX(xi) && inY(yi), i);
148+
ctx.update(!inX ? (_, yi) => !inY(yi) : !inY ? (xi) => !inX(xi) : (xi, yi) => !(inX(xi) && inY(yi)), i);
149+
focus.update(!inX ? (_, yi) => inY(yi) : !inY ? inX : (xi, yi) => inX(xi) && inY(yi), i);
157150
}
158151

159152
const [x1, x2] = invertX && [invertX(px1), invertX(px2)].sort(ascending);
160153
const [y1, y2] = invertY && [invertY(py1), invertY(py2)].sort(ascending);
161154

162155
// Snap to interval on end
163-
if (type === "end" && interval && !snapping) {
164-
const s1 = dim === "x" ? x1 : y1;
165-
const s2 = dim === "x" ? x2 : y2;
156+
if (!snapping && type === "end" && interval) {
157+
const [s1, s2] = dim === "x" ? [x1, x2] : [y1, y2];
166158
const r1 = intervalRound(interval, s1);
167159
let r2 = intervalRound(interval, s2);
168160
if (+r1 === +r2) r2 = interval.offset(r1);
@@ -205,10 +197,9 @@ export class Brush extends Mark {
205197
return;
206198
}
207199
const {x1, x2, y1, y2, fx, fy} = value;
208-
const node = this._brushNodes.find((n) => {
209-
const d = n.__data__;
210-
return (fx === undefined || d?.x === fx) && (fy === undefined || d?.y === fy);
211-
});
200+
const node = this._brushNodes.find(
201+
(n) => (fx === undefined || n.__data__?.x === fx) && (fy === undefined || n.__data__?.y === fy)
202+
);
212203
if (!node) return;
213204
const [px1, px2] = [x1, x2].map(this._applyX).sort(ascending);
214205
const [py1, py2] = [y1, y2].map(this._applyY).sort(ascending);
@@ -313,24 +304,22 @@ function renderFilter(initialTest, channelDefaults = {}) {
313304
pointerEvents: "none",
314305
...channelDefaults,
315306
...options,
316-
render: composeRender(function (index, scales, values, dimensions, context, next) {
317-
const {x: X, y: Y, x1: X1, x2: X2, y1: Y1, y2: Y2} = values;
307+
render: composeRender((index, scales, values, dimensions, context, next) => {
308+
const {x: X, y: Y, x1: X1, x2: X2, y1: Y1, y2: Y2, z: Z} = values;
318309
const MX = X ?? (X1 && X2 ? Float64Array.from(X1, (v, i) => (v + X2[i]) / 2) : undefined);
319310
const MY = Y ?? (Y1 && Y2 ? Float64Array.from(Y1, (v, i) => (v + Y2[i]) / 2) : undefined);
311+
const G = Array((MX ?? MY).length);
320312
const render = (test) => {
321313
if (typeof test !== "function") test = constant(test);
322-
let run = [];
323-
const runs = [run];
314+
const I = [];
315+
let run = 0;
324316
for (const i of index) {
325-
if (test(MX?.[i], MY?.[i])) run.push(i);
326-
else if (run.length) runs.push((run = []));
317+
if (test(MX?.[i], MY?.[i])) {
318+
I.push(i);
319+
G[i] = `${Z?.[i] ?? ""}/${run}`;
320+
} else ++run;
327321
}
328-
const g = next(runs[0], scales, values, dimensions, context);
329-
for (const run of runs.slice(1)) {
330-
const h = next(run, scales, values, dimensions, context);
331-
while (h.firstChild) g.appendChild(h.firstChild);
332-
}
333-
return g;
322+
return next(I, scales, {...values, z: G}, dimensions, context);
334323
};
335324
let g = render(initialTest);
336325
updatePerFacet.push((test) => {
@@ -349,3 +338,9 @@ function renderFilter(initialTest, channelDefaults = {}) {
349338
}
350339
);
351340
}
341+
342+
function precisionInvert(scale, projection) {
343+
if (projection || !scale?.invert) return (d) => d;
344+
const round = pixelRound(scale);
345+
return (p) => round(scale.invert(p));
346+
}

src/precision.d.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type {ScaleType} from "./scales.js";
2+
3+
/** Internal d3 scale with type, as produced by createScaleFunctions. */
4+
export interface MaterializedScale {
5+
(value: any): number;
6+
type: ScaleType;
7+
domain(): any[];
8+
range(): number[];
9+
invert(value: number): any;
10+
}
11+
12+
/**
13+
* Returns a function that rounds values in data space to the coarsest
14+
* precision that distinguishes neighboring pixels. For temporal scales, finds
15+
* the coarsest calendar interval that spans at most 1px; for linear scales,
16+
* uses a uniform step; for non-linear scales (where the data density varies),
17+
* computes the step locally.
18+
*/
19+
export function pixelRound(scale: MaterializedScale): (value: any) => any;

src/precision.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import {tickStep, timeTickInterval, utcTickInterval} from "d3";
2+
import {numberInterval} from "./options.js";
3+
4+
export function pixelRound(scale) {
5+
if (scale.type === "identity") return Math.round;
6+
if (!scale.invert) throw new Error(`Unsupported scale ${scale.type}`);
7+
const [d0, d1] = scale.domain();
8+
const r = scale.range();
9+
const span = Math.abs(r[1] - r[0]);
10+
return !span
11+
? (v) => v
12+
: scale.type === "linear"
13+
? niceRound(tickStep(0, Math.abs(d1 - d0) / span, 2))
14+
: scale.type === "utc" || scale.type === "time"
15+
? temporalPrecision(scale, d0, d1, span)
16+
: (v) => niceRound(tickStep(0, Math.abs(scale.invert(scale(v) + 0.5) - v), 2))(v);
17+
}
18+
19+
// Find the coarsest calendar interval whose offset spans at most 1px;
20+
// fall back to identity for sub-millisecond domains. The multipliers
21+
// 1, 1.5, 2, 2.5 cover the possible ratios between adjacent intervals.
22+
function temporalPrecision(scale, d0, d1, span) {
23+
const tickInterval = scale.type === "utc" ? utcTickInterval : timeTickInterval;
24+
const p0 = scale(d0);
25+
for (let k = 1; k <= 2.5; k += 0.5) {
26+
const interval = tickInterval(d0, d1, k * span);
27+
if (!interval) break;
28+
if (Math.abs(scale(interval.offset(d0)) - p0) <= 1) return interval.round;
29+
}
30+
return (v) => v;
31+
}
32+
33+
function niceRound(step) {
34+
const {floor} = numberInterval(step);
35+
return (v) => floor(+v + step / 2);
36+
}

test/output/arcCollatzUp.svg

Lines changed: 37 additions & 37 deletions
Loading

0 commit comments

Comments
 (0)