Skip to content

packages/core/src/features/viz/inspection/queries.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 {
2   NearestRecordedPointHit,
3   NearestRecordedXHit,
4   NumericDomainInterpolation,
5   NumericDuplicateResolver,
6   InspectionCandidate,
7   NumericRenderedFilter,
8   NumericRenderedObservation,
9   NumericRenderedSeries,
10   RecordedXSample,
11   RenderedSeriesPoint,
12 } from "./query-types.js";
13 
14 function finite(value: unknown, name: string): asserts value is number {
15   if (typeof value !== "number" || !Number.isFinite(value))
16     throw new RangeError(`${name} must be finite.`);
17 }
18 
19 function limit(value: number | undefined, name: string): void {
20   if (value === undefined) return;
21   finite(value, name);
22   if (value < 0) throw new RangeError(`${name} must be nonnegative.`);
23 }
24 
25 function point(value: Readonly<RenderedSeriesPoint>, name: string): void {
26   finite(value.x, `${name}.x`);
27   finite(value.y, `${name}.y`);
28 }
29 
30 function candidate<D>(
31   rendered: NumericRenderedSeries<D>,
32   observation: NumericRenderedObservation<D>,
33 ): InspectionCandidate<D> {
34   return Object.freeze({
35     ...observation,
36     anchorInsideClips: () =>
37       rendered.anchorInsideClips(observation.anchorLocal),
38   });
39 }
40 
41 function accepts<D>(
42   rendered: NumericRenderedSeries<D>,
43   observation: NumericRenderedObservation<D>,
44   filter: NumericRenderedFilter<D> | undefined,
45 ): boolean {
46   return filter === undefined || filter(candidate(rendered, observation));
47 }
48 
49 function anchored<D>(
50   rendered: NumericRenderedSeries<D>,
51   observation: NumericRenderedObservation<D>,
52 ): NumericRenderedObservation<D> {
53   return { ...observation, ...rendered.anchorAtDomain(observation) };
54 }
55 
56 /**
57  * Retains the first source observation at a duplicate domain coordinate.
58  *
59  * @param first - First selected observation at this domain coordinate.
60  * @param _next - Later observation at the same domain coordinate.
61  * @returns The earliest source observation.
62  * @see {@link NumericDuplicateResolver}
63  */
64 export function firstSourceObservation<D>(
65   first: NumericRenderedObservation<D>,
66   _next: NumericRenderedObservation<D>,
67 ): NumericRenderedObservation<D> {
68   return first;
69 }
70 
71 /**
72  * Replaces a source observation with the next one at a duplicate domain coordinate.
73  *
74  * @param _first - Earlier selected observation at this domain coordinate.
75  * @param next - Later observation at the same domain coordinate.
76  * @returns The latest source observation.
77  * @see {@link NumericDuplicateResolver}
78  */
79 export function lastSourceObservation<D>(
80   _first: NumericRenderedObservation<D>,
81   next: NumericRenderedObservation<D>,
82 ): NumericRenderedObservation<D> {
83   return next;
84 }
85 
86 /**
87  * Returns whether a filter candidate's anchor lies inside the committed clip stack.
88  *
89  * Pass this function as a query filter when visibility at the anchor is required.
90  * Queries otherwise examine all eligible rendered observations.
91  *
92  * @param candidate - Candidate whose committed local anchor should be clip-tested.
93  * @returns Whether the candidate anchor lies within every committed clip.
94  * @see {@link NumericRenderedFilter}
95  */
96 export function anchorInsideClips<D>(
97   candidate: InspectionCandidate<D>,
98 ): boolean {
99   return candidate.anchorInsideClips();
100 }
101 
102 /** Options for {@link nearestRecordedX}. */
103 export interface NearestRecordedXOptions<D> {
104   /** Numeric horizontal domain value to inspect, or undefined for no request. */
105   readonly domainX: number | undefined;
106   /** Maximum accepted absolute horizontal-domain delta. */
107   readonly maxDomainDelta?: number;
108   /** Further restricts committed observations for this request. */
109   readonly filter?: NumericRenderedFilter<D>;
110 }
111 
112 /**
113  * Finds the recorded observation nearest to a numeric horizontal domain value in O(n) time.
114  *
115  * Ties retain source order. Distance is measured in domain units, while both
116  * returned anchors retain their explicitly named local and root-logical units.
117  *
118  * @param rendered - Committed rendered-series generation to inspect synchronously.
119  * @param options - Domain position, optional limit, and observation filter.
120  * @returns The selected recorded observation, or undefined when none qualifies.
121  * @see {@link NearestRecordedXOptions}
122  */
123 export function nearestRecordedX<D>(
124   rendered: NumericRenderedSeries<D>,
125   options: NearestRecordedXOptions<D>,
126 ): NearestRecordedXHit<D> | undefined {
127   rendered.assertLive();
128   const query = options.domainX;
129   if (query === undefined) return undefined;
130   finite(query, "nearestRecordedX domainX");
131   limit(options.maxDomainDelta, "nearestRecordedX maxDomainDelta");
132   let best: NearestRecordedXHit<D> | undefined;
133   let delta = Infinity;
134   for (const observation of rendered.observations) {
135     if (!accepts(rendered, observation, options.filter)) continue;
136     const next = Math.abs(observation.domainX - query);
137     finite(next, "nearestRecordedX domain delta");
138     if (
139       next < delta &&
140       (options.maxDomainDelta === undefined || next <= options.maxDomainDelta)
141     ) {
142       delta = next;
143       best = Object.freeze({
144         ...anchored(rendered, observation),
145         domainDelta: next,
146       });
147     }
148   }
149   return best;
150 }
151 
152 /** Options for {@link nearestRecordedPoint}. */
153 export interface NearestRecordedPointOptions<D> {
154   /** Root-logical point to inspect, or undefined for no request. */
155   readonly rootPoint: Readonly<RenderedSeriesPoint> | undefined;
156   /** Maximum accepted Euclidean root-logical distance. */
157   readonly maxRootDistance?: number;
158   /** Further restricts committed observations for this request. */
159   readonly filter?: NumericRenderedFilter<D>;
160 }
161 
162 /**
163  * Finds the recorded observation nearest to a root-logical point in O(n) time.
164  *
165  * Ties retain source order. This query measures distance in root-logical units
166  * so it remains meaningful under nonuniform series transforms.
167  *
168  * @param rendered - Committed rendered-series generation to inspect synchronously.
169  * @param options - Root position, optional limit, and observation filter.
170  * @returns The selected recorded observation, or undefined when none qualifies.
171  * @see {@link NearestRecordedPointOptions}
172  */
173 export function nearestRecordedPoint<D>(
174   rendered: NumericRenderedSeries<D>,
175   options: NearestRecordedPointOptions<D>,
176 ): NearestRecordedPointHit<D> | undefined {
177   rendered.assertLive();
178   const query = options.rootPoint;
179   if (query === undefined) return undefined;
180   point(query, "nearestRecordedPoint rootPoint");
181   limit(options.maxRootDistance, "nearestRecordedPoint maxRootDistance");
182   let best: NearestRecordedPointHit<D> | undefined;
183   let distance = Infinity;
184   for (const observation of rendered.observations) {
185     if (!accepts(rendered, observation, options.filter)) continue;
186     const next = Math.hypot(
187       observation.anchorRoot.x - query.x,
188       observation.anchorRoot.y - query.y,
189     );
190     finite(next, "nearestRecordedPoint root distance");
191     if (
192       next < distance &&
193       (options.maxRootDistance === undefined || next <= options.maxRootDistance)
194     ) {
195       distance = next;
196       best = Object.freeze({
197         ...anchored(rendered, observation),
198         rootDistance: next,
199       });
200     }
201   }
202   return best;
203 }
204 
205 /** Options for {@link sampleRecordedX}. */
206 export interface SampleRecordedXOptions<D> {
207   /** Numeric horizontal domain value to sample, or undefined for no request. */
208   readonly domainX: number | undefined;
209   /** Computes the estimated numeric domain value between two recorded values. */
210   readonly interpolation: NumericDomainInterpolation;
211   /** Explicitly chooses one observation from each duplicate-domain group. */
212   readonly resolveDuplicate?: NumericDuplicateResolver<D>;
213   /** Maximum domain interval that may be interpolated across. */
214   readonly maxDomainGap?: number;
215   /** Further restricts committed observations for this request. */
216   readonly filter?: NumericRenderedFilter<D>;
217 }
218 
219 function resolvedRows<D>(
220   rendered: NumericRenderedSeries<D>,
221   filter: NumericRenderedFilter<D> | undefined,
222   resolver: NumericDuplicateResolver<D> | undefined,
223 ): readonly (readonly NumericRenderedObservation<D>[])[] {
224   let validatedRun: number | undefined;
225   let validatedX = -Infinity;
226   for (const observation of rendered.observations) {
227     finite(observation.domainX, "sampleRecordedX observation.domainX");
228     finite(observation.domainY, "sampleRecordedX observation.domainY");
229     if (validatedRun === observation.run && observation.domainX < validatedX)
230       throw new RangeError(
231         "sampleRecordedX requires nondecreasing domainX within each rendered run.",
232       );
233     validatedRun = observation.run;
234     validatedX = observation.domainX;
235   }
236   const runs: NumericRenderedObservation<D>[][] = [];
237   let current: NumericRenderedObservation<D>[] = [];
238   let previousRun: number | undefined;
239   const finish = () => {
240     if (current.length) runs.push(current);
241     current = [];
242     previousRun = undefined;
243   };
244   for (const observation of rendered.observations) {
245     if (previousRun !== undefined && observation.run !== previousRun) finish();
246     previousRun = observation.run;
247     if (!accepts(rendered, observation, filter)) {
248       finish();
249       continue;
250     }
251     current.push(observation);
252   }
253   finish();
254   return runs.map((run) => {
255     const resolved: NumericRenderedObservation<D>[] = [];
256     for (let start = 0; start < run.length;) {
257       let end = start + 1;
258       while (end < run.length && run[end]!.domainX === run[start]!.domainX)
259         end++;
260       let selected = run[start]!;
261       for (let duplicate = start + 1; duplicate < end; duplicate++) {
262         const next = run[duplicate]!;
263         if (resolver === undefined)
264           throw new RangeError(
265             "sampleRecordedX duplicate domainX requires resolveDuplicate.",
266           );
267         const resolved = resolver(selected, next);
268         if (resolved !== selected && resolved !== next)
269           throw new TypeError(
270             "sampleRecordedX resolveDuplicate must return its first or next observation.",
271           );
272         selected = resolved;
273       }
274       resolved.push(selected);
275       start = end;
276     }
277     return resolved;
278   });
279 }
280 
281 /**
282  * Returns an exact recorded observation or a caller-defined interpolation between records.
283  *
284  * It never bridges separate rendered runs, filtered observations, or an interval larger
285  * than `maxDomainGap`. Duplicate x values reject by default and require an explicit
286  * `resolveDuplicate` function. Estimated results retain their two source observations and
287  * map explicit local/root anchors through the committed generation. This is O(n) in captured
288  * observations and the result is borrowed until the rendered generation is replaced or disposed.
289  *
290  * @param rendered - Committed rendered-series generation to inspect synchronously.
291  * @param options - Domain position, interpolation, duplicate policy, and filter.
292  * @returns An exact record, an explicitly interpolated estimate, or undefined.
293  * @see {@link SampleRecordedXOptions}
294  */
295 export function sampleRecordedX<D>(
296   rendered: NumericRenderedSeries<D>,
297   options: SampleRecordedXOptions<D>,
298 ): RecordedXSample<D> | undefined {
299   rendered.assertLive();
300   const query = options.domainX;
301   if (query === undefined) return undefined;
302   finite(query, "sampleRecordedX domainX");
303   limit(options.maxDomainGap, "sampleRecordedX maxDomainGap");
304   if (typeof options.interpolation !== "function")
305     throw new TypeError("sampleRecordedX interpolation must be a function.");
306   if (
307     options.resolveDuplicate !== undefined &&
308     typeof options.resolveDuplicate !== "function"
309   )
310     throw new TypeError("sampleRecordedX resolveDuplicate must be a function.");
311   for (const run of resolvedRows(
312     rendered,
313     options.filter,
314     options.resolveDuplicate,
315   )) {
316     for (let index = 0; index < run.length; index++) {
317       const before = run[index]!;
318       if (before.domainX === query)
319         return Object.freeze({ ...anchored(rendered, before), kind: "exact" });
320       const after = run[index + 1];
321       if (!after || query <= before.domainX || query >= after.domainX) continue;
322       const gap = after.domainX - before.domainX;
323       finite(gap, "sampleRecordedX domain gap");
324       if (options.maxDomainGap !== undefined && gap > options.maxDomainGap)
325         return undefined;
326       const fraction = (query - before.domainX) / gap;
327       const domainY = options.interpolation(
328         before.domainY,
329         after.domainY,
330         fraction,
331       );
332       finite(domainY, "sampleRecordedX interpolation result");
333       return Object.freeze({
334         kind: "interpolated",
335         domainX: query,
336         domainY,
337         ...rendered.anchorAtDomain({ domainX: query, domainY }),
338         before,
339         after,
340         fraction,
341       });
342     }
343   }
344   return undefined;
345 }
346 

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