packages/core/src/features/viz/lib/nearest.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
1 import type { ResolvedContinuousScale } from "./types.js";
2 import type { PointAccessor } from "./point-types.js";
3 import { finiteCoordinate, pointAccessor, pointData } from "./point-data.js";
4 /**
5 * Coordinate accessors, resolved scales, and candidate filtering for a nearest-point search.
6 *
7 * @see {@link PointAccessor}
8 * @see {@link ResolvedContinuousScale}
9 * @see {@link nearestPoint}
10 */
11 export interface NearestPointOptions<D> {
12 /** Property key or callback extracting the horizontal domain coordinate. See {@link PointAccessor}. */
13 readonly x: PointAccessor<D>;
14 /** Property key or callback extracting the vertical domain coordinate. See {@link PointAccessor}. */
15 readonly y: PointAccessor<D>;
16 /** The scale used to map horizontal data coordinates. See {@link ResolvedContinuousScale}. */
17 readonly xScale: ResolvedContinuousScale;
18 /** The scale used to map vertical data coordinates. See {@link ResolvedContinuousScale}. */
19 readonly yScale: ResolvedContinuousScale;
20 /**
21 * Returns whether a datum contributes drawable or queryable coordinates. See
22 * {@link NearestPointOptions}.
23 * @param datum - Datum to test.
24 * @param index - Zero-based index in the source data.
25 * @param data - Complete source data array.
26 * @returns Whether this datum should participate in the series or query.
27 */
28 readonly defined?: (datum: D, index: number, data: readonly D[]) => boolean;
29 /** Maximum distance accepted by the query. See {@link NearestPointOptions}. */
30 readonly maxDistance?: number;
31 }
32 /**
33 * The selected datum, source index, mapped coordinates, and distance from the query point.
34 *
35 * @see {@link nearestPoint}
36 */
37 export interface NearestPoint<D> {
38 /** The original data item selected by this result. See {@link NearestPoint}. */
39 readonly datum: D;
40 /** Index of the selected datum in its source data. See {@link NearestPoint}. */
41 readonly index: number;
42 /**
43 * Horizontal coordinate or displacement in the containing coordinate system. See
44 * {@link NearestPoint}.
45 */
46 readonly x: number;
47 /**
48 * Vertical coordinate or displacement in the containing coordinate system. See
49 * {@link NearestPoint}.
50 */
51 readonly y: number;
52 /** Distance measured in this query or guide's coordinate system. See {@link NearestPoint}. */
53 readonly distance: number;
54 }
55 function finite(value: unknown, name: string): asserts value is number {
56 if (!finiteCoordinate(value)) throw new RangeError(`${name} must be finite.`);
57 }
58 /**
59 * Finds the domain value nearest to a logical range coordinate; returns undefined for no
60 * candidate.
61 *
62 * @param values - Candidate domain values.
63 * @param rangeValue - Pointer coordinate in the scale's range.
64 * @param scale - Scale mapping candidates into range coordinates. See {@link ResolvedContinuousScale}.
65 * @returns The nearest domain value, or undefined when no candidate can be selected.
66 *
67 * @see {@link ResolvedContinuousScale}
68 */
69 export function nearestDomainValue(
70 values: readonly number[],
71 rangeValue: number,
72 scale: ResolvedContinuousScale,
73 ): number | undefined {
74 pointData(values, "nearestDomainValue");
75 finite(rangeValue, "nearestDomainValue rangeValue");
76 let best: number | undefined,
77 distance = Infinity;
78 for (const value of values) {
79 if (!finiteCoordinate(value)) continue;
80 const next = Math.abs(scale.map(value) - rangeValue);
81 finite(next, "nearestDomainValue distance");
82 if (next < distance) {
83 best = value;
84 distance = next;
85 }
86 }
87 return best;
88 }
89 /**
90 * Finds the nearest defined datum in scaled coordinates, subject to an optional maximum distance.
91 *
92 * @param data - Candidate data records.
93 * @param point - Query point in plotted range coordinates.
94 * @param options - Accessors, scales, eligibility, and distance settings. See
95 * {@link NearestPointOptions} .
96 * @returns The nearest matching point, or undefined when no candidate qualifies. See
97 * {@link NearestPoint} .
98 *
99 * @see {@link NearestPointOptions}
100 * @see {@link NearestPoint}
101 */
102 export function nearestPoint<D>(
103 data: readonly D[],
104 point: Readonly<{ x: number; y: number }>,
105 options: NearestPointOptions<D>,
106 ): NearestPoint<D> | undefined {
107 pointData(data, "nearestPoint");
108 finite(point.x, "nearestPoint x");
109 finite(point.y, "nearestPoint y");
110 const max = options.maxDistance;
111 if (max !== undefined) {
112 finite(max, "nearestPoint maxDistance");
113 if (max < 0)
114 throw new RangeError("nearestPoint maxDistance must be nonnegative.");
115 }
116 const x = pointAccessor(options.x),
117 y = pointAccessor(options.y);
118 let best: NearestPoint<D> | undefined,
119 distance = Infinity;
120 for (let i = 0; i < data.length; i++) {
121 const datum = data[i];
122 if (options.defined && !options.defined(datum, i, data)) continue;
123 const xv = x(datum, i, data),
124 yv = y(datum, i, data);
125 if (!finiteCoordinate(xv) || !finiteCoordinate(yv)) continue;
126 const px = options.xScale.map(xv),
127 py = options.yScale.map(yv);
128 const next = Math.hypot(px - point.x, py - point.y);
129 finite(next, "nearestPoint distance");
130 if (next < distance && (max === undefined || next <= max)) {
131 distance = next;
132 best = { datum, index: i, x: px, y: py, distance: next };
133 }
134 }
135 return best;
136 }
137
Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.