Skip to content

packages/core/src/features/viz/lib/hover-placement.ts

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

1 import type { HoverCardProps } from './hover-card.js';
2 import type { PieLayoutProps } from './pie-types.js';
3 import type { HoverPoint } from "./hover-types.js";
4 /**
5  * Preferred side of an anchor, or automatic placement within available bounds.
6  *
7  * @see {@link HoverCardProps}
8  */
9 export type HoverPlacement = "auto" | "above" | "below" | "right" | "left";
10 /**
11  * Logical bounds used to constrain hover-card placement.
12  *
13  * @see {@link HoverCardProps}
14  * @see {@link PieLayoutProps}
15  */
16 export interface HoverBounds {
17   /**
18    * Horizontal coordinate or displacement in the containing coordinate system. See
19    * {@link HoverBounds}.
20    */
21   readonly x: number;
22   /**
23    * Vertical coordinate or displacement in the containing coordinate system. See
24    * {@link HoverBounds}.
25    */
26   readonly y: number;
27   /**
28    * Horizontal extent in the units of the containing geometry or surface. See {@link HoverBounds}
29    * .
30    */
31   readonly width: number;
32   /**
33    * Vertical extent in the units of the containing geometry or surface. See {@link HoverBounds}.
34    */
35   readonly height: number;
36 }
37 export function placeHoverCard(input: {
38   anchor: HoverPoint;
39   anchorRadius: number;
40   gap: number;
41   bounds: HoverBounds;
42   width: number;
43   height: number;
44   strokeWidth: number;
45   placement: HoverPlacement;
46 }): HoverPoint {
47   const {
48     anchor,
49     anchorRadius,
50     gap,
51     bounds,
52     width,
53     height,
54     strokeWidth,
55     placement,
56   } = input;
57   for (const [name, value] of Object.entries({
58     x: anchor.x,
59     y: anchor.y,
60     boundsX: bounds.x,
61     boundsY: bounds.y,
62     anchorRadius,
63     gap,
64     width,
65     height,
66     strokeWidth,
67     boundsWidth: bounds.width,
68     boundsHeight: bounds.height,
69   })) {
70     if (
71       !Number.isFinite(value) ||
72       (!["x", "y", "boundsX", "boundsY"].includes(name) && value < 0)
73     )
74       throw new RangeError(
75         `HoverCard ${name} must be finite${name.endsWith("X") || name.endsWith("Y") || name === "x" || name === "y" ? "" : " and nonnegative"}.`,
76       );
77   }
78   const sides = ["above", "below", "right", "left"] as const;
79   if (placement !== "auto" && !sides.includes(placement))
80     throw new TypeError("HoverCard placement is invalid.");
81   const order =
82     placement === "auto"
83       ? sides
84       : [placement, ...sides.filter((s) => s !== placement)];
85   const half = strokeWidth / 2,
86     clearance = anchorRadius + gap + half;
87   const slide = (
88     value: number,
89     start: number,
90     available: number,
91     size: number,
92   ) => Math.max(start + half, Math.min(value, start + available - size - half));
93   const candidates = order.map((side) => {
94     const vertical = side === "above" || side === "below";
95     const x = vertical
96       ? slide(anchor.x - width / 2, bounds.x, bounds.width, width)
97       : side === "right"
98         ? anchor.x + clearance
99         : anchor.x - clearance - width;
100     const y = !vertical
101       ? slide(anchor.y - height / 2, bounds.y, bounds.height, height)
102       : side === "below"
103         ? anchor.y + clearance
104         : anchor.y - clearance - height;
105     if (
106       !Number.isFinite(x) ||
107       !Number.isFinite(y) ||
108       !Number.isFinite(x + width + half) ||
109       !Number.isFinite(y + height + half)
110     )
111       throw new RangeError("HoverCard placement arithmetic must be finite.");
112     return { x, y };
113   });
114   return (
115     candidates.find(
116       (p) =>
117         p.x - half >= bounds.x &&
118         p.y - half >= bounds.y &&
119         p.x + width + half <= bounds.x + bounds.width &&
120         p.y + height + half <= bounds.y + bounds.height,
121     ) ?? candidates[0]
122   );
123 }
124 

Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.