packages/core/src/features/viz/lib/candlestick-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 FillStyle,
6 type StrokeStyle,
7 type SignalValue,
8 type PibblEventHandlers,
9 type PibblEventParticipationOptions,
10 type PibblNode,
11 } from "@pibbl/core";
12 import { withHooksForbidden } from "@pibbl/core/internal";
13 import type { LineAccessor, ScaleId } from "./types.js";
14 import type {
15 CandleInspectionRef,
16 CapturedCandleMark,
17 } from "../inspection/candle.js";
18 import { useNumericPosition } from "./series-position.js";
19 import { useContinuousScale } from "./use-continuous-scale.js";
20 import { pointAccessor, pointData, finiteCoordinate } from "./point-data.js";
21 import { financialMarkWidth, candleGeometry } from "./financial-geometry.js";
22 import { opacity, width } from "./paint.js";
23 /** Open/close body paint, independent from wick paint.
24 * @see {@link CandlestickSeries}
25 */
26 export interface CandleBodyStyle {
27 /** Interior; default teal for close >= open, coral otherwise. Empty string disables fill. */ readonly fill?: FillStyle;
28 /** Optional body outline. */ readonly stroke?: StrokeStyle;
29 /** Nonnegative local outline width; default 1. */ readonly strokeWidth?: number;
30 }
31 /** High/low wick paint.
32 * @see {@link CandlestickSeries}
33 */
34 export interface CandleWickStyle {
35 /** Wick paint; default dark teal. Empty string disables wick paint. */ readonly stroke?: StrokeStyle;
36 /** Nonnegative local width; default 1. */ readonly strokeWidth?: number;
37 }
38 /** Accepted named body/wick presentation for OHLC marks.
39 * @see {@link CandlestickSeries}
40 */
41 export interface CandlestickSeriesStyle {
42 /** Positive local body width, or automatic 70% spacing capped at 12; singleton fallback 8. */ readonly bandwidth?:
43 number | "auto";
44 /** Independent open/close body appearance. */ readonly body?: CandleBodyStyle;
45 /** Independent high/low wick appearance. */ readonly wick?: CandleWickStyle;
46 /** Opacity zero through one; default 1. */ readonly opacity?: number;
47 /** Cursor while the series owns targeting. */ readonly cursor?: string;
48 }
49 /** Source-ordered OHLC records with named accessors and optional shared inspection.
50 * @see {@link CandlestickSeries}
51 */
52 export interface CandlestickSeriesProps<D>
53 extends PibblEventHandlers, PibblEventParticipationOptions {
54 /** Original records/signal; never sorted or mutated. */ readonly data: SignalValue<
55 readonly D[]
56 >;
57 /** UTC-millisecond property or callback. */ readonly time: LineAccessor<D>;
58 /** Open price property or callback. */ readonly open: LineAccessor<D>;
59 /** High price property or callback. */ readonly high: LineAccessor<D>;
60 /** Low price property or callback. */ readonly low: LineAccessor<D>;
61 /** Close price property or callback. */ readonly close: LineAccessor<D>;
62 /** Numeric/UTC/trading horizontal mapping identity. */ readonly xScale: ScaleId;
63 /** Independent numeric price mapping identity. */ readonly yScale: ScaleId;
64 /** Tests original rows before accessor evaluation.
65 * @param datum - Original row.
66 * @param index - Source index including skipped rows.
67 * @param data - Original complete array.
68 * @returns Whether the candle participates.
69 */ readonly defined?: (
70 datum: D,
71 index: number,
72 data: readonly D[],
73 ) => boolean;
74 /** Named body/wick appearance or a hook-free datum resolver.
75 * @param datum - Original row.
76 * @param index - Original source index.
77 * @param data - Complete source array.
78 * @returns Plain named candle styles.
79 */ readonly style?:
80 | SignalValue<CandlestickSeriesStyle>
81 | ((datum: D, index: number, data: readonly D[]) => CandlestickSeriesStyle);
82 /** Optional capture on the ordinary inspection foundation; no queries while painting. */ readonly inspection?: CandleInspectionRef<D>;
83 }
84 /**
85 * Paints OHLC bodies and wicks, preserving source order and skipping complete missing candles.
86 * Finite malformed OHLC rejects; equal open/close paints a one-local-unit doji.
87 * @param props - Named OHLC accessors, scales, styles and optional inspection.
88 * @returns Ordinary Pibbl composition; disjoint equal styles share paint paths.
89 * @see {@link CandlestickSeriesProps}
90 */
91 export function CandlestickSeries<D>({
92 data: input,
93 time: timeInput,
94 open: openInput,
95 high: highInput,
96 low: lowInput,
97 close: closeInput,
98 xScale,
99 yScale,
100 defined,
101 style: styleInput,
102 inspection,
103 pointerEvents,
104 ...events
105 }: CandlestickSeriesProps<D>): PibblNode {
106 const xs = useNumericPosition(xScale),
107 ys = useContinuousScale(yScale).get();
108 const data = pointData(resolveSignalValue(input), "CandlestickSeries");
109 const time = pointAccessor(timeInput),
110 open = pointAccessor(openInput),
111 high = pointAccessor(highInput),
112 low = pointAccessor(lowInput),
113 close = pointAccessor(closeInput);
114 const rows: {
115 datum: D;
116 sourceIndex: number;
117 time: number;
118 open: number;
119 high: number;
120 low: number;
121 close: number;
122 x: number;
123 }[] = [];
124 for (let i = 0; i < data.length; i++) {
125 const datum = data[i];
126 if (defined && !defined(datum, i, data)) continue;
127 const t = time(datum, i, data),
128 o = open(datum, i, data),
129 h = high(datum, i, data),
130 l = low(datum, i, data),
131 c = close(datum, i, data);
132 if (
133 !finiteCoordinate(t) ||
134 !finiteCoordinate(o) ||
135 !finiteCoordinate(h) ||
136 !finiteCoordinate(l) ||
137 !finiteCoordinate(c)
138 )
139 continue;
140 if (l > Math.min(o, c) || h < Math.max(o, c))
141 throw new RangeError(
142 `CandlestickSeries: malformed OHLC at source index ${i}; require low <= open/close <= high.`,
143 );
144 rows.push({
145 datum,
146 sourceIndex: i,
147 time: t,
148 open: o,
149 high: h,
150 low: l,
151 close: c,
152 x: xs.map(t),
153 });
154 }
155 const defaultWidth = financialMarkWidth(rows.map((row) => row.x));
156 const specified =
157 typeof styleInput === "function"
158 ? styleInput
159 : resolveSignalValue(styleInput);
160 const marks: CapturedCandleMark<D>[] | undefined = inspection
161 ? []
162 : undefined;
163 const batches: {
164 body: Path2D;
165 wick: Path2D;
166 fill: FillStyle;
167 bodyStroke: StrokeStyle | undefined;
168 bodyWidth: number;
169 wickStroke: StrokeStyle;
170 wickWidth: number;
171 opacity: number;
172 cursor: string | undefined;
173 minX: number;
174 maxX: number;
175 }[] = [];
176 for (const row of rows) {
177 const style =
178 typeof specified === "function"
179 ? withHooksForbidden("CandlestickSeries style callback", () =>
180 specified(row.datum, row.sourceIndex, data),
181 )
182 : (specified ?? {});
183 const markWidth =
184 style.bandwidth === undefined || style.bandwidth === "auto"
185 ? defaultWidth
186 : financialMarkWidth([], { width: style.bandwidth });
187 const g = candleGeometry({
188 centerX: row.x,
189 openY: ys.map(row.open),
190 closeY: ys.map(row.close),
191 highY: ys.map(row.high),
192 lowY: ys.map(row.low),
193 width: markWidth,
194 dojiHeight: 1,
195 });
196 const fill =
197 style.body?.fill ?? (row.close >= row.open ? "#087f8c" : "#d9674c"),
198 bodyStroke = style.body?.stroke,
199 bodyWidth = width(style.body?.strokeWidth ?? 1, "CandlestickSeries body"),
200 wickStroke = style.wick?.stroke ?? "#173a35",
201 wickWidth = width(style.wick?.strokeWidth ?? 1, "CandlestickSeries wick"),
202 alpha = opacity(style.opacity ?? 1, "CandlestickSeries");
203 marks?.push(
204 Object.freeze({
205 datum: row.datum,
206 sourceIndex: row.sourceIndex,
207 time: row.time,
208 open: row.open,
209 high: row.high,
210 low: row.low,
211 close: row.close,
212 anchorLocal: Object.freeze({ x: row.x, y: ys.map(row.close) }),
213 bodyBoundsLocal: g.bodyBoundsLocal,
214 wickLocal: g.wickLocal,
215 bodyFill: !!fill && alpha > 0,
216 bodyStrokeWidthLocal: bodyStroke && alpha > 0 ? bodyWidth : 0,
217 wickStrokeWidthLocal: wickStroke && alpha > 0 ? wickWidth : 0,
218 }),
219 );
220 if (alpha === 0) continue;
221 const half =
222 Math.max(bodyStroke ? bodyWidth : 0, wickStroke ? wickWidth : 0) / 2,
223 minX = g.bodyBoundsLocal.x - half,
224 maxX = g.bodyBoundsLocal.x + markWidth + half;
225 let batch = batches.at(-1);
226 if (
227 !batch ||
228 (minX < batch.maxX && maxX > batch.minX) ||
229 batch.fill !== fill ||
230 batch.bodyStroke !== bodyStroke ||
231 batch.bodyWidth !== bodyWidth ||
232 batch.wickStroke !== wickStroke ||
233 batch.wickWidth !== wickWidth ||
234 batch.opacity !== alpha ||
235 batch.cursor !== style.cursor
236 ) {
237 batch = {
238 body: new Path2D(),
239 wick: new Path2D(),
240 fill,
241 bodyStroke,
242 bodyWidth,
243 wickStroke,
244 wickWidth,
245 opacity: alpha,
246 cursor: style.cursor,
247 minX,
248 maxX,
249 };
250 batches.push(batch);
251 }
252 batch.minX = Math.min(batch.minX, minX);
253 batch.maxX = Math.max(batch.maxX, maxX);
254 const b = g.bodyBoundsLocal;
255 batch.body.rect(b.x, b.y, b.width, b.height);
256 batch.wick.moveTo(g.wickLocal.x, g.wickLocal.highY);
257 batch.wick.lineTo(g.wickLocal.x, g.wickLocal.lowY);
258 }
259 inspection?.capture({ marks: marks!, xScale: xs, yScale: ys });
260 return (
261 <Group {...events}>
262 {batches.map((batch, index) => (
263 <Group key={index}>
264 <Path
265 pointerEvents={pointerEvents}
266 style={{
267 d: batch.wick,
268 stroke: batch.wickStroke,
269 strokeWidth: batch.wickWidth,
270 opacity: batch.opacity,
271 cursor: batch.cursor,
272 }}
273 />
274 <Path
275 pointerEvents={pointerEvents}
276 style={{
277 d: batch.body,
278 fill: batch.fill,
279 stroke: batch.bodyStroke,
280 strokeWidth: batch.bodyWidth,
281 opacity: batch.opacity,
282 cursor: batch.cursor,
283 }}
284 />
285 </Group>
286 ))}
287 </Group>
288 );
289 }
290
Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision dd39ce2. ALPHA — NOT FOR PRODUCTION USE.