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