Skip to content

packages/core/src/features/viz/lib/volume-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 { 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 built with @pibbl/core 0.0.2, revision dd39ce2. ALPHA — NOT FOR PRODUCTION USE.