Skip to content

packages/core/src/features/viz/lib/domain-navigation.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 { LinearDomain, ResolvedContinuousScale } from "./types.js";
2 /** Explicit hard bounds and minimum mapped extent for continuous-domain navigation. @see {@link panDomain} @see {@link zoomDomain} */
3 export interface DomainNavigationConstraints {
4   /** Finite hard domain endpoints, in either order, valid for the supplied scale. See {@link LinearDomain}. */
5   readonly bounds: LinearDomain;
6   /** Minimum span as a fraction of the mapped hard bounds, greater than zero through one. Defaults to 0.01. */
7   readonly minimumSpanRatio?: number;
8 }
9 /** Named pan settings with explicit navigation limits. @see {@link panDomain} */
10 export interface PanDomainOptions extends DomainNavigationConstraints {
11   /** Finite displacement in range units; positive shifts mapped values toward increasing range coordinates. */
12   readonly rangeDelta: number;
13 }
14 /** Named zoom settings with explicit navigation limits. @see {@link zoomDomain} */
15 export interface ZoomDomainOptions extends DomainNavigationConstraints {
16   /** Finite anchor in the scale's range coordinates, using the same units as its range. */
17   readonly rangeAnchor: number;
18   /** Finite positive magnification; greater than one zooms in. */
19   readonly factor: number;
20 }
21 function finite(value: number, name: string): number {
22   if (typeof value !== "number" || !Number.isFinite(value))
23     throw new RangeError(`${name} must be finite.`);
24   return value;
25 }
26 function navigate(
27   scale: ResolvedContinuousScale,
28   start: number,
29   end: number,
30   constraints: DomainNavigationConstraints,
31   requestedSpan?: number,
32 ): LinearDomain {
33   const [r0, r1] = scale.range;
34   if (r0 === r1 || scale.domain[0] === scale.domain[1])
35     throw new RangeError(
36       "Domain navigation requires noncollapsed domain and range.",
37     );
38   const { bounds, minimumSpanRatio = 0.01 } = constraints;
39   if (!Array.isArray(bounds) || bounds.length !== 2)
40     throw new RangeError("Navigation bounds require two endpoints.");
41   if (scale.type === "utc" && !bounds.every(Number.isSafeInteger))
42     throw new RangeError(
43       "UTC navigation bounds must be safe integer milliseconds.",
44     );
45   const b0 = scale.map(finite(bounds[0], "Lower bound")),
46     b1 = scale.map(finite(bounds[1], "Upper bound"));
47   const lo = Math.min(b0, b1),
48     hi = Math.max(b0, b1),
49     extent = hi - lo;
50   if (!Number.isFinite(extent) || extent <= 0)
51     throw new RangeError(
52       "Navigation bounds must have a finite nonzero mapped extent.",
53     );
54   if (
55     !Number.isFinite(minimumSpanRatio) ||
56     minimumSpanRatio <= 0 ||
57     minimumSpanRatio > 1
58   )
59     throw new RangeError(
60       "minimumSpanRatio must be greater than zero and at most one.",
61     );
62   finite(start, "Mapped start");
63   finite(end, "Mapped end");
64   const requested = requestedSpan ?? Math.abs(end - start);
65   if (!Number.isFinite(requested))
66     throw new RangeError("Navigation span must be finite.");
67   const span = Math.min(extent, Math.max(extent * minimumSpanRatio, requested));
68   const center = start / 2 + end / 2;
69   const low = Math.max(lo, Math.min(hi - span, center - span / 2)),
70     high = low + span;
71   let a = scale.invert(r0 < r1 ? low : high),
72     b = scale.invert(r0 < r1 ? high : low);
73   if (scale.type === "utc") {
74     a = Math.round(a);
75     b = Math.round(b);
76   }
77   finite(a, "Domain start");
78   finite(b, "Domain end");
79   if (a === b)
80     throw new RangeError(
81       "Navigation result collapses at the scale's numeric precision.",
82     );
83   return Object.freeze([a, b] as const);
84 }
85 /**
86  * Moves the visible continuous domain by a displacement expressed in range units.
87  * Positive displacement shifts mapped values toward increasing range coordinates; the returned domain moves oppositely.
88  * @param scale - Current resolved continuous mapping. See {@link ResolvedContinuousScale}.
89  * @param options - Displacement, bounds, and minimum span. See {@link PanDomainOptions}.
90  * @returns A new immutable domain pair. See {@link LinearDomain}.
91  * @throws When inputs, bounds, or the resulting extent are invalid or collapsed.
92  * @see {@link zoomDomain}
93  */
94 export function panDomain(
95   scale: ResolvedContinuousScale,
96   options: PanDomainOptions,
97 ): LinearDomain {
98   const { rangeDelta } = options;
99   finite(rangeDelta, "Pan displacement");
100   return navigate(
101     scale,
102     scale.range[0] - rangeDelta,
103     scale.range[1] - rangeDelta,
104     options,
105     Math.abs(scale.range[1] - scale.range[0]),
106   );
107 }
108 /**
109  * Magnifies a continuous domain around an anchor in range coordinates.
110  * Factor greater than one zooms in; hard bounds and minimum span may limit anchor preservation.
111  * @param scale - Current resolved continuous mapping. See {@link ResolvedContinuousScale}.
112  * @param options - Anchor, magnification, bounds, and minimum span. See {@link ZoomDomainOptions}.
113  * @returns A new immutable domain pair. See {@link LinearDomain}.
114  * @throws When factor, mapping, bounds, or the resulting extent are invalid or collapsed.
115  * @see {@link panDomain}
116  */
117 export function zoomDomain(
118   scale: ResolvedContinuousScale,
119   options: ZoomDomainOptions,
120 ): LinearDomain {
121   const { rangeAnchor, factor } = options;
122   finite(rangeAnchor, "Zoom anchor");
123   finite(factor, "Zoom factor");
124   if (factor <= 0) throw new RangeError("Zoom factor must be positive.");
125   return navigate(
126     scale,
127     rangeAnchor + (scale.range[0] - rangeAnchor) / factor,
128     rangeAnchor + (scale.range[1] - rangeAnchor) / factor,
129     options,
130   );
131 }
132 

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