Skip to content

packages/core/src/features/viz/lib/nearest.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 { 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 built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.