packages/core/src/features/viz/inspection/volume.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
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 version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision dd39ce2. ALPHA — NOT FOR PRODUCTION USE.