Skip to content

packages/core/src/features/viz/inspection/ohlc-bar.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 { OhlcBarSeriesProps } from "../lib/ohlc-bar-series.js";
2 import {
3   invertNumericPosition,
4   type NumericPositionScale,
5 } from "../lib/series-position.js";
6 import type { ResolvedContinuousScale } from "../lib/types.js";
7 import { useRenderedRef } from "./owned-ref.js";
8 import { nearestRecordedX } from "./queries.js";
9 import type {
10   NumericRenderedSeries,
11   RenderedSeriesPoint,
12 } from "./query-types.js";
13 
14 /** One original OHLC record and its values and geometry as evaluated during paint.
15  * @see {@link OhlcBarRenderedSeries}
16  */
17 export interface OhlcBarObservation<D> {
18   /** Caller-owned original record. */
19   readonly datum: D;
20   /** Original source index, including skipped rows. */
21   readonly sourceIndex: number;
22   /** Evaluated UTC-millisecond timestamp. */
23   readonly time: number;
24   /** Evaluated opening value. */
25   readonly open: number;
26   /** Evaluated high value. */
27   readonly high: number;
28   /** Evaluated low value. */
29   readonly low: number;
30   /** Evaluated closing value. */
31   readonly close: number;
32   /** Closing endpoint in series-local coordinates. */ readonly anchorLocal: Readonly<RenderedSeriesPoint>;
33   /** Closing endpoint in root-logical coordinates. */ readonly anchorRoot: Readonly<RenderedSeriesPoint>;
34   /** High/low segment before world transforms. */ readonly stemLocal: Readonly<{
35     /** Horizontal center. */ x: number;
36     /** Mapped high endpoint. */ highY: number;
37     /** Mapped low endpoint. */ lowY: number;
38   }>;
39   /** Left-facing opening segment before world transforms. */ readonly openTickLocal: Readonly<{
40     /** Left endpoint. */ x1: number;
41     /** Center endpoint. */ x2: number;
42     /** Mapped opening ordinate. */ y: number;
43   }>;
44   /** Right-facing closing segment before world transforms. */ readonly closeTickLocal: Readonly<{
45     /** Center endpoint. */ x1: number;
46     /** Right endpoint. */ x2: number;
47     /** Mapped closing ordinate. */ y: number;
48   }>;
49 }
50 /** A nearest-time OHLC candidate with committed clip eligibility.
51  * @see {@link nearestRecordedOhlcBarX}
52  */
53 export interface OhlcBarInspectionCandidate<D> extends OhlcBarObservation<D> {
54   /** @returns Whether the close anchor lies inside every committed clip. */
55   anchorInsideClips(): boolean;
56 }
57 /** Original OHLC selected by nearest timestamp.
58  * @see {@link nearestRecordedOhlcBarX}
59  */
60 export interface NearestOhlcBarHit<D> extends OhlcBarObservation<D> {
61   /** Absolute UTC-millisecond distance from the requested timestamp. */
62   readonly timeDelta: number;
63 }
64 /** An actual painted OHLC stroke segment covering a root-logical point.
65  * @see {@link paintedOhlcBarAt}
66  */
67 export interface PaintedOhlcBarHit<D> extends OhlcBarObservation<D> {
68   /** Last painted matching segment. */
69   readonly part: "stem" | "openTick" | "closeTick";
70 }
71 /** @internal Local OHLC paint capture. */
72 export interface CapturedOhlcBarMark<D> extends Omit<
73   OhlcBarObservation<D>,
74   "anchorRoot"
75 > {
76   readonly strokeWidthLocal: number;
77 }
78 /** Borrowed committed OHLC generation.
79  * @see {@link OhlcBarInspectionRef}
80  */
81 export interface OhlcBarRenderedSeries<D> {
82   /** Captured original observations in source order. */
83   readonly observations: readonly OhlcBarObservation<D>[];
84   /** Throws after replacement or disposal. */
85   assertLive(): void;
86   /** Inverts a root-logical point to a timestamp.
87    * @param pointRoot - Root-logical point.
88    * @returns Timestamp, or undefined for a singular transform.
89    */
90   timeAtRoot(pointRoot: Readonly<RenderedSeriesPoint>): number | undefined;
91   /** Tests a local point against every committed clip.
92    * @param pointLocal - Series-local point.
93    * @returns Whether every committed clip contains it.
94    */
95   anchorInsideClips(pointLocal: Readonly<RenderedSeriesPoint>): boolean;
96   /** @internal */ readonly marks: readonly CapturedOhlcBarMark<D>[];
97   /** @internal */ readonly numeric: NumericRenderedSeries<D>;
98   /** @internal Converts a root-logical point to local geometry.
99    * @param point - Root-logical coordinate.
100    * @returns Local coordinate, or undefined for a singular transform.
101    */ localAtRoot(
102     point: Readonly<RenderedSeriesPoint>,
103   ): Readonly<RenderedSeriesPoint> | undefined;
104 }
105 /** Mount-owned optional OHLC bar receiver.
106  * @see {@link useOhlcBarInspection}
107  */
108 export interface OhlcBarInspectionRef<D> {
109   /** @returns Current borrowed generation, or undefined before paint and after disposal. */
110   rendered(): OhlcBarRenderedSeries<D> | undefined;
111   /** @internal Records completed local paint without querying.
112    * @param input - Captured marks and committed mappings.
113    */ capture(input: {
114     /** Local marks in source order. */
115     marks: readonly CapturedOhlcBarMark<D>[];
116     /** Committed horizontal mapping. */
117     xScale: NumericPositionScale;
118     /** Committed price mapping. */
119     yScale: ResolvedContinuousScale;
120   }): void;
121 }
122 
123 /** Creates a stable OHLC-bar inspection ref without reading data or subscribing.
124  * @param _inputs - Inputs retained only to infer the original datum type.
125  * @returns A ref for exactly one OhlcBarSeries inspection prop.
126  * @see {@link OhlcBarInspectionRef}
127  */
128 export function useOhlcBarInspection<D>(
129   _inputs: Pick<
130     OhlcBarSeriesProps<D>,
131     "data" | "time" | "open" | "high" | "low" | "close"
132   >,
133 ): OhlcBarInspectionRef<D> {
134   return useRenderedRef<
135     {
136       marks: readonly CapturedOhlcBarMark<D>[];
137       xScale: NumericPositionScale;
138       yScale: ResolvedContinuousScale;
139     },
140     OhlcBarRenderedSeries<D>
141   >("ohlcBarInspection", (candidate, geometry) => {
142     const observations = Object.freeze(
143       candidate.marks.map((mark) =>
144         Object.freeze({
145           datum: mark.datum,
146           sourceIndex: mark.sourceIndex,
147           time: mark.time,
148           open: mark.open,
149           high: mark.high,
150           low: mark.low,
151           close: mark.close,
152           anchorLocal: mark.anchorLocal,
153           anchorRoot: geometry.rootAtLocal(mark.anchorLocal),
154           stemLocal: mark.stemLocal,
155           openTickLocal: mark.openTickLocal,
156           closeTickLocal: mark.closeTickLocal,
157         }),
158       ),
159     );
160     const numeric: NumericRenderedSeries<D> = Object.freeze({
161       observations: Object.freeze(
162         observations.map((row) =>
163           Object.freeze({
164             ...row,
165             domainX: row.time,
166             domainY: row.close,
167             run: 0,
168           }),
169         ),
170       ),
171       assertLive: geometry.assertLive,
172       domainXAtRoot(point: Readonly<RenderedSeriesPoint>) {
173         const local = geometry.localAtRoot(point);
174         return local
175           ? invertNumericPosition(candidate.xScale, local.x)
176           : undefined;
177       },
178       anchorInsideClips: geometry.anchorInsideClips,
179       anchorAtDomain(point: Readonly<{ domainX: number; domainY: number }>) {
180         geometry.assertLive();
181         const anchorLocal = Object.freeze({
182           x: candidate.xScale.map(point.domainX),
183           y: candidate.yScale.map(point.domainY),
184         });
185         return Object.freeze({
186           anchorLocal,
187           anchorRoot: geometry.rootAtLocal(anchorLocal),
188         });
189       },
190     });
191     return Object.freeze({
192       observations,
193       marks: Object.freeze(candidate.marks.slice()),
194       numeric,
195       assertLive: geometry.assertLive,
196       localAtRoot: geometry.localAtRoot,
197       anchorInsideClips: geometry.anchorInsideClips,
198       timeAtRoot: numeric.domainXAtRoot,
199     });
200   });
201 }
202 
203 /** Selects captured original OHLC by nearest timestamp, retaining source-order ties.
204  * @param rendered - Borrowed committed OHLC generation.
205  * @param options - Timestamp and optional pure candidate filter.
206  * @returns First source-order match on a tie, or undefined when none qualifies.
207  * @see {@link NearestOhlcBarHit}
208  */
209 export function nearestRecordedOhlcBarX<D>(
210   rendered: OhlcBarRenderedSeries<D>,
211   options: {
212     time: number;
213     filter?: (observation: OhlcBarInspectionCandidate<D>) => boolean;
214   },
215 ): NearestOhlcBarHit<D> | undefined {
216   const hit = nearestRecordedX(rendered.numeric, {
217     domainX: options.time,
218     filter: options.filter
219       ? (candidate) =>
220           options.filter!(
221             candidate as typeof candidate & OhlcBarInspectionCandidate<D>,
222           )
223       : undefined,
224   });
225   if (!hit) return undefined;
226   const row = rendered.observations.find(
227     (observation) => observation.sourceIndex === hit.sourceIndex,
228   )!;
229   return Object.freeze({ ...row, timeDelta: hit.domainDelta });
230 }
231 
232 function segmentCovers(
233   point: Readonly<RenderedSeriesPoint>,
234   segment: Readonly<{ x1: number; x2: number; y: number }>,
235   strokeWidth: number,
236 ): boolean {
237   if (strokeWidth <= 0) return false;
238   const half = strokeWidth / 2;
239   return (
240     point.x >= Math.min(segment.x1, segment.x2) &&
241     point.x <= Math.max(segment.x1, segment.x2) &&
242     point.y >= segment.y - half &&
243     point.y <= segment.y + half
244   );
245 }
246 function stemCovers(
247   point: Readonly<RenderedSeriesPoint>,
248   stem: Readonly<{ x: number; highY: number; lowY: number }>,
249   strokeWidth: number,
250 ): boolean {
251   return (
252     segmentCovers(point, { x1: stem.x, x2: stem.x, y: 0 }, 0) ||
253     (strokeWidth > 0 &&
254       point.x >= stem.x - strokeWidth / 2 &&
255       point.x <= stem.x + strokeWidth / 2 &&
256       point.y >= Math.min(stem.highY, stem.lowY) &&
257       point.y <= Math.max(stem.highY, stem.lowY))
258   );
259 }
260 /** Finds the last-painted OHLC segment covering a root-logical point.
261  * @param rendered - Borrowed committed OHLC generation.
262  * @param options - Root-logical point; clips and singular transforms produce no result.
263  * @returns Captured OHLC record and painted segment, or undefined for gaps.
264  * @see {@link PaintedOhlcBarHit}
265  */
266 export function paintedOhlcBarAt<D>(
267   rendered: OhlcBarRenderedSeries<D>,
268   options: { rootPoint: Readonly<RenderedSeriesPoint> },
269 ): PaintedOhlcBarHit<D> | undefined {
270   rendered.assertLive();
271   if (
272     !Number.isFinite(options.rootPoint.x) ||
273     !Number.isFinite(options.rootPoint.y)
274   )
275     throw new RangeError("paintedOhlcBarAt rootPoint must be finite.");
276   const point = rendered.localAtRoot(options.rootPoint);
277   if (!point || !rendered.anchorInsideClips(point)) return undefined;
278   for (let index = rendered.marks.length - 1; index >= 0; index--) {
279     const mark = rendered.marks[index]!;
280     if (segmentCovers(point, mark.closeTickLocal, mark.strokeWidthLocal))
281       return Object.freeze({
282         ...rendered.observations[index]!,
283         part: "closeTick" as const,
284       });
285     if (segmentCovers(point, mark.openTickLocal, mark.strokeWidthLocal))
286       return Object.freeze({
287         ...rendered.observations[index]!,
288         part: "openTick" as const,
289       });
290     if (stemCovers(point, mark.stemLocal, mark.strokeWidthLocal))
291       return Object.freeze({
292         ...rendered.observations[index]!,
293         part: "stem" as const,
294       });
295   }
296   return undefined;
297 }
298 

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