From 353fed862707eef16f8330d443998db41c78f9ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 18 Mar 2026 12:04:21 +0100 Subject: [PATCH 1/4] fixes #2011 --- src/marks/tip.js | 8 ++++- test/output/tipAnchorOverflow.svg | 50 +++++++++++++++++++++++++++++++ test/plots/index.ts | 1 + test/plots/tip-anchor-overflow.ts | 12 ++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 test/output/tipAnchorOverflow.svg create mode 100644 test/plots/tip-anchor-overflow.ts diff --git a/src/marks/tip.js b/src/marks/tip.js index a23bedb994..70a20dcfbc 100644 --- a/src/marks/tip.js +++ b/src/marks/tip.js @@ -231,9 +231,15 @@ export class Tip extends Mark { : fitTop && fitBottom ? fitLeft ? "left" - : "right" + : fitRight + ? "right" + : "bottom" : (fitLeft || fitRight) && (fitTop || fitBottom) ? `${fitBottom ? "bottom" : "top"}-${fitLeft ? "left" : "right"}` + : fitLeft + ? "left" + : fitRight + ? "right" : mark.preferredAnchor; } const path = this.firstChild; // note: assumes exactly two children! diff --git a/test/output/tipAnchorOverflow.svg b/test/output/tipAnchorOverflow.svg new file mode 100644 index 0000000000..f347911520 --- /dev/null +++ b/test/output/tipAnchorOverflow.svg @@ -0,0 +1,50 @@ + + + + + 0.0 + 0.5 + 1.0 + 1.5 + 2.0 + 2.5 + 3.0 + 3.5 + 4.0 + 4.5 + 5.0 + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/index.ts b/test/plots/index.ts index 3257fc4698..d038fdb736 100644 --- a/test/plots/index.ts +++ b/test/plots/index.ts @@ -318,6 +318,7 @@ import "./text-overflow.js"; import "./this-is-just-to-say.js"; import "./tick-format.js"; import "./time-axis.js"; +import "./tip-anchor-overflow.js"; import "./tip-format.js"; import "./tip.js"; import "./title.js"; diff --git a/test/plots/tip-anchor-overflow.ts b/test/plots/tip-anchor-overflow.ts new file mode 100644 index 0000000000..7cb59d381d --- /dev/null +++ b/test/plots/tip-anchor-overflow.ts @@ -0,0 +1,12 @@ +import * as Plot from "@observablehq/plot"; +import {test} from "test/plot"; + +test(async function tipAnchorOverflow() { + return Plot.rectX([1, 1, 1, 1, 1], { + x: Plot.identity, + fill: Plot.indexOf, + title: () => + "Lorem ipsum lorem ipsum lorem ipsum Lorem ipsum lorem ipsum lorem ipsum Lorem ipsum lorem ipsum lorem ipsum", + tip: true + }).plot({height: 100, marginTop: 20}); +}); From 94d8bd77d668370e50cedbc856428b2d93e75659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 15 Apr 2026 10:47:48 +0200 Subject: [PATCH 2/4] better test --- test/output/tipAnchorOverflow.svg | 45 +++++++++---------------------- test/plots/tip-anchor-overflow.ts | 12 +++++++-- 2 files changed, 22 insertions(+), 35 deletions(-) diff --git a/test/output/tipAnchorOverflow.svg b/test/output/tipAnchorOverflow.svg index f347911520..bd714cc386 100644 --- a/test/output/tipAnchorOverflow.svg +++ b/test/output/tipAnchorOverflow.svg @@ -1,4 +1,4 @@ - + - - - 0.0 - 0.5 - 1.0 - 1.5 - 2.0 - 2.5 - 3.0 - 3.5 - 4.0 - 4.5 - 5.0 - - - - - - + + + + + + + + + + ​Lorem ipsum lorem ipsum lorem ipsum​Lorem ipsum lorem ipsum lorem ipsum​Lorem ipsum lorem ipsum lorem ipsum + - \ No newline at end of file diff --git a/test/plots/tip-anchor-overflow.ts b/test/plots/tip-anchor-overflow.ts index 7cb59d381d..03df40490d 100644 --- a/test/plots/tip-anchor-overflow.ts +++ b/test/plots/tip-anchor-overflow.ts @@ -2,11 +2,19 @@ import * as Plot from "@observablehq/plot"; import {test} from "test/plot"; test(async function tipAnchorOverflow() { - return Plot.rectX([1, 1, 1, 1, 1], { + const plot = Plot.rectX([1, 1, 1, 1, 1], { x: Plot.identity, fill: Plot.indexOf, title: () => "Lorem ipsum lorem ipsum lorem ipsum Lorem ipsum lorem ipsum lorem ipsum Lorem ipsum lorem ipsum lorem ipsum", tip: true - }).plot({height: 100, marginTop: 20}); + }).plot({height: 80, marginTop: 20, axis: null}); + plot.dispatchEvent( + new PointerEvent("pointermove", { + pointerType: "mouse", + clientX: 580, + clientY: 30 + }) + ); + return Object.assign(plot, {ready: new Promise((resolve) => setTimeout(resolve, 100))}); }); From 11e932690b2f88f550f0ceb8194f9794c6501353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 15 Apr 2026 10:59:50 +0200 Subject: [PATCH 3/4] same logic for the vertical case + tests --- src/marks/tip.js | 4 ++ test/output/tipAnchorOverflowY.svg | 29 +++++++++ test/output/tipAnchorPositions.svg | 65 ++++++++++++++++++++ test/output/tipAnchors.svg | 98 ------------------------------ test/plots/index.ts | 2 +- test/plots/tip-anchor-overflow.ts | 20 ------ test/plots/tip-anchor.ts | 75 +++++++++++++++++++++++ 7 files changed, 174 insertions(+), 119 deletions(-) create mode 100644 test/output/tipAnchorOverflowY.svg create mode 100644 test/output/tipAnchorPositions.svg delete mode 100644 test/output/tipAnchors.svg delete mode 100644 test/plots/tip-anchor-overflow.ts create mode 100644 test/plots/tip-anchor.ts diff --git a/src/marks/tip.js b/src/marks/tip.js index 70a20dcfbc..68c8c56de3 100644 --- a/src/marks/tip.js +++ b/src/marks/tip.js @@ -240,6 +240,10 @@ export class Tip extends Mark { ? "left" : fitRight ? "right" + : fitTop + ? "top" + : fitBottom + ? "bottom" : mark.preferredAnchor; } const path = this.firstChild; // note: assumes exactly two children! diff --git a/test/output/tipAnchorOverflowY.svg b/test/output/tipAnchorOverflowY.svg new file mode 100644 index 0000000000..bb3d4c1e57 --- /dev/null +++ b/test/output/tipAnchorOverflowY.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + ​Lorem​ipsum​lorem​ipsum​lorem​ipsum​Lorem​ipsum​lorem​ipsum​lorem​ipsum​Lorem​ipsum​lorem​ipsum​lorem​ipsum + + + \ No newline at end of file diff --git a/test/output/tipAnchorPositions.svg b/test/output/tipAnchorPositions.svg new file mode 100644 index 0000000000..940638ae8a --- /dev/null +++ b/test/output/tipAnchorPositions.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + ​A tip that is wide enough to test anchor​fitting behavior across positions + + + + ​A tip that is wide enough to test anchor​fitting behavior across positions + + + + ​A tip that is wide enough to test anchor​fitting behavior across positions + + + + ​A tip that is wide enough to test anchor​fitting behavior across positions + + + + ​A tip that is wide enough to test anchor​fitting behavior across positions + + + + ​A tip that is wide enough to test anchor​fitting behavior across positions + + + + ​A tip that is wide enough to test anchor​fitting behavior across positions + + + + ​A tip that is wide enough to test anchor​fitting behavior across positions + + + + ​A tip that is wide enough to test anchor​fitting behavior across positions + + + \ No newline at end of file diff --git a/test/output/tipAnchors.svg b/test/output/tipAnchors.svg deleted file mode 100644 index b2a3d74ba7..0000000000 --- a/test/output/tipAnchors.svg +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - ​top - - - - - - - - - ​right - - - - - - - - - ​bottom - - - - - - - - - ​left - - - - - - - - - ​top-left - - - - - - - - - ​top-right - - - - - - - - - ​bottom-right - - - - - - - - - ​bottom-left - - - - - - - - - ​middle - - - \ No newline at end of file diff --git a/test/plots/index.ts b/test/plots/index.ts index d038fdb736..49b43e85bc 100644 --- a/test/plots/index.ts +++ b/test/plots/index.ts @@ -318,7 +318,7 @@ import "./text-overflow.js"; import "./this-is-just-to-say.js"; import "./tick-format.js"; import "./time-axis.js"; -import "./tip-anchor-overflow.js"; +import "./tip-anchor.js"; import "./tip-format.js"; import "./tip.js"; import "./title.js"; diff --git a/test/plots/tip-anchor-overflow.ts b/test/plots/tip-anchor-overflow.ts deleted file mode 100644 index 03df40490d..0000000000 --- a/test/plots/tip-anchor-overflow.ts +++ /dev/null @@ -1,20 +0,0 @@ -import * as Plot from "@observablehq/plot"; -import {test} from "test/plot"; - -test(async function tipAnchorOverflow() { - const plot = Plot.rectX([1, 1, 1, 1, 1], { - x: Plot.identity, - fill: Plot.indexOf, - title: () => - "Lorem ipsum lorem ipsum lorem ipsum Lorem ipsum lorem ipsum lorem ipsum Lorem ipsum lorem ipsum lorem ipsum", - tip: true - }).plot({height: 80, marginTop: 20, axis: null}); - plot.dispatchEvent( - new PointerEvent("pointermove", { - pointerType: "mouse", - clientX: 580, - clientY: 30 - }) - ); - return Object.assign(plot, {ready: new Promise((resolve) => setTimeout(resolve, 100))}); -}); diff --git a/test/plots/tip-anchor.ts b/test/plots/tip-anchor.ts new file mode 100644 index 0000000000..8265cc8b3b --- /dev/null +++ b/test/plots/tip-anchor.ts @@ -0,0 +1,75 @@ +import * as Plot from "@observablehq/plot"; +import {test} from "test/plot"; + +// Test tip anchor selection near the right edge of the chart. +test(async function tipAnchorOverflow() { + const plot = Plot.rectX([1, 1, 1, 1, 1], { + x: Plot.identity, + fill: Plot.indexOf, + title: () => + "Lorem ipsum lorem ipsum lorem ipsum Lorem ipsum lorem ipsum lorem ipsum Lorem ipsum lorem ipsum lorem ipsum", + tip: true + }).plot({height: 80, marginTop: 20, axis: null}); + plot.dispatchEvent( + new PointerEvent("pointermove", { + pointerType: "mouse", + clientX: 580, + clientY: 30 + }) + ); + return Object.assign(plot, {ready: new Promise((resolve) => setTimeout(resolve, 100))}); +}); + +// Test tip anchor selection near the bottom edge of the chart. +test(async function tipAnchorOverflowY() { + const plot = Plot.rectY([1, 1, 1, 1, 1], { + y: Plot.identity, + fill: Plot.indexOf, + title: () => + "Lorem ipsum lorem ipsum lorem ipsum Lorem ipsum lorem ipsum lorem ipsum Lorem ipsum lorem ipsum lorem ipsum", + tip: {lineWidth: 5} + }).plot({width: 80, marginLeft: 20, axis: null}); + plot.dispatchEvent( + new PointerEvent("pointermove", { + pointerType: "mouse", + clientX: 30, + clientY: 20 + }) + ); + return Object.assign(plot, {ready: new Promise((resolve) => setTimeout(resolve, 100))}); +}); + +// Test tip anchor selection at all 9 positions across the chart. +test(async function tipAnchorPositions() { + const data = []; + for (const px of [40, 320, 600]) { + for (const py of [20, 100, 180]) { + data.push({px, py}); + } + } + const plot = Plot.plot({ + width: 640, + height: 200, + axis: null, + inset: 20, + marks: [ + Plot.dot(data, {x: "px", y: "py", r: 5, fill: "currentColor"}), + Plot.tip(data, { + x: "px", + y: "py", + title: () => "A tip that is wide enough to test anchor fitting behavior across positions" + }) + ] + }); + // Dispatch a pointer event to trigger all tips + for (const {px, py} of data) { + plot.dispatchEvent( + new PointerEvent("pointermove", { + pointerType: "mouse", + clientX: px, + clientY: py + }) + ); + } + return Object.assign(plot, {ready: new Promise((resolve) => setTimeout(resolve, 100))}); +}); From 4d3fb2243b539430ed15a636ce47ae0037975674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Wed, 15 Apr 2026 11:03:17 +0200 Subject: [PATCH 4/4] add snapshot --- test/output/tipAnchors.svg | 98 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 test/output/tipAnchors.svg diff --git a/test/output/tipAnchors.svg b/test/output/tipAnchors.svg new file mode 100644 index 0000000000..b2a3d74ba7 --- /dev/null +++ b/test/output/tipAnchors.svg @@ -0,0 +1,98 @@ + + + + + + + + + + ​top + + + + + + + + + ​right + + + + + + + + + ​bottom + + + + + + + + + ​left + + + + + + + + + ​top-left + + + + + + + + + ​top-right + + + + + + + + + ​bottom-right + + + + + + + + + ​bottom-left + + + + + + + + + ​middle + + + \ No newline at end of file