diff --git a/src/marks/tip.js b/src/marks/tip.js
index a23bedb994..68c8c56de3 100644
--- a/src/marks/tip.js
+++ b/src/marks/tip.js
@@ -231,9 +231,19 @@ 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"
+ : fitTop
+ ? "top"
+ : fitBottom
+ ? "bottom"
: 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..bd714cc386
--- /dev/null
+++ b/test/output/tipAnchorOverflow.svg
@@ -0,0 +1,29 @@
+
\ No newline at end of file
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 @@
+
\ 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 @@
+
\ No newline at end of file
diff --git a/test/plots/index.ts b/test/plots/index.ts
index 3257fc4698..49b43e85bc 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.js";
import "./tip-format.js";
import "./tip.js";
import "./title.js";
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))});
+});