Skip to content

packages/core/src/features/viz/lib/ohlc-bar-series.tsx

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   Group,
3   Path,
4   resolveSignalValue,
5   type PibblEventHandlers,
6   type PibblEventParticipationOptions,
7   type PibblNode,
8   type SignalValue,
9   type StrokeStyle,
10 } from "@pibbl/core";
11 import { withHooksForbidden } from "@pibbl/core/internal";
12 import type {
13   OhlcBarInspectionRef,
14   CapturedOhlcBarMark,
15 } from "../inspection/ohlc-bar.js";
16 import { financialMarkWidth } from "./financial-geometry.js";
17 import { opacity, width } from "./paint.js";
18 import { finiteCoordinate, pointAccessor, pointData } from "./point-data.js";
19 import { useNumericPosition } from "./series-position.js";
20 import type { LineAccessor, ScaleId } from "./types.js";
21 import { useContinuousScale } from "./use-continuous-scale.js";
22 
23 /** Shared stroke presentation for an OHLC bar.
24  * @see {@link OhlcBarSeries}
25  */
26 export interface OhlcBarSeriesStyle {
27   /** Positive total width, or automatic 70% spacing capped at 12; singleton fallback 8. */ readonly bandwidth?:
28     number | "auto";
29   /** Stem and tick stroke; defaults to dark teal. Empty string disables painting. */ readonly stroke?: StrokeStyle;
30   /** Nonnegative local stroke width; default 1. */ readonly strokeWidth?: number;
31   /** Paint opacity from zero through one; default 1. */ readonly opacity?: number;
32   /** Cursor while the series owns targeting. */ readonly cursor?: string;
33 }
34 
35 /** Source-ordered OHLC records rendered as a high/low stem with open/close ticks.
36  * @see {@link OhlcBarSeries}
37  */
38 export interface OhlcBarSeriesProps<D>
39   extends PibblEventHandlers, PibblEventParticipationOptions {
40   /** Original records or signal; never sorted or mutated. */
41   readonly data: SignalValue<readonly D[]>;
42   /** UTC-millisecond property or callback. */
43   readonly time: LineAccessor<D>;
44   /** Opening price property or callback. */
45   readonly open: LineAccessor<D>;
46   /** High price property or callback. */
47   readonly high: LineAccessor<D>;
48   /** Low price property or callback. */
49   readonly low: LineAccessor<D>;
50   /** Closing price property or callback. */
51   readonly close: LineAccessor<D>;
52   /** Numeric, UTC, or trading-time horizontal mapping identity. */
53   readonly xScale: ScaleId;
54   /** Continuous price mapping identity. */
55   readonly yScale: ScaleId;
56   /** Tests original rows before accessor evaluation.
57    * @param datum - Original row.
58    * @param index - Source index including skipped rows.
59    * @param data - Complete source array.
60    * @returns Whether the bar participates.
61    */
62   readonly defined?: (datum: D, index: number, data: readonly D[]) => boolean;
63   /** Shared presentation or a hook-free original-row resolver.
64    * @param datum - Original row.
65    * @param index - Source index.
66    * @param data - Complete source array.
67    * @returns Plain OHLC-bar style.
68    */
69   readonly style?:
70     | SignalValue<OhlcBarSeriesStyle>
71     | ((datum: D, index: number, data: readonly D[]) => OhlcBarSeriesStyle);
72   /** Optional committed capture; the renderer does not execute inspection queries. */ readonly inspection?: OhlcBarInspectionRef<D>;
73 }
74 
75 /** Paints source-ordered OHLC bars with a high/low stem, left open tick, and right close tick.
76  * @param props - Original OHLC accessors, scales, styles, events, and optional capture.
77  * @returns Ordinary Pibbl composition; equal disjoint styles share paths.
78  * @see {@link OhlcBarSeriesProps}
79  */
80 export function OhlcBarSeries<D>({
81   data: input,
82   time: timeInput,
83   open: openInput,
84   high: highInput,
85   low: lowInput,
86   close: closeInput,
87   xScale,
88   yScale,
89   defined,
90   style: styleInput,
91   inspection,
92   pointerEvents,
93   ...events
94 }: OhlcBarSeriesProps<D>): PibblNode {
95   const xs = useNumericPosition(xScale),
96     ys = useContinuousScale(yScale).get();
97   const data = pointData(resolveSignalValue(input), "OhlcBarSeries");
98   const time = pointAccessor(timeInput),
99     open = pointAccessor(openInput),
100     high = pointAccessor(highInput),
101     low = pointAccessor(lowInput),
102     close = pointAccessor(closeInput);
103   const rows: {
104     datum: D;
105     sourceIndex: number;
106     time: number;
107     open: number;
108     high: number;
109     low: number;
110     close: number;
111     x: number;
112   }[] = [];
113   for (let i = 0; i < data.length; i++) {
114     const datum = data[i];
115     if (defined && !defined(datum, i, data)) continue;
116     const t = time(datum, i, data),
117       o = open(datum, i, data),
118       h = high(datum, i, data),
119       l = low(datum, i, data),
120       c = close(datum, i, data);
121     if (
122       !finiteCoordinate(t) ||
123       !finiteCoordinate(o) ||
124       !finiteCoordinate(h) ||
125       !finiteCoordinate(l) ||
126       !finiteCoordinate(c)
127     )
128       continue;
129     if (l > Math.min(o, c) || h < Math.max(o, c))
130       throw new RangeError(
131         `OhlcBarSeries: malformed OHLC at source index ${i}; require low <= open/close <= high.`,
132       );
133     rows.push({
134       datum,
135       sourceIndex: i,
136       time: t,
137       open: o,
138       high: h,
139       low: l,
140       close: c,
141       x: xs.map(t),
142     });
143   }
144   const defaultWidth = financialMarkWidth(rows.map((row) => row.x));
145   const specified =
146     typeof styleInput === "function"
147       ? styleInput
148       : resolveSignalValue(styleInput);
149   const marks: CapturedOhlcBarMark<D>[] | undefined = inspection
150     ? []
151     : undefined;
152   const batches: {
153     stem: Path2D;
154     openTick: Path2D;
155     closeTick: Path2D;
156     stroke: StrokeStyle;
157     strokeWidth: number;
158     opacity: number;
159     cursor: string | undefined;
160     minX: number;
161     maxX: number;
162   }[] = [];
163   for (const row of rows) {
164     const style =
165       typeof specified === "function"
166         ? withHooksForbidden("OhlcBarSeries style callback", () =>
167             specified(row.datum, row.sourceIndex, data),
168           )
169         : (specified ?? {});
170     const markWidth =
171       style.bandwidth === undefined || style.bandwidth === "auto"
172         ? defaultWidth
173         : financialMarkWidth([], { width: style.bandwidth });
174     const halfWidth = markWidth / 2;
175     const stemLocal = Object.freeze({
176       x: row.x,
177       highY: ys.map(row.high),
178       lowY: ys.map(row.low),
179     });
180     const openTickLocal = Object.freeze({
181       x1: row.x - halfWidth,
182       x2: row.x,
183       y: ys.map(row.open),
184     });
185     const closeTickLocal = Object.freeze({
186       x1: row.x,
187       x2: row.x + halfWidth,
188       y: ys.map(row.close),
189     });
190     const stroke = style.stroke ?? "#173a35",
191       strokeWidth = width(style.strokeWidth ?? 1, "OhlcBarSeries"),
192       alpha = opacity(style.opacity ?? 1, "OhlcBarSeries");
193     marks?.push(
194       Object.freeze({
195         datum: row.datum,
196         sourceIndex: row.sourceIndex,
197         time: row.time,
198         open: row.open,
199         high: row.high,
200         low: row.low,
201         close: row.close,
202         anchorLocal: Object.freeze({ x: row.x, y: closeTickLocal.y }),
203         stemLocal,
204         openTickLocal,
205         closeTickLocal,
206         strokeWidthLocal: stroke && alpha > 0 ? strokeWidth : 0,
207       }),
208     );
209     if (alpha === 0 || !stroke) continue;
210     const xPad = strokeWidth / 2,
211       minX = row.x - halfWidth - xPad,
212       maxX = row.x + halfWidth + xPad;
213     let batch = batches.at(-1);
214     if (
215       !batch ||
216       (minX < batch.maxX && maxX > batch.minX) ||
217       batch.stroke !== stroke ||
218       batch.strokeWidth !== strokeWidth ||
219       batch.opacity !== alpha ||
220       batch.cursor !== style.cursor
221     ) {
222       batch = {
223         stem: new Path2D(),
224         openTick: new Path2D(),
225         closeTick: new Path2D(),
226         stroke,
227         strokeWidth,
228         opacity: alpha,
229         cursor: style.cursor,
230         minX,
231         maxX,
232       };
233       batches.push(batch);
234     }
235     batch.minX = Math.min(batch.minX, minX);
236     batch.maxX = Math.max(batch.maxX, maxX);
237     batch.stem.moveTo(stemLocal.x, stemLocal.highY);
238     batch.stem.lineTo(stemLocal.x, stemLocal.lowY);
239     batch.openTick.moveTo(openTickLocal.x1, openTickLocal.y);
240     batch.openTick.lineTo(openTickLocal.x2, openTickLocal.y);
241     batch.closeTick.moveTo(closeTickLocal.x1, closeTickLocal.y);
242     batch.closeTick.lineTo(closeTickLocal.x2, closeTickLocal.y);
243   }
244   inspection?.capture({ marks: marks!, xScale: xs, yScale: ys });
245   return (
246     <Group {...events}>
247       {batches.map((batch, index) => (
248         <Group key={index}>
249           <Path
250             pointerEvents={pointerEvents}
251             style={{
252               d: batch.stem,
253               stroke: batch.stroke,
254               strokeWidth: batch.strokeWidth,
255               opacity: batch.opacity,
256               cursor: batch.cursor,
257             }}
258           />
259           <Path
260             pointerEvents={pointerEvents}
261             style={{
262               d: batch.openTick,
263               stroke: batch.stroke,
264               strokeWidth: batch.strokeWidth,
265               opacity: batch.opacity,
266               cursor: batch.cursor,
267             }}
268           />
269           <Path
270             pointerEvents={pointerEvents}
271             style={{
272               d: batch.closeTick,
273               stroke: batch.stroke,
274               strokeWidth: batch.strokeWidth,
275               opacity: batch.opacity,
276               cursor: batch.cursor,
277             }}
278           />
279         </Group>
280       ))}
281     </Group>
282   );
283 }
284 

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