packages/core/src/features/viz/lib/hover-placement.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
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 version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.