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