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