Skip to content

Commit 8174cfa

Browse files
committed
isolate state per-pointer
1 parent da1df91 commit 8174cfa

File tree

2 files changed

+26
-26
lines changed

2 files changed

+26
-26
lines changed

src/interactions/pointer.js

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,24 @@ function pointerK(kx, ky, {px, py, maxRadius = 40, channels, ...options} = {}) {
55
maxRadius = +maxRadius;
66
if (px != null) channels = {...channels, px: {value: px, scale: "x"}};
77
if (py != null) channels = {...channels, py: {value: py, scale: "y"}};
8+
const stateBySvg = new WeakMap();
89
return {
910
channels,
1011
...options,
1112
render(index, scales, values, dimensions, context) {
1213
const mark = this;
1314
const svg = context.ownerSVGElement;
15+
16+
// Isolate state per-pointer, per-plot; if the pointer is reused by
17+
// multiple marks, they will share the same state (e.g., sticky modality).
18+
let state = stateBySvg.get(svg);
19+
if (!state) stateBySvg.set(svg, (state = {sticky: false, roots: [], renders: []}));
20+
let renderIndex = state.renders.push(render) - 1;
21+
1422
const faceted = index.fi != null;
15-
const facetState = faceted ? getFacetState(mark, svg) : null;
23+
const facetState = faceted ? (state.facetState ??= new Map()) : null;
1624
const {x: X0, y: Y0, x1: X1, y1: Y1, x2: X2, y2: Y2, px: X = X0, py: Y = Y0} = values;
1725
const [cx, cy] = applyFrameAnchor(this, dimensions);
18-
let sticky = false;
1926
let i; // currently focused index
2027
let g; // currently rendered mark
2128

@@ -58,11 +65,12 @@ function pointerK(kx, ky, {px, py, maxRadius = 40, channels, ...options} = {}) {
5865
}
5966
g.replaceWith(r);
6067
}
68+
state.roots[renderIndex] = r;
6169
return (g = r);
6270
}
6371

6472
function pointermove(event) {
65-
if (sticky || (event.pointerType === "mouse" && event.buttons === 1)) return; // dragging
73+
if (state.sticky || (event.pointerType === "mouse" && event.buttons === 1)) return; // dragging
6674
const [xp, yp] = pointof(event, faceted ? g : g.parentNode);
6775
let ii = null;
6876
let ri = maxRadius * maxRadius;
@@ -79,14 +87,16 @@ function pointerK(kx, ky, {px, py, maxRadius = 40, channels, ...options} = {}) {
7987

8088
function pointerdown(event) {
8189
if (event.pointerType !== "mouse") return;
82-
if (sticky && g.contains(event.target)) return; // stay sticky
83-
if (sticky) (sticky = false), render(null);
84-
else if (i != null) (sticky = true), facetState?.set(index.fi, -1); // suppress other facets
90+
if (i == null) return; // not pointing
91+
if (state.sticky && state.roots.some((r) => r?.contains(event.target))) return; // stay sticky
92+
if (state.sticky) (state.sticky = false), state.renders.forEach((r) => r(null)); // clear all pointers
93+
else state.sticky = true;
94+
event.stopImmediatePropagation(); // suppress other pointers
8595
}
8696

8797
function pointerleave(event) {
8898
if (event.pointerType !== "mouse") return;
89-
if (!sticky) render(null);
99+
if (!state.sticky) render(null);
90100
}
91101

92102
// We listen to the svg element; listening to the window instead would let
@@ -114,16 +124,3 @@ export function pointerX(options) {
114124
export function pointerY(options) {
115125
return pointerK(0.01, 1, options);
116126
}
117-
118-
const facetStateByMark = new WeakMap();
119-
120-
// This isolates facet state per-mark, per-plot. Most of the time a separate
121-
// pointer will be instantiated per mark, but it’s possible to reuse the same
122-
// pointer instance with multiple marks so we protect against it.
123-
function getFacetState(mark, svg) {
124-
let stateBySvg = facetStateByMark.get(mark);
125-
if (!stateBySvg) facetStateByMark.set(mark, (stateBySvg = new WeakMap()));
126-
let state = stateBySvg.get(svg);
127-
if (!state) stateBySvg.set(svg, (state = new Map()));
128-
return state;
129-
}

src/marks/crosshair.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@ import {text} from "./text.js";
55

66
export function crosshair(data, options = {}) {
77
const {x, y, dx = -9, dy = 9} = options;
8+
const p = pointer({px: x, py: y});
89
return marks(
9-
ruleX(data, ruleOptions({x, py: y}, options)),
10-
ruleY(data, ruleOptions({px: x, y}, options)),
11-
text(data, textOptions({px: x, y, text: y, dx, frameAnchor: "left", textAnchor: "end"}, options)),
12-
text(data, textOptions({x, py: y, text: x, dy, frameAnchor: "bottom", lineAnchor: "top"}, options))
10+
ruleX(data, ruleOptions(p, {x}, options)),
11+
ruleY(data, ruleOptions(p, {y}, options)),
12+
text(data, textOptions(p, {y, text: y, dx, frameAnchor: "left", textAnchor: "end"}, options)),
13+
text(data, textOptions(p, {x, text: x, dy, frameAnchor: "bottom", lineAnchor: "top"}, options))
1314
);
1415
}
1516

1617
function ruleOptions(
18+
pointer,
1719
options,
1820
{
1921
color = "currentColor",
@@ -22,10 +24,11 @@ function ruleOptions(
2224
ruleStrokeWidth: strokeWidth
2325
}
2426
) {
25-
return pointer({...options, stroke, strokeOpacity, strokeWidth});
27+
return {...pointer, ...options, stroke, strokeOpacity, strokeWidth};
2628
}
2729

2830
function textOptions(
31+
pointer,
2932
options,
3033
{
3134
color = "currentColor",
@@ -35,5 +38,5 @@ function textOptions(
3538
textStrokeWidth: strokeWidth = 5
3639
}
3740
) {
38-
return pointer({...options, fill, stroke, strokeOpacity, strokeWidth});
41+
return {...pointer, ...options, fill, stroke, strokeOpacity, strokeWidth};
3942
}

0 commit comments

Comments
 (0)