Skip to content

packages/core/src/features/viz/inspection/volume.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 {
2   invertNumericPosition,
3   type NumericPositionScale,
4 } from "../lib/series-position.js";
5 import type { LayoutBox } from "@pibbl/core";
6 import type { VolumeSeriesProps } from "../lib/volume-series.js";
7 import type { ResolvedContinuousScale } from "../lib/types.js";
8 import type {
9   RenderedSeriesPoint,
10   NumericRenderedSeries,
11 } from "./query-types.js";
12 import { useRenderedRef } from "./owned-ref.js";
13 import { nearestRecordedX } from "./queries.js";
14 
15 /** One original volume record and the values/geometry evaluated during paint.
16  * @see {@link useVolumeInspection}
17  */
18 export interface VolumeObservation<D> {
19   /** Caller-owned original record. */ readonly datum: D;
20   /** Original index including skipped rows. */ readonly sourceIndex: number;
21   /** Evaluated UTC milliseconds. */ readonly time: number;
22   /** Evaluated nonnegative volume. */ readonly volume: number;
23   /** Zero-volume baseline. */ readonly baseline: 0;
24   /** Volume endpoint in series-local logical coordinates. */ readonly anchorLocal: Readonly<RenderedSeriesPoint>;
25   /** Volume endpoint in root-logical coordinates. */ readonly anchorRoot: Readonly<RenderedSeriesPoint>;
26   /** Column bounds in series-local logical coordinates. */ readonly boundsLocal: Readonly<LayoutBox>;
27 }
28 /** A nearest-time candidate with an explicit optional clip predicate.
29  * @see {@link useVolumeInspection}
30  */
31 export interface VolumeInspectionCandidate<D> extends VolumeObservation<D> {
32   /** @returns Whether the endpoint lies inside every committed clip. */ anchorInsideClips(): boolean;
33 }
34 /** A recorded volume selected by timestamp proximity.
35  * @see {@link useVolumeInspection}
36  */
37 export interface NearestVolumeHit<D> extends VolumeObservation<D> {
38   /** Absolute UTC-millisecond distance from the requested time. */ readonly timeDelta: number;
39 }
40 /** A recorded volume whose painted column covers the query point.
41  * @see {@link useVolumeInspection}
42  */
43 export interface PaintedVolumeHit<D> extends VolumeObservation<D> {}
44 /** @internal Immutable local renderer capture, without queries or transformed allocation.
45  * @see {@link useVolumeInspection}
46  */
47 export interface CapturedVolumeMark<D> extends Omit<
48   VolumeObservation<D>,
49   "anchorRoot"
50 > {
51   /** Whether the rectangle paints its interior. */ readonly fill: boolean;
52   /** Outline width when painted, zero otherwise. */ readonly strokeWidthLocal: number;
53 }
54 /** Borrowed committed volume values; expires after a new root generation or disposal.
55  * @see {@link useVolumeInspection}
56  */
57 export interface VolumeRenderedSeries<D> {
58   /** Original observations and evaluated values in source order. */ readonly observations: readonly VolumeObservation<D>[];
59   /** Throws when borrowed values are stale or disposed. */ assertLive(): void;
60   /** Inverts a root-logical point to UTC milliseconds.
61    * @param pointRoot - Root-logical Canvas position.
62    * @returns Timestamp or undefined for unavailable inversion.
63    */ timeAtRoot(pointRoot: Readonly<RenderedSeriesPoint>): number | undefined;
64   /** Tests a local point against the committed clip stack.
65    * @param pointLocal - Series-local position.
66    * @returns Whether every clip includes it.
67    */ anchorInsideClips(pointLocal: Readonly<RenderedSeriesPoint>): boolean;
68   /** @internal Committed paint flags. */ readonly marks: readonly CapturedVolumeMark<D>[];
69   /** @internal Shared numeric query view. */ readonly numeric: NumericRenderedSeries<D>;
70   /** @internal Converts root coordinates to local coordinates.
71    * @param point - Root-logical position.
72    * @returns Local position or undefined for a singular transform.
73    */ localAtRoot(
74     point: Readonly<RenderedSeriesPoint>,
75   ): Readonly<RenderedSeriesPoint> | undefined;
76 }
77 /** Mount-owned optional volume capture.
78  * @see {@link useVolumeInspection}
79  */
80 export interface VolumeInspectionRef<D> {
81   /** @returns Borrowed committed values, or undefined before paint/after disposal. */ rendered():
82     VolumeRenderedSeries<D> | undefined;
83   /** @internal Records local values without executing a query.
84    * @param input - Immutable local marks and committed time mapping.
85    */ capture(input: {
86     /** Captured local paint marks. */ marks: readonly CapturedVolumeMark<D>[];
87     /** Committed horizontal mapping. */ xScale: NumericPositionScale;
88     /** Committed volume mapping. */ yScale: ResolvedContinuousScale;
89   }): void;
90 }
91 /**
92  * Creates a stable optional volume receiver without reading data or subscribing.
93  * @param _inputs - Data/accessors used only for original datum inference.
94  * @returns Ref to pass to exactly one VolumeSeries inspection prop.
95  * @see {@link VolumeInspectionRef}
96  */
97 export function useVolumeInspection<D>(
98   _inputs: Pick<VolumeSeriesProps<D>, "data" | "time" | "volume">,
99 ): VolumeInspectionRef<D> {
100   return useRenderedRef<
101     {
102       marks: readonly CapturedVolumeMark<D>[];
103       xScale: NumericPositionScale;
104       yScale: ResolvedContinuousScale;
105     },
106     VolumeRenderedSeries<D>
107   >("volumeInspection", (candidate, geometry) => {
108     const observations = Object.freeze(
109       candidate.marks.map((mark) =>
110         Object.freeze({
111           datum: mark.datum,
112           sourceIndex: mark.sourceIndex,
113           time: mark.time,
114           volume: mark.volume,
115           baseline: mark.baseline,
116           anchorLocal: mark.anchorLocal,
117           anchorRoot: geometry.rootAtLocal(mark.anchorLocal),
118           boundsLocal: mark.boundsLocal,
119         }),
120       ),
121     );
122     const numeric: NumericRenderedSeries<D> = Object.freeze({
123       observations: Object.freeze(
124         observations.map((row) =>
125           Object.freeze({
126             ...row,
127             domainX: row.time,
128             domainY: row.volume,
129             run: 0,
130           }),
131         ),
132       ),
133       assertLive: geometry.assertLive,
134       domainXAtRoot(point: Readonly<RenderedSeriesPoint>) {
135         const local = geometry.localAtRoot(point);
136         return local
137           ? invertNumericPosition(candidate.xScale, local.x)
138           : undefined;
139       },
140       anchorInsideClips: geometry.anchorInsideClips,
141       anchorAtDomain(point: Readonly<{ domainX: number; domainY: number }>) {
142         geometry.assertLive();
143         const anchorLocal = Object.freeze({
144           x: candidate.xScale.map(point.domainX),
145           y: candidate.yScale.map(point.domainY),
146         });
147         return Object.freeze({
148           anchorLocal,
149           anchorRoot: geometry.rootAtLocal(anchorLocal),
150         });
151       },
152     });
153     return Object.freeze({
154       observations,
155       marks: Object.freeze(candidate.marks.slice()),
156       numeric,
157       assertLive: geometry.assertLive,
158       localAtRoot: geometry.localAtRoot,
159       anchorInsideClips: geometry.anchorInsideClips,
160       timeAtRoot: numeric.domainXAtRoot,
161     });
162   });
163 }
164 /**
165  * Selects a recorded volume by timestamp; equal distances choose first source index.
166  * @param rendered - Borrowed committed volume values.
167  * @param options - UTC-millisecond request and optional pure candidate filter.
168  * @returns Captured original volume and time delta, or undefined when no candidate survives.
169  * @see {@link NearestVolumeHit}
170  */
171 export function nearestRecordedVolumeX<D>(
172   rendered: VolumeRenderedSeries<D>,
173   options: {
174     time: number;
175     filter?: (observation: VolumeInspectionCandidate<D>) => boolean;
176   },
177 ): NearestVolumeHit<D> | undefined {
178   const hit = nearestRecordedX(rendered.numeric, {
179     domainX: options.time,
180     filter: options.filter
181       ? (candidate) =>
182           options.filter!(
183             candidate as typeof candidate & VolumeInspectionCandidate<D>,
184           )
185       : undefined,
186   });
187   if (!hit) return undefined;
188   const row = rendered.observations.find(
189     (row) => row.sourceIndex === hit.sourceIndex,
190   )!;
191   return Object.freeze({ ...row, timeDelta: hit.domainDelta });
192 }
193 /**
194  * Finds the last-painted volume column covering a root-logical point.
195  * @param rendered - Borrowed committed volume values and paint geometry.
196  * @param options - Root-logical point; respects clips, zero height and source paint order.
197  * @returns Captured original volume, or undefined for gaps/singular transforms.
198  * @see {@link PaintedVolumeHit}
199  */
200 export function paintedVolumeAt<D>(
201   rendered: VolumeRenderedSeries<D>,
202   options: { rootPoint: Readonly<RenderedSeriesPoint> },
203 ): PaintedVolumeHit<D> | undefined {
204   rendered.assertLive();
205   if (
206     !Number.isFinite(options.rootPoint.x) ||
207     !Number.isFinite(options.rootPoint.y)
208   )
209     throw new RangeError("paintedVolumeAt rootPoint must be finite.");
210   const p = rendered.localAtRoot(options.rootPoint);
211   if (!p || !rendered.anchorInsideClips(p)) return undefined;
212   for (let i = rendered.marks.length - 1; i >= 0; i--) {
213     const mark = rendered.marks[i],
214       b = mark.boundsLocal,
215       h = mark.strokeWidthLocal / 2;
216     if (b.height === 0) continue;
217     const inside =
218       p.x >= b.x && p.x <= b.x + b.width && p.y >= b.y && p.y <= b.y + b.height;
219     const outline =
220       h > 0 &&
221       p.x >= b.x - h &&
222       p.x <= b.x + b.width + h &&
223       p.y >= b.y - h &&
224       p.y <= b.y + b.height + h &&
225       !(
226         p.x > b.x + h &&
227         p.x < b.x + b.width - h &&
228         p.y > b.y + h &&
229         p.y < b.y + b.height - h
230       );
231     if ((mark.fill && inside) || outline) return rendered.observations[i];
232   }
233   return undefined;
234 }
235 

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