Skip to content

packages/core/src/features/viz/lib/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   isSignal,
4   Rectangle,
5   resolveSignalValue,
6   useCanvasContext,
7 } from "@pibbl/core";
8 import type {
9   PibblEventHandlers,
10   PibblEventParticipationOptions,
11   PibblNode,
12   FillStyle,
13   SignalValue,
14   StrokeStyle,
15 } from "@pibbl/core";
16 import { withHooksForbidden } from "@pibbl/core/internal";
17 import { lookupScale } from "./scale-context.js";
18 import type { SignalStyle } from "@pibbl/core/internal";
19 import type { BandCategory, LineAccessor, ScaleId } from "./types.js";
20 import { useContinuousScale } from "./use-continuous-scale.js";
21 import { useBarScale } from "./use-bar-scale.js";
22 import { pointAccessor, pointData, finiteCoordinate } from "./point-data.js";
23 import { opacity, width } from "./paint.js";
24 /**
25  * A data property or callback that extracts a category, with nullish values treated as missing.
26  *
27  * @param datum - Datum whose category is being read.
28  * @param index - Zero-based index in the source data.
29  * @param data - Complete source data array.
30  * @returns The category, or null/undefined to indicate a missing category. See
31  * {@link BandCategory} .
32  *
33  * @see {@link BandCategory}
34  * @see {@link BarSeriesProps}
35  */
36 export type BarCategoryAccessor<D> =
37   | {
38       [K in keyof D]-?: D[K] extends BandCategory | null | undefined
39         ? K
40         : never;
41     }[keyof D]
42   | ((
43       datum: D,
44       index: number,
45       data: readonly D[],
46     ) => BandCategory | null | undefined);
47 import { barFill, type BarThresholdFill } from "./bar-fill.js";
48 export type { BarThresholdFill } from "./bar-fill.js";
49 /**
50  * Supported geometry and presentation properties for BarSeries.
51  *
52  * @see {@link FillStyle}
53  * @see {@link BarThresholdFill}
54  * @see {@link StrokeStyle}
55  * @see {@link BarStyleResolver}
56  */
57 export interface BarSeriesStyle {
58   /** Width allocated to one categorical band. See {@link BarSeriesStyle}. */
59   readonly bandwidth?: number | "auto";
60   /** Paint used for the interior. See {@link FillStyle}, {@link BarThresholdFill}. */
61   readonly fill?: FillStyle | BarThresholdFill;
62   /** Paint used for the outline. See {@link StrokeStyle}. */
63   readonly stroke?: StrokeStyle;
64   /** Width of the painted outline. See {@link BarSeriesStyle}. */
65   readonly strokeWidth?: number;
66   /** Opacity of the painted result. See {@link BarSeriesStyle}. */
67   readonly opacity?: number;
68   /** Cursor shown while this target owns pointer presentation. See {@link BarSeriesStyle}. */
69   readonly cursor?: string;
70 }
71 /**
72  * Computes bar styling from the datum, index, and source data.
73  *
74  * @param datum - Datum being styled.
75  * @param index - Zero-based index in the source data.
76  * @param data - Complete source data array.
77  * @returns Style for this datum's bar. See {@link BarSeriesStyle}.
78  *
79  * @see {@link BarSeriesStyle}
80  */
81 export type BarStyleResolver<D> = (
82   datum: D,
83   index: number,
84   data: readonly D[],
85 ) => BarSeriesStyle;
86 /**
87  * Shared data, category/value scales, orientation, filtering, and styling for bar variants.
88  *
89  * @see {@link BarCategoryAccessor}
90  * @see {@link BarSeriesStyle}
91  * @see {@link BarSeriesProps}
92  */
93 interface BarSeriesBaseProps<D> extends PibblEventHandlers, PibblEventParticipationOptions {
94   /**
95    * Application data supplied to the component or reported by the event. See {@link SignalValue}.
96    */
97   readonly data: SignalValue<readonly D[]>;
98   /** Selects `"vertical"`, `"horizontal"` for orientation. See {@link BarSeriesBaseProps}. */
99   readonly orientation?: "vertical" | "horizontal";
100   /** Property or callback extracting a bar's discrete category. See {@link BarCategoryAccessor}. */
101   readonly category: BarCategoryAccessor<D>;
102   /** Band scale used to place categories. See {@link ScaleId}. */
103   readonly categoryScale: ScaleId;
104   /** Numeric scale used to map bar values. See {@link ScaleId}. */
105   readonly valueScale: ScaleId;
106   /**
107    * Selects the records that participate in the bar series. See {@link BarSeriesProps}.
108    * @param datum - Record to test.
109    * @param index - Zero-based index in the source array.
110    * @param data - Complete source data array.
111    * @returns Whether this record should contribute a bar.
112    */
113   readonly defined?: (datum: D, index: number, data: readonly D[]) => boolean;
114   /**
115    * Declared presentation and layout properties. See {@link SignalValue}, {@link SignalStyle},
116    * {@link BarSeriesStyle}, {@link BarStyleResolver}.
117    */
118   readonly style?: SignalValue<SignalStyle<BarSeriesStyle> | BarStyleResolver<D>>;
119 }
120 /**
121  * Authored inputs for BarSeries, including the declared data and presentation options.
122  *
123  * @see {@link LineAccessor}
124  * @see {@link SignalValue}
125  * @see {@link BarCategoryAccessor}
126  * @see {@link ScaleId}
127  * @see {@link BarSeries}
128  */
129 export type BarSeriesProps<D> = BarSeriesBaseProps<D> &
130   (
131     | {
132         /** Value associated with this sample, input, or result. See {@link LineAccessor}. */
133         readonly value: LineAccessor<D>;
134         /** Numeric value from which each ordinary bar starts. See {@link SignalValue}. */
135         readonly baseline?: SignalValue<number>;
136         /**
137          * Not accepted in this variant; use the alternative fields instead. See
138          * {@link BarSeriesProps}.
139          */
140         readonly valueStart?: never;
141         /**
142          * Not accepted in this variant; use the alternative fields instead. See
143          * {@link BarSeriesProps}.
144          */
145         readonly valueEnd?: never;
146       }
147     | {
148         /**
149          * Not accepted in this variant; use the alternative fields instead. See
150          * {@link BarSeriesProps}.
151          */
152         readonly value?: never;
153         /**
154          * Not accepted in this variant; use the alternative fields instead. See
155          * {@link BarSeriesProps}.
156          */
157         readonly baseline?: never;
158         /** Accessor for the beginning of a range bar. See {@link LineAccessor}. */
159         readonly valueStart: LineAccessor<D>;
160         /** Accessor for the end of a range bar. See {@link LineAccessor}. */
161         readonly valueEnd: LineAccessor<D>;
162       }
163   ) &
164   (
165     | {
166         /** Accessor identifying the subgroup of each bar. See {@link BarCategoryAccessor}. */
167         readonly group: BarCategoryAccessor<D>;
168         /** Band scale used to place subgroups within a category. See {@link ScaleId}. */
169         readonly groupScale: ScaleId;
170       }
171     | {
172         /** Accessor identifying the subgroup of each bar. See {@link BarSeriesProps}. */
173         readonly group?: never;
174         /**
175          * Not accepted in this variant; use the alternative fields instead. See
176          * {@link BarSeriesProps}.
177          */
178         readonly groupScale?: never;
179       }
180   );
181 /**
182  * Value bars in source order; rectangles supply ordinary paint and event geometry.
183  *
184  * @param props - Data, category and value accessors, scales, grouping, and styling. See
185  * {@link BarSeriesProps} .
186  * @returns Pibbl nodes drawing the bars.
187  *
188  * @see {@link BarSeriesProps}
189  */
190 export function BarSeries<D>({
191   data: input,
192   orientation = "vertical",
193   category: categoryInput,
194   value: valueInput,
195   valueStart,
196   valueEnd,
197   categoryScale,
198   valueScale,
199   baseline,
200   group,
201   groupScale,
202   defined,
203   style: styleInput,
204   pointerEvents,
205   ...events
206 }: BarSeriesProps<D>) {
207   const categories = useBarScale(categoryScale).get();
208   const values = useContinuousScale(valueScale).get();
209   const groups = lookupScale(groupScale, "BarSeries groupScale")?.get();
210   if ((group === undefined) !== (groupScale === undefined))
211     throw new TypeError("BarSeries requires group and groupScale together.");
212   if (groups && groups.type !== "band")
213     throw new TypeError("BarSeries groupScale requires a band scale.");
214   const groupValue =
215     typeof group === "function"
216       ? group
217       : (datum: D) =>
218           group === undefined
219             ? undefined
220             : (datum[group] as BandCategory | null | undefined);
221   const slotWidth =
222     groups?.type === "band" ? groups.bandwidth : categories.bandwidth;
223   if (orientation !== "vertical" && orientation !== "horizontal")
224     throw new TypeError("BarSeries orientation must be vertical or horizontal.");
225   const data = pointData(resolveSignalValue(input), "BarSeries");
226   const category =
227     typeof categoryInput === "function"
228       ? categoryInput
229       : (datum: D) =>
230           datum[categoryInput] as BandCategory | null | undefined;
231   const ranged = valueStart !== undefined || valueEnd !== undefined;
232   if (
233     ranged
234       ? valueInput !== undefined ||
235         baseline !== undefined ||
236         valueStart === undefined ||
237         valueEnd === undefined
238       : valueInput === undefined
239   )
240     throw new TypeError(
241       "BarSeries requires either value with optional baseline, or both valueStart and valueEnd without value or baseline.",
242     );
243   const value = pointAccessor((ranged ? valueEnd : valueInput)!);
244   const startValue = ranged ? pointAccessor(valueStart!) : undefined;
245   const baselineValue = resolveSignalValue(baseline ?? 0);
246   const sharedBase = ranged ? undefined : values.map(baselineValue);
247   const specified = resolveSignalValue(styleInput);
248   const ctx = useCanvasContext();
249   function normalize(input: unknown, callback: boolean) {
250     if (!input || typeof input !== "object" || Array.isArray(input))
251       throw new TypeError("BarSeries style must be an object.");
252     if (
253       callback &&
254       Object.getPrototypeOf(input) !== Object.prototype &&
255       Object.getPrototypeOf(input) !== null
256     )
257       throw new TypeError(
258         "BarSeries datum style must be a plain synchronous object.",
259       );
260     const style: Record<string, unknown> = {};
261     for (const key of Object.keys(input)) {
262       if (
263         ![
264           "bandwidth",
265           "fill",
266           "stroke",
267           "strokeWidth",
268           "opacity",
269           "cursor",
270         ].includes(key)
271       )
272         throw new TypeError(`BarSeries: unsupported style field ${key}.`);
273       const value = (input as Record<string, unknown>)[key];
274       if (callback && isSignal(value))
275         throw new TypeError(
276           "BarSeries datum styles require plain values; read signals explicitly.",
277         );
278       style[key] = callback ? value : resolveSignalValue(value);
279     }
280     const resolved = style as BarSeriesStyle;
281     if (resolved.cursor !== undefined && typeof resolved.cursor !== "string")
282       throw new TypeError("BarSeries cursor must be a string.");
283     return {
284       bandwidth:
285         resolved.bandwidth === undefined || resolved.bandwidth === "auto"
286           ? slotWidth
287           : width(resolved.bandwidth, "BarSeries bandwidth"),
288       fill: barFill(ctx, resolved.fill ?? "#2563eb", (value) =>
289         values.map(value),
290         orientation,
291       ),
292       stroke: resolved.stroke,
293       strokeWidth: width(resolved.strokeWidth ?? 1, "BarSeries strokeWidth"),
294       alpha: opacity(resolved.opacity ?? 1, "BarSeries"),
295       cursor: resolved.cursor,
296     };
297   }
298   const shared =
299     typeof specified === "function"
300       ? undefined
301       : normalize(specified ?? {}, false);
302   const nodes: PibblNode[] = [];
303   for (let i = 0; i < data.length; i++) {
304     const datum = data[i];
305     if (defined && !defined(datum, i, data)) continue;
306     const c = category(datum, i, data),
307       v = value(datum, i, data),
308       from = startValue ? startValue(datum, i, data) : baselineValue;
309     if (c == null || !finiteCoordinate(v) || !finiteCoordinate(from)) continue;
310     const categoryStart = categories.map(c);
311     const g = groups ? groupValue(datum, i, data) : undefined;
312     const offset = groups ? (g == null ? undefined : groups.map(g)) : 0;
313     if (categoryStart === undefined || offset === undefined || slotWidth === 0)
314       continue;
315     const start = categoryStart + offset;
316     const { bandwidth, fill, stroke, strokeWidth, alpha, cursor } =
317       shared ??
318       normalize(
319         withHooksForbidden("BarSeries style callback", () =>
320           (specified as BarStyleResolver<D>)(datum, i, data),
321         ),
322         true,
323       );
324     if (bandwidth === 0) continue;
325     const base = sharedBase ?? values.map(from);
326     const end = values.map(v);
327     const extent = Math.abs(end - base);
328     if (!Number.isFinite(extent))
329       throw new RangeError("BarSeries value extent must be finite.");
330     if (extent === 0) continue;
331     const vertical = orientation === "vertical";
332     nodes.push(
333       <Rectangle
334         key={i}
335         pointerEvents={pointerEvents}
336         style={{
337           left: vertical ? start + (slotWidth - bandwidth) / 2 : Math.min(base, end),
338           top: vertical ? Math.min(base, end) : start + (slotWidth - bandwidth) / 2,
339           width: vertical ? bandwidth : extent,
340           height: vertical ? extent : bandwidth,
341           fill: fill(from, v),
342           stroke,
343           strokeWidth,
344           opacity: alpha,
345           cursor,
346         }}
347       />,
348     );
349   }
350   return <Group {...events}>{nodes}</Group>;
351 }
352 

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