-
Notifications
You must be signed in to change notification settings - Fork 184
tip mark + pointer interaction #1527
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
56 commits
Select commit
Hold shift + click to select a range
c4723ff
tip mark
mbostock c4e627c
render transform!
mbostock 5a50933
pointer interaction
mbostock 56a23ef
port mbostock/tooltip fixes
mbostock 568d218
improved tip & pointer
mbostock 9854e0d
simplify
mbostock 98be543
better facets; stable anchor
mbostock 778b38b
px, py; crosshairs
mbostock a054cf4
crosshairs composite mark
mbostock fbdc96c
crosshair singular
mbostock 85771cf
transpose facets and marks
mbostock 532a605
prefer top-left
mbostock e6c1ab3
only pass index.f[xyi] if faceted
mbostock 9a69230
[xy][12]; don’t apply stroke as text fill
mbostock 5645501
tip + hexbin test
mbostock 383fcb9
optimize faceting by swapping transforms
mbostock 992ad54
renderTransform instead of _render
mbostock 57bd084
only one pointer across facets
mbostock 72a0d40
suppress other facets when sticky
mbostock da1df91
prevent duplicate ARIA when faceting
mbostock 8174cfa
isolate state per-pointer
mbostock e0ac0e0
fix crash with one-dimensional tip
mbostock c01a775
if px, default x to null; same for py
mbostock 9282918
tidier crosshair options
mbostock 0ede492
use channel label if available
mbostock 42a6d78
only separating space if named
mbostock 5759945
crosshair initializer fixes
mbostock 50a136a
tidier crosshair options
mbostock 0d15771
remove to-do
mbostock 8ec7a31
tip + dodge test
mbostock 8b6f84e
cleaner facet translate
mbostock c385d6b
crosshair text using channel alias
mbostock 736b45d
preTtier
mbostock 99d4554
fix transform for [xy][12]
mbostock 0521e70
p[xy] precedence
mbostock f6e68dd
pointer comments
mbostock 98ad85b
tip textAnchor
mbostock d24bb0a
more tip options
mbostock 6512367
more tip options, comments
mbostock f40792f
bandwidth offset
mbostock 45d1d43
fix for multi-facet, multi-pointer
mbostock 00be814
fix dimensions
mbostock c8417e3
tip side anchors
mbostock 5b52428
tipped helper
mbostock c394c6e
raster nearest
mbostock 3365727
color swatch; fix f[xy]; no tip aesthetic channels
mbostock 4570bb9
multi-line, summary ariaLabel
mbostock 36aa6b1
tidier formatting
mbostock 383cb40
tidier crosshair
mbostock c0ec5c5
project p[xy], too
mbostock 5f9c77e
centroid test
mbostock 74e8423
geoCentroid test
mbostock 0003e4f
shorthand extra channels
mbostock 727c45b
no pointer-specific state
mbostock 3921672
revert Mark interface change
mbostock 7faaee3
remove dead code
mbostock File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import type {Rendered} from "../transforms/basic.js"; | ||
|
||
/** TODO */ | ||
export interface PointerOptions { | ||
/** TODO */ | ||
maxRadius?: number; | ||
} | ||
|
||
/** TODO */ | ||
export function pointer<T>(options: T & PointerOptions): Rendered<T>; | ||
|
||
/** TODO */ | ||
export function pointerX<T>(options: T & PointerOptions): Rendered<T>; | ||
|
||
/** TODO */ | ||
export function pointerY<T>(options: T & PointerOptions): Rendered<T>; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
import {pointer as pointof} from "d3"; | ||
import {applyFrameAnchor} from "../style.js"; | ||
|
||
const states = new WeakMap(); | ||
|
||
function pointerK(kx, ky, {x, y, px, py, maxRadius = 40, channels, ...options} = {}) { | ||
maxRadius = +maxRadius; | ||
// When px or py is used, register an extra channel that the pointer | ||
// interaction can use to control which point is focused; this allows pointing | ||
// to function independently of where the downstream mark (e.g., a tip) is | ||
// displayed. Also default x or y to null to disable maybeTuple etc. | ||
if (px != null) (x ??= null), (channels = {...channels, px: {value: px, scale: "x"}}); | ||
if (py != null) (y ??= null), (channels = {...channels, py: {value: py, scale: "y"}}); | ||
return { | ||
x, | ||
y, | ||
channels, | ||
...options, | ||
render(index, scales, values, dimensions, context) { | ||
const mark = this; | ||
const svg = context.ownerSVGElement; | ||
|
||
// Isolate state per-pointer, per-plot; if the pointer is reused by | ||
// multiple marks, they will share the same state (e.g., sticky modality). | ||
let state = states.get(svg); | ||
if (!state) states.set(svg, (state = {sticky: false, roots: [], renders: []})); | ||
|
||
// This serves as a unique identifier of the rendered mark per-plot; it is | ||
// used to record the currently-rendered elements (state.roots) so that we | ||
// can tell when a rendered element is clicked on. | ||
let renderIndex = state.renders.push(render) - 1; | ||
|
||
// For faceting, we want to compute the local coordinates of each point, | ||
// which means subtracting out the facet translation, if any. (It’s | ||
// tempting to do this using the local coordinates in SVG, but that’s | ||
// complicated by mark-specific transforms such as dx and dy.) Also, since | ||
// band scales return the upper bound of the band, we have to offset by | ||
// half the bandwidth. | ||
const {x, y, fx, fy} = scales; | ||
let tx = fx ? fx(index.fx) - dimensions.marginLeft : 0; | ||
let ty = fy ? fy(index.fy) - dimensions.marginTop : 0; | ||
if (x?.bandwidth) tx += x.bandwidth() / 2; | ||
if (y?.bandwidth) ty += y.bandwidth() / 2; | ||
|
||
// For faceting, we also need to record the closest point per facet per | ||
// mark (!), since each facet has its own pointer event listeners; we only | ||
// want the closest point across facets to be visible. | ||
const faceted = index.fi != null; | ||
let facetState; | ||
if (faceted) { | ||
let facetStates = state.facetStates; | ||
if (!facetStates) state.facetStates = facetStates = new Map(); | ||
facetState = facetStates.get(mark); | ||
if (!facetState) facetStates.set(mark, (facetState = new Map())); | ||
} | ||
|
||
// The order of precedence when determining the point position is: px & | ||
// py; the middle of x1 & y1 and x2 & y2; or lastly x & y. If any | ||
// dimension is unspecified, we fallback to the frame anchor. | ||
const {x: X, y: Y, x1: X1, y1: Y1, x2: X2, y2: Y2, px: PX, py: PY} = values; | ||
const [cx, cy] = applyFrameAnchor(this, dimensions); | ||
const px = PX ? (i) => PX[i] : X2 ? (i) => (X1[i] + X2[i]) / 2 : X ? (i) => X[i] : () => cx; | ||
const py = PY ? (i) => PY[i] : Y2 ? (i) => (Y1[i] + Y2[i]) / 2 : Y ? (i) => Y[i] : () => cy; | ||
|
||
let i; // currently focused index | ||
let g; // currently rendered mark | ||
let f; // current animation frame | ||
|
||
// When faceting, if more than one pointer would be visible, only show | ||
// this one if it is the closest. We defer rendering using an animation | ||
// frame to allow all pointer events to be received before deciding which | ||
// mark to render; although when hiding, we render immediately. | ||
function update(ii, ri) { | ||
if (faceted) { | ||
if (f) f = cancelAnimationFrame(f); | ||
if (ii == null) facetState.delete(index.fi); | ||
else { | ||
facetState.set(index.fi, ri); | ||
f = requestAnimationFrame(() => { | ||
f = null; | ||
for (const r of facetState.values()) { | ||
if (r < ri) { | ||
ii = null; | ||
break; | ||
} | ||
} | ||
render(ii); | ||
}); | ||
return; | ||
} | ||
} | ||
render(ii); | ||
} | ||
|
||
function render(ii) { | ||
if (i === ii) return; // the tooltip hasn’t moved | ||
i = ii; | ||
const I = i == null ? [] : [i]; | ||
if (faceted) (I.fx = index.fx), (I.fy = index.fy), (I.fi = index.fi); | ||
const r = mark.render(I, scales, values, dimensions, context); | ||
if (g) { | ||
// When faceting, preserve swapped mark and facet transforms; also | ||
// remove ARIA attributes since these are promoted to the parent. This | ||
// is perhaps brittle in that it depends on how Plot renders facets, | ||
// but it produces a cleaner and more accessible SVG structure. | ||
if (faceted) { | ||
const p = g.parentNode; | ||
const ft = g.getAttribute("transform"); | ||
const mt = r.getAttribute("transform"); | ||
ft ? r.setAttribute("transform", ft) : r.removeAttribute("transform"); | ||
mt ? p.setAttribute("transform", mt) : p.removeAttribute("transform"); | ||
r.removeAttribute("aria-label"); | ||
r.removeAttribute("aria-description"); | ||
r.removeAttribute("aria-hidden"); | ||
} | ||
g.replaceWith(r); | ||
} | ||
state.roots[renderIndex] = r; | ||
return (g = r); | ||
} | ||
|
||
function pointermove(event) { | ||
if (state.sticky || (event.pointerType === "mouse" && event.buttons === 1)) return; // dragging | ||
let [xp, yp] = pointof(event); | ||
(xp -= tx), (yp -= ty); // correct for facets and band scales | ||
let ii = null; | ||
let ri = maxRadius * maxRadius; | ||
for (const j of index) { | ||
const dx = kx * (px(j) - xp); | ||
const dy = ky * (py(j) - yp); | ||
const rj = dx * dx + dy * dy; | ||
if (rj <= ri) (ii = j), (ri = rj); | ||
} | ||
update(ii, ri); | ||
} | ||
|
||
function pointerdown(event) { | ||
if (event.pointerType !== "mouse") return; | ||
if (i == null) return; // not pointing | ||
if (state.sticky && state.roots.some((r) => r?.contains(event.target))) return; // stay sticky | ||
if (state.sticky) (state.sticky = false), state.renders.forEach((r) => r(null)); // clear all pointers | ||
else state.sticky = true; | ||
event.stopImmediatePropagation(); // suppress other pointers | ||
} | ||
|
||
function pointerleave(event) { | ||
if (event.pointerType !== "mouse") return; | ||
if (!state.sticky) update(null); | ||
} | ||
|
||
// We listen to the svg element; listening to the window instead would let | ||
// us receive pointer events from farther away, but would also make it | ||
// hard to know when to remove the listeners. (Using a mutation observer | ||
// to watch the entire document is likely too expensive.) | ||
svg.addEventListener("pointerenter", pointermove); | ||
svg.addEventListener("pointermove", pointermove); | ||
svg.addEventListener("pointerdown", pointerdown); | ||
svg.addEventListener("pointerleave", pointerleave); | ||
|
||
return render(null); | ||
} | ||
}; | ||
} | ||
|
||
export function pointer(options) { | ||
return pointerK(1, 1, options); | ||
} | ||
|
||
export function pointerX(options) { | ||
return pointerK(1, 0.01, options); | ||
} | ||
|
||
export function pointerY(options) { | ||
return pointerK(0.01, 1, options); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.