From ee458836bda6786bca2f7014bc60b11877a107e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Tue, 1 Aug 2023 13:05:40 +0200 Subject: [PATCH 1/6] Return the usual euclidian distance to determine the winner across facets, not the squashed distance. fixes #1776 --- src/interactions/pointer.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/interactions/pointer.js b/src/interactions/pointer.js index 5ca1f858a5..d780076ae2 100644 --- a/src/interactions/pointer.js +++ b/src/interactions/pointer.js @@ -130,6 +130,10 @@ function pointerK(kx, ky, {x, y, px, py, maxRadius = 40, channels, render, ...op return r; } + // Select the closest point to the mouse in the current facet; for + // pointerX or pointerY, the orthogonal component of the distance is + // squashed, selecting primarily on the dominant dimension. Return the + // usual euclidian distance to determine the winner across facets. function pointermove(event) { if (state.sticky || (event.pointerType === "mouse" && event.buttons === 1)) return; // dragging let [xp, yp] = pointof(event); @@ -142,7 +146,7 @@ function pointerK(kx, ky, {x, y, px, py, maxRadius = 40, channels, render, ...op const rj = dx * dx + dy * dy; if (rj <= ri) (ii = j), (ri = rj); } - update(ii, ri); + update(ii, Math.hypot(px(ii) - xp, py(ii) - yp)); } function pointerdown(event) { From 1625ceb69ebebe564ac6a38228f6975fbf8dd2d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Tue, 1 Aug 2023 18:44:26 +0200 Subject: [PATCH 2/6] a test case that makes sense: bitemporal libor projections --- test/output/liborProjectionsFacet.html | 392 +++++++++++++++++++++++++ test/plots/libor-projections.ts | 20 ++ 2 files changed, 412 insertions(+) create mode 100644 test/output/liborProjectionsFacet.html diff --git a/test/output/liborProjectionsFacet.html b/test/output/liborProjectionsFacet.html new file mode 100644 index 0000000000..8c6dbb8dd1 --- /dev/null +++ b/test/output/liborProjectionsFacet.html @@ -0,0 +1,392 @@ +
+
+ + + H1 + + H2 + + H3 + + H4 +
+ + + + 2014 + + + 2015 + + + 2016 + + + 2017 + + + 2018 + + + 2019 + + + 2020 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 1 + 2 + 3 + 4 + + + 0 + 1 + 2 + 3 + 4 + + + 0 + 1 + 2 + 3 + 4 + + + 0 + 1 + 2 + 3 + 4 + + + 0 + 1 + 2 + 3 + 4 + + + 0 + 1 + 2 + 3 + 4 + + + 0 + 1 + 2 + 3 + 4 + + + + rate (%) ↑ + + + + + + + + + + + + + + + + 2016 + 2018 + 2020 + 2022 + 2024 + 2026 + 2028 + 2030 + + + + about → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/test/plots/libor-projections.ts b/test/plots/libor-projections.ts index 78001de3e6..ea8f24a512 100644 --- a/test/plots/libor-projections.ts +++ b/test/plots/libor-projections.ts @@ -24,3 +24,23 @@ export async function liborProjections() { y: {grid: true, line: true} }); } + +export async function liborProjectionsFacet() { + const libor = await d3.csv("data/libor-projections.csv", d3.autoType); + return Plot.plot({ + fy: {tickFormat: "d"}, + y: {percent: true, nice: true, grid: true, axis: "right", label: "rate (%)"}, + color: {legend: true}, + marks: [ + Plot.frame(), + Plot.lineY(libor, { + markerStart: true, + fy: (d) => d.on.getFullYear(), + x: "about", + stroke: (d) => "H" + (1 + d3.utcMonth.count(d3.utcYear(d.on), d.on)) / 3, + y: "value", + tip: true + }) + ] + }); +} From e8ab37397b897f02260402f7b7d366067709fe3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 7 Aug 2023 12:25:10 +0200 Subject: [PATCH 3/6] don't attempt to compute a NaN distance when ii is nullish --- src/interactions/pointer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interactions/pointer.js b/src/interactions/pointer.js index d780076ae2..a718db01cc 100644 --- a/src/interactions/pointer.js +++ b/src/interactions/pointer.js @@ -146,7 +146,7 @@ function pointerK(kx, ky, {x, y, px, py, maxRadius = 40, channels, render, ...op const rj = dx * dx + dy * dy; if (rj <= ri) (ii = j), (ri = rj); } - update(ii, Math.hypot(px(ii) - xp, py(ii) - yp)); + update(ii, ii != null && Math.hypot(px(ii) - xp, py(ii) - yp)); } function pointerdown(event) { From 48231877929ae9c0f08eaa326d2de493c1a8173a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Mon, 7 Aug 2023 15:41:46 +0200 Subject: [PATCH 4/6] when the pointer is outside the frame, filter out the winning values that are further than the euclidian distance this solves the weirdness of pointer selection detailed in https://github.com/observablehq/plot/pull/1777#issuecomment-1667796056 --- src/interactions/pointer.js | 18 +++- test/output/tipFacetX.svg | 201 ++++++++++++++++++++++++++++++++++++ test/plots/tip.ts | 19 ++++ 3 files changed, 235 insertions(+), 3 deletions(-) create mode 100644 test/output/tipFacetX.svg diff --git a/src/interactions/pointer.js b/src/interactions/pointer.js index a718db01cc..9f450077d5 100644 --- a/src/interactions/pointer.js +++ b/src/interactions/pointer.js @@ -132,11 +132,17 @@ function pointerK(kx, ky, {x, y, px, py, maxRadius = 40, channels, render, ...op // Select the closest point to the mouse in the current facet; for // pointerX or pointerY, the orthogonal component of the distance is - // squashed, selecting primarily on the dominant dimension. Return the - // usual euclidian distance to determine the winner across facets. + // squashed, selecting primarily on the dominant dimension. Across facets, + // use the euclidian distance to determine the winner. The current facet’s + // surface includes the frame, margins and pointer radius. function pointermove(event) { if (state.sticky || (event.pointerType === "mouse" && event.buttons === 1)) return; // dragging let [xp, yp] = pointof(event); + const hw = dimensions.width / 2; + const hh = dimensions.height / 2; + const offFrame = + (kx !== 1 && fx && Math.abs(xp - (fx(index.fx) + hw)) > hw + maxRadius) || + (ky !== 1 && fy && Math.abs(yp - (fy(index.fy) + hh)) > hh + maxRadius); (xp -= tx), (yp -= ty); // correct for facets and band scales let ii = null; let ri = maxRadius * maxRadius; @@ -146,7 +152,13 @@ function pointerK(kx, ky, {x, y, px, py, maxRadius = 40, channels, render, ...op const rj = dx * dx + dy * dy; if (rj <= ri) (ii = j), (ri = rj); } - update(ii, ii != null && Math.hypot(px(ii) - xp, py(ii) - yp)); + if (ii != null) { + const dx = px(ii) - xp; + const dy = py(ii) - yp; + ri = dx * dx + dy * dy; + if (offFrame && ri > maxRadius * maxRadius) ii = null; + } + update(ii, ri); } function pointerdown(event) { diff --git a/test/output/tipFacetX.svg b/test/output/tipFacetX.svg new file mode 100644 index 0000000000..2625017782 --- /dev/null +++ b/test/output/tipFacetX.svg @@ -0,0 +1,201 @@ + + + + + a + + + b + + + + f + + + + + + + + + + + + + + + + + + 0 + 2 + 4 + 6 + + + 0 + 2 + 4 + 6 + + + + ↑ y + + + + + + + + + + + + + 0 + 20 + 40 + 60 + 80 + + + + x → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/tip.ts b/test/plots/tip.ts index d2b1b78c0d..41bcaf5105 100644 --- a/test/plots/tip.ts +++ b/test/plots/tip.ts @@ -237,3 +237,22 @@ export async function tipTransform() { marks: [Plot.dotX([0, 0.1, 0.3, 1], {fill: Plot.identity, r: 10, frameAnchor: "middle", tip: true})] }); } + +export async function tipFacetX() { + const data = d3.range(100).map((i) => ({f: i > 60 || i % 2 ? "b" : "a", x: i, y: i / 10})); + return Plot.plot({ + inset: 10, + y: {domain: [0, 7]}, + marks: [ + Plot.frame(), + Plot.dot(data, {fy: "f", x: "x", y: "y", tip: "x", fill: "f"}), + Plot.dot( + [ + {f: "a", y: 3}, + {f: "b", y: 1} + ], + {fy: "f", x: 90, y: "y", r: 30, fill: "f", fillOpacity: 0.1, stroke: "currentColor", strokeDasharray: 4} + ) + ] + }); +} From ff48dc52296870644770f3b4d9d9563fbbd401bb Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 10 Aug 2023 13:25:51 -0700 Subject: [PATCH 5/6] only squash within the frame --- src/interactions/pointer.js | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/interactions/pointer.js b/src/interactions/pointer.js index 9f450077d5..c92addcff7 100644 --- a/src/interactions/pointer.js +++ b/src/interactions/pointer.js @@ -133,30 +133,25 @@ function pointerK(kx, ky, {x, y, px, py, maxRadius = 40, channels, render, ...op // Select the closest point to the mouse in the current facet; for // pointerX or pointerY, the orthogonal component of the distance is // squashed, selecting primarily on the dominant dimension. Across facets, - // use the euclidian distance to determine the winner. The current facet’s - // surface includes the frame, margins and pointer radius. + // use unsquashed distance to determine the winner. function pointermove(event) { if (state.sticky || (event.pointerType === "mouse" && event.buttons === 1)) return; // dragging let [xp, yp] = pointof(event); - const hw = dimensions.width / 2; - const hh = dimensions.height / 2; - const offFrame = - (kx !== 1 && fx && Math.abs(xp - (fx(index.fx) + hw)) > hw + maxRadius) || - (ky !== 1 && fy && Math.abs(yp - (fy(index.fy) + hh)) > hh + maxRadius); (xp -= tx), (yp -= ty); // correct for facets and band scales + const kpx = xp < dimensions.marginLeft || xp > dimensions.width - dimensions.marginRight ? 1 : kx; + const kpy = yp < dimensions.marginTop || yp > dimensions.height - dimensions.marginBottom ? 1 : ky; 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 dx = kpx * (px(j) - xp); + const dy = kpy * (py(j) - yp); const rj = dx * dx + dy * dy; if (rj <= ri) (ii = j), (ri = rj); } - if (ii != null) { + if (ii != null && (kx !== 1 || ky !== 1)) { const dx = px(ii) - xp; const dy = py(ii) - yp; ri = dx * dx + dy * dy; - if (offFrame && ri > maxRadius * maxRadius) ii = null; } update(ii, ri); } From e73c71b0e6cc1800b2cb94d506021cec22d4a8ac Mon Sep 17 00:00:00 2001 From: Mike Bostock Date: Thu, 10 Aug 2023 13:27:49 -0700 Subject: [PATCH 6/6] fix test --- test/output/liborProjectionsFacet.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/output/liborProjectionsFacet.html b/test/output/liborProjectionsFacet.html index 8c6dbb8dd1..1469444943 100644 --- a/test/output/liborProjectionsFacet.html +++ b/test/output/liborProjectionsFacet.html @@ -1,4 +1,4 @@ -
+