Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tooltip-clip-bounds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@human-kit/charts': patch
---

A `Chart.Tooltip` that follows the pointer stays in the nearest ancestor that clips it, not in the page: a page frame with `overflow: hidden` no longer cuts it.
2 changes: 1 addition & 1 deletion docs/src/content/tooltip/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The tooltip is HTML, thus it goes in `Chart.Root` and not in `Chart.Plot`. It ma

`offset` is the distance between the point and the tooltip, in pixels. The tooltip is above the point, and its middle is at the x position of the point.

With `follow`, the tooltip goes to the pointer and moves with it, over the point under the pointer. Use it for a large mark, as a long bar, whose position can be far from the pointer. The point that has the keyboard focus keeps its tooltip at the point. A tooltip that follows the pointer stays in the width of the page instead of the figure: on a figure narrower than the tooltip, it still goes where the pointer goes.
With `follow`, the tooltip goes to the pointer and moves with it, over the point under the pointer. Use it for a large mark, as a long bar, whose position can be far from the pointer. The point that has the keyboard focus keeps its tooltip at the point. A tooltip that follows the pointer stays in the box that would cut it (the nearest ancestor whose `overflow` is not `visible`, or the page) instead of the figure: on a figure narrower than the tooltip, it still goes where the pointer goes.

## Content of your own

Expand Down
21 changes: 16 additions & 5 deletions packages/charts/src/lib/tooltip/chart-tooltip.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,8 @@
// The figure is the containing block of the tooltip, thus the position adds the distance from
// the padding edge of the figure to the plot. The tooltip shows only in a browser.
// `bounds` is the space the tooltip stays in, from the padding edge of the figure: the width of
// the figure, or the width of the page for a tooltip that follows the pointer, which must go
// wherever the pointer goes on a figure narrower than itself.
// the figure, or for a tooltip that follows the pointer —which must go wherever the pointer goes
// on a figure narrower than itself— the width of the box that would cut it.
const placement = $derived.by(() => {
const plot = ctx.plotElement;
const figure = plot?.closest('figure');
Expand All @@ -105,12 +105,23 @@
const start = b.left + figure.clientLeft;
return {
offset: [a.left - start, a.top - b.top - figure.clientTop],
bounds: follow
? [-start, document.documentElement.clientWidth - start]
: [0, figure.clientWidth]
bounds: follow ? clipBounds(figure).map((x) => x - start) : [0, figure.clientWidth]
};
});

/**
* The left and the right of the nearest ancestor that cuts what overflows it, on the screen,
* or of the page without one.
*/
function clipBounds(figure: Element): [number, number] {
for (let node = figure.parentElement; node; node = node.parentElement) {
if (getComputedStyle(node).overflowX === 'visible') continue;
const box = node.getBoundingClientRect();
return [box.left + node.clientLeft, box.left + node.clientLeft + node.clientWidth];
}
return [0, document.documentElement.clientWidth];
}

// Near an edge of its bounds, the tooltip moves away from the point. Thus it does not make the
// page wider on a narrow screen.
let width = $state(0);
Expand Down
9 changes: 9 additions & 0 deletions packages/charts/src/lib/tooltip/chart-tooltip.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ describe('Chart.Tooltip', () => {
expect(box.right).toBeLessThanOrEqual(document.documentElement.clientWidth + 0.5);
});

it('follows the pointer only as far as the box that cuts it', async () => {
const { container } = render(PartsTest, { follow: true, wide: true });
container.style.cssText = 'overflow: hidden; width: 320px; padding: 0 10px';
const clip = container.getBoundingClientRect();
const point = points()[5];
await hoverAt(Number(point.getAttribute('cx')), Number(point.getAttribute('cy')));
expect(tooltip()!.getBoundingClientRect().right).toBeLessThanOrEqual(clip.right + 0.5);
});

it('keeps the tooltip of the focused point at the point when it follows the pointer', async () => {
render(PartsTest, { follow: true });
// The pointer was in the plot and left it: the focus has no pointer to follow.
Expand Down
Loading