Skip to content

Commit 26add8c

Browse files
committed
pointer interaction
1 parent c18a6eb commit 26add8c

File tree

9 files changed

+98
-28
lines changed

9 files changed

+98
-28
lines changed

src/context.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ export interface Context {
88
*/
99
document: Document;
1010

11+
/** The current owner SVG element. */
12+
ownerSVGElement: SVGSVGElement;
13+
1114
/** The Plot’s (typically generated) class name, for custom styles. */
1215
className: string;
1316

src/context.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import {createProjection} from "./projection.js";
33

44
export function createContext(options = {}, dimensions, className) {
55
const {document = typeof window !== "undefined" ? window.document : undefined} = options;
6-
return {document, className, projection: createProjection(options, dimensions)};
6+
const ownerSVGElement = creator("svg").call(document.documentElement);
7+
const projection = createProjection(options, dimensions);
8+
return {document, ownerSVGElement, className, projection};
79
}
810

911
export function create(name, {document}) {

src/index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,4 @@ export * from "./transforms/select.js";
5252
export * from "./transforms/stack.js";
5353
export * from "./transforms/tree.js";
5454
export * from "./transforms/window.js";
55+
export * from "./interactions/pointer.js";

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export {window, windowX, windowY} from "./transforms/window.js";
4040
export {select, selectFirst, selectLast, selectMaxX, selectMaxY, selectMinX, selectMinY} from "./transforms/select.js";
4141
export {stackX, stackX1, stackX2, stackY, stackY1, stackY2} from "./transforms/stack.js";
4242
export {treeNode, treeLink} from "./transforms/tree.js";
43+
export {pointer} from "./interactions/pointer.js";
4344
export {formatIsoDate, formatWeekday, formatMonth} from "./format.js";
4445
export {scale} from "./scales.js";
4546
export {legend} from "./legends.js";

src/interactions/pointer.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import type {Rendered} from "../transforms/basic.js";
2+
3+
/** TODO */
4+
export function pointer<T>(options: T): Rendered<T>;

src/interactions/pointer.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import {pointer as pointof} from "d3";
2+
3+
export function pointer(options) {
4+
return {
5+
...options,
6+
render(index, scales, values, dimensions, context) {
7+
const mark = this;
8+
const svg = context.ownerSVGElement;
9+
const {x: X, y: Y, x1: X1, y1: Y1, x2: X2, y2: Y2} = values;
10+
// let sticky = false; // TODO
11+
const maxRadius = 40; // TODO option
12+
const [cx, cy] = [0, 0]; // TODO applyFrameAnchor
13+
const [kx, ky] = [1, 1]; // TODO axis option
14+
let i; // currently focused index
15+
let g; // currently rendered mark
16+
17+
function render(ii) {
18+
if (i === ii) return; // abort if the tooltip hasn’t moved
19+
i = ii;
20+
const r = mark._render(i == null ? [] : [i], scales, values, dimensions, context);
21+
if (g) g.replaceWith(r);
22+
return (g = r);
23+
}
24+
25+
function pointermove(event) {
26+
// if (sticky || (event.pointerType === "mouse" && event.buttons === 1)) return; // dragging
27+
const rect = svg.getBoundingClientRect();
28+
let ii = null;
29+
if (
30+
// Check if the pointer is near before scanning.
31+
event.clientX + maxRadius > rect.left &&
32+
event.clientX - maxRadius < rect.right &&
33+
event.clientY + maxRadius > rect.top &&
34+
event.clientY - maxRadius < rect.bottom
35+
) {
36+
const [xp, yp] = pointof(event, g.parentNode);
37+
let ri = maxRadius * maxRadius;
38+
for (const j of index) {
39+
const xj = X2 ? (X1[j] + X2[j]) / 2 : X ? X[j] : cx; // + oxj;
40+
const yj = Y2 ? (Y1[j] + Y2[j]) / 2 : Y ? Y[j] : cy; // + oyj;
41+
const dx = kx * (xj - xp);
42+
const dy = ky * (yj - yp);
43+
const rj = dx * dx + dy * dy;
44+
if (rj <= ri) (ii = j), (ri = rj);
45+
}
46+
}
47+
render(ii);
48+
}
49+
50+
// function pointerdown(event) {
51+
// if (event.pointerType !== "mouse") return;
52+
// if (sticky && tip.node().contains(event.target)) return; // stay sticky
53+
// if (sticky) (sticky = false), tip.attr("display", "none");
54+
// else if (i !== undefined) sticky = true;
55+
// }
56+
57+
function pointerleave(event) {
58+
if (event.pointerType !== "mouse") return;
59+
// if (!sticky) tip.attr("display", "none");
60+
render(null);
61+
}
62+
63+
// We listen to the svg element; listening to the window instead would let
64+
// us receive pointer events from farther away, but would also make it
65+
// hard to know when to remove the listeners. (Using a mutation observer
66+
// to watch the entire document is likely too expensive.)
67+
svg.addEventListener("pointerenter", pointermove);
68+
svg.addEventListener("pointermove", pointermove);
69+
// svg.addEventListener("pointerdown", pointerdown);
70+
svg.addEventListener("pointerleave", pointerleave);
71+
72+
return render(null);
73+
}
74+
};
75+
}

src/plot.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ export function plot(options = {}) {
142142
const subdimensions = fx || fy ? innerDimensions(scaleDescriptors, dimensions) : dimensions;
143143
const superdimensions = fx || fy ? actualDimensions(scales, dimensions) : dimensions;
144144
const context = createContext(options, subdimensions, className);
145+
const svg = context.ownerSVGElement;
145146

146147
// Reinitialize; for deriving channels dependent on other channels.
147148
const newByScale = new Set();
@@ -204,7 +205,7 @@ export function plot(options = {}) {
204205

205206
const {width, height} = dimensions;
206207

207-
const svg = create("svg", context)
208+
select(svg)
208209
.attr("class", className)
209210
.attr("fill", "currentColor")
210211
.attr("font-family", "system-ui, sans-serif")
@@ -231,11 +232,7 @@ export function plot(options = {}) {
231232
}`
232233
)
233234
)
234-
.call(applyInlineStyles, style)
235-
.node();
236-
237-
// TODO Cleaner.
238-
context.ownerSVGElement = svg;
235+
.call(applyInlineStyles, style);
239236

240237
// Render facets.
241238
if (facets !== undefined) {

src/transforms/basic.d.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import type {PlotOptions} from "../plot.js";
21
import type {ChannelName, Channels, ChannelValue} from "../channel.js";
32
import type {Context} from "../context.js";
43
import type {Dimensions} from "../dimensions.js";
4+
import type {RenderFunction} from "../mark.js";
5+
import type {PlotOptions} from "../plot.js";
56
import type {ScaleFunctions} from "../scales.js";
67

78
/**
@@ -67,6 +68,9 @@ export type Transformed<T> = T & {transform: TransformFunction};
6768
/** Mark options with a mark initializer. */
6869
export type Initialized<T> = T & {initializer: InitializerFunction};
6970

71+
/** Mark options with a mark render transform. */
72+
export type Rendered<T> = T & {render: RenderFunction};
73+
7074
/**
7175
* Given an *options* object that may specify some basic transforms (**filter**,
7276
* **sort**, or **reverse**) or a custom **transform**, composes those

test/plots/tip.ts

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,9 @@ export async function tipDot() {
55
const penguins = await d3.csv<any>("data/penguins.csv", d3.autoType);
66
return Plot.plot({
77
marks: [
8-
Plot.dot(penguins, {
9-
x: "culmen_length_mm",
10-
y: "culmen_depth_mm"
11-
}),
12-
Plot.tip(penguins, {
13-
x: "culmen_length_mm",
14-
y: "culmen_depth_mm",
15-
render(index, ...args) {
16-
const mark = this;
17-
let i = 0;
18-
index = d3.sort(index, (i) => penguins[i].culmen_length_mm);
19-
let g = mark._render([index[i]], ...args);
20-
setTimeout(function tick() {
21-
if (!g.isConnected) return;
22-
g.replaceWith((g = mark._render([index[(i = (i + 1) % index.length)]], ...args)));
23-
setTimeout(tick, 100);
24-
}, 100);
25-
return g;
26-
}
27-
})
8+
Plot.dot(penguins, {x: "culmen_length_mm", y: "culmen_depth_mm"}),
9+
Plot.dot(penguins, Plot.pointer({x: "culmen_length_mm", y: "culmen_depth_mm", r: 4, stroke: "red"})),
10+
Plot.tip(penguins, Plot.pointer({x: "culmen_length_mm", y: "culmen_depth_mm"}))
2811
]
2912
});
3013
}

0 commit comments

Comments
 (0)