packages/core/src/features/viz/lib/volume-series.tsx
This is the source snapshot used to build these API details. View this revision on GitHub.
1 import { useNumericPosition } from "./series-position.js";
2 import {
3 Group,
4 Path,
5 resolveSignalValue,
6 type FillStyle,
7 type StrokeStyle,
8 type SignalValue,
9 type PibblEventHandlers,
10 type PibblEventParticipationOptions,
11 type PibblNode,
12 } from "@pibbl/core";
13 import { withHooksForbidden } from "@pibbl/core/internal";
14 import type { LineAccessor, ScaleId } from "./types.js";
15 import type {
16 VolumeInspectionRef,
17 CapturedVolumeMark,
18 } from "../inspection/volume.js";
19 import { useContinuousScale } from "./use-continuous-scale.js";
20 import { pointAccessor, pointData, finiteCoordinate } from "./point-data.js";
21 import { financialMarkWidth, volumeGeometry } from "./financial-geometry.js";
22 import { opacity, width } from "./paint.js";
23
24 /** Local column presentation; color meaning is supplied by the application.
25 * @see {@link VolumeSeries}
26 */
27 export interface VolumeSeriesStyle {
28 /** Positive column width in local logical units; default 70% spacing capped at 12. */
29 readonly width?: number;
30 /** Interior paint; defaults to teal. */
31 readonly fill?: FillStyle;
32 /** Optional column outline paint. */
33 readonly stroke?: StrokeStyle;
34 /** Nonnegative outline width in local units; default 1. */
35 readonly strokeWidth?: number;
36 /** Paint opacity from zero through one; default 1. */
37 readonly opacity?: number;
38 /** Cursor when the series owns coordinate targeting. */
39 readonly cursor?: string;
40 }
41 /** Time-aligned volume columns with an independent numeric y-scale.
42 * @see {@link VolumeSeries}
43 */
44 export interface VolumeSeriesProps<D>
45 extends PibblEventHandlers, PibblEventParticipationOptions {
46 /** Original records or a signal of records; never sorted or mutated. */
47 readonly data: SignalValue<readonly D[]>;
48 /** UTC-millisecond property or callback; missing/nonfinite values skip the row. */
49 readonly time: LineAccessor<D>;
50 /** Nonnegative volume property or callback; missing/nonfinite values skip the row. */
51 readonly volume: LineAccessor<D>;
52 /** Selects source records before evaluating accessors.
53 * @param datum - Original record.
54 * @param index - Original source index.
55 * @param data - Complete source array.
56 * @returns Whether the row participates.
57 */
58 readonly defined?: (datum: D, index: number, data: readonly D[]) => boolean;
59 /** Numeric time scale identity. */
60 readonly xScale: ScaleId;
61 /** Independent numeric volume scale identity; the baseline is zero. */
62 readonly yScale: ScaleId;
63 /** Constant presentation or a hook-free per-record resolver.
64 * @param datum - Original record.
65 * @param index - Original source index.
66 * @param data - Complete source array.
67 * @returns Column presentation.
68 */
69 readonly style?:
70 | SignalValue<VolumeSeriesStyle>
71 | ((datum: D, index: number, data: readonly D[]) => VolumeSeriesStyle);
72 /** Optional committed capture; importing the renderer does not import inspection queries. */
73 readonly inspection?: VolumeInspectionRef<D>;
74 }
75 /**
76 * Paints source-ordered time-aligned volume columns, batching adjacent equal styles.
77 * Zero volume is recorded but has no painted coverage. Finite negative volume rejects.
78 * @param props - Original records, numeric scales, accessors and optional capture.
79 * @returns Ordinary Pibbl paint/event composition.
80 * @see {@link VolumeSeriesProps}
81 */
82 export function VolumeSeries<D>({
83 data: input,
84 time: timeInput,
85 volume: volumeInput,
86 xScale,
87 yScale,
88 defined,
89 style: styleInput,
90 inspection,
91 pointerEvents,
92 ...events
93 }: VolumeSeriesProps<D>): PibblNode {
94 const xs = useNumericPosition(xScale),
95 ys = useContinuousScale(yScale).get();
96 const data = pointData(resolveSignalValue(input), "VolumeSeries");
97 const time = pointAccessor(timeInput),
98 volume = pointAccessor(volumeInput);
99 const rows: {
100 datum: D;
101 sourceIndex: number;
102 time: number;
103 volume: number;
104 x: number;
105 y: number;
106 }[] = [];
107 for (let i = 0; i < data.length; i++) {
108 const datum = data[i];
109 if (defined && !defined(datum, i, data)) continue;
110 const t = time(datum, i, data),
111 v = volume(datum, i, data);
112 if (!finiteCoordinate(t) || !finiteCoordinate(v)) continue;
113 if (v < 0)
114 throw new RangeError(
115 `VolumeSeries: negative volume at source index ${i}.`,
116 );
117 rows.push({
118 datum,
119 sourceIndex: i,
120 time: t,
121 volume: v,
122 x: xs.map(t),
123 y: ys.map(v),
124 });
125 }
126 const positions = rows.map((row) => row.x),
127 baselineY = ys.map(0),
128 defaultWidth = financialMarkWidth(positions);
129 const specified =
130 typeof styleInput === "function"
131 ? styleInput
132 : resolveSignalValue(styleInput);
133 const marks: CapturedVolumeMark<D>[] | undefined = inspection
134 ? []
135 : undefined;
136 const batches: {
137 path: Path2D;
138 fill: FillStyle;
139 stroke: StrokeStyle | undefined;
140 strokeWidth: number;
141 opacity: number;
142 cursor: string | undefined;
143 minX: number;
144 maxX: number;
145 }[] = [];
146 for (const row of rows) {
147 const style =
148 typeof specified === "function"
149 ? withHooksForbidden("VolumeSeries style callback", () =>
150 specified(row.datum, row.sourceIndex, data),
151 )
152 : (specified ?? {});
153 const markWidth =
154 style.width === undefined
155 ? defaultWidth
156 : financialMarkWidth([], { width: style.width });
157 const b = volumeGeometry({
158 centerX: row.x,
159 valueY: row.y,
160 baselineY,
161 width: markWidth,
162 });
163 const fill = style.fill ?? "#087f8c",
164 stroke = style.stroke,
165 strokeWidth = width(style.strokeWidth ?? 1, "VolumeSeries"),
166 alpha = opacity(style.opacity ?? 1, "VolumeSeries");
167 marks?.push(
168 Object.freeze({
169 datum: row.datum,
170 sourceIndex: row.sourceIndex,
171 time: row.time,
172 volume: row.volume,
173 baseline: 0,
174 anchorLocal: Object.freeze({ x: row.x, y: row.y }),
175 boundsLocal: b,
176 fill: !!fill && alpha > 0,
177 strokeWidthLocal: stroke && alpha > 0 ? strokeWidth : 0,
178 }),
179 );
180 if (b.height === 0 || alpha === 0) continue;
181 let batch = batches.at(-1);
182 const half = stroke ? strokeWidth / 2 : 0,
183 minX = b.x - half,
184 maxX = b.x + b.width + half;
185 if (
186 !batch ||
187 (minX < batch.maxX && maxX > batch.minX) ||
188 batch.fill !== fill ||
189 batch.stroke !== stroke ||
190 batch.strokeWidth !== strokeWidth ||
191 batch.opacity !== alpha ||
192 batch.cursor !== style.cursor
193 ) {
194 batch = {
195 path: new Path2D(),
196 fill,
197 stroke,
198 strokeWidth,
199 opacity: alpha,
200 cursor: style.cursor,
201 minX,
202 maxX,
203 };
204 batches.push(batch);
205 }
206 batch.minX = Math.min(batch.minX, minX);
207 batch.maxX = Math.max(batch.maxX, maxX);
208 batch.path.rect(b.x, b.y, b.width, b.height);
209 }
210 inspection?.capture({ marks: marks!, xScale: xs, yScale: ys });
211 return (
212 <Group {...events}>
213 {batches.map((batch, index) => (
214 <Path
215 key={index}
216 pointerEvents={pointerEvents}
217 style={{
218 d: batch.path,
219 fill: batch.fill,
220 stroke: batch.stroke,
221 strokeWidth: batch.strokeWidth,
222 opacity: batch.opacity,
223 cursor: batch.cursor,
224 }}
225 />
226 ))}
227 </Group>
228 );
229 }
230
Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision dd39ce2. ALPHA — NOT FOR PRODUCTION USE.