packages/core/src/features/viz/lib/paired-series.tsx
This is the source snapshot used to build these API details. View this revision on GitHub.
1 import { Path, resolveSignalValue } from "@pibbl/core";
2 import type {
3 PibblEventHandlers,
4 PibblEventParticipationOptions,
5 SignalValue,
6 StrokeStyle,
7 } from "@pibbl/core";
8 import { withHooksForbidden } from "@pibbl/core/internal";
9 import type { SignalStyle } from "@pibbl/core/internal";
10 import type { LineAccessor, ScaleId, SeriesCoordinate } from "./types.js";
11 import type { TrajectorySeriesStyle } from "./trajectory-series.js";
12 import { pointAccessor, pointData } from "./point-data.js";
13 import { opacity, width } from "./paint.js";
14 import {
15 mapSeriesPosition,
16 useNumericPosition,
17 useSeriesPosition,
18 } from "./series-position.js";
19
20 /** Shared connector appearance for {@link PairedSeries}. */
21 export type PairedSeriesStyle = TrajectorySeriesStyle;
22
23 /** Authored wide-record inputs for {@link PairedSeries}. */
24 export interface PairedSeriesProps<D>
25 extends PibblEventHandlers, PibblEventParticipationOptions {
26 /** Source records or a signal containing them. */
27 readonly data: SignalValue<readonly D[]>;
28 /** Numeric baseline or first-observation value. */
29 readonly before: LineAccessor<D>;
30 /** Numeric follow-up or second-observation value. */
31 readonly after: LineAccessor<D>;
32 /** Fixed x coordinate used for every `before` endpoint. */
33 readonly beforeX: SeriesCoordinate;
34 /** Fixed x coordinate used for every `after` endpoint. */
35 readonly afterX: SeriesCoordinate;
36 /** Scale mapping the two endpoint coordinates. */
37 readonly xScale: ScaleId;
38 /** Numeric scale mapping both endpoint values. */
39 readonly yScale: ScaleId;
40 /**
41 * Excludes a record before before/after accessors run.
42 * @param datum - Candidate source datum.
43 * @param index - Zero-based source index.
44 * @param data - Complete source data array.
45 * @returns Whether this record contributes a connector.
46 */
47 readonly defined?: (datum: D, index: number, data: readonly D[]) => boolean;
48 /** Shared connector style, including whole-object and shallow field signals. */
49 readonly style?: SignalValue<SignalStyle<PairedSeriesStyle>>;
50 }
51
52 interface ResolvedPairedStyle {
53 readonly stroke: StrokeStyle;
54 readonly strokeWidth: number;
55 readonly lineInteractionWidth: number;
56 readonly opacity: number;
57 readonly cursor?: string;
58 }
59
60 function style(input: unknown): ResolvedPairedStyle {
61 if (input === null || typeof input !== "object" || Array.isArray(input))
62 throw new TypeError("PairedSeries: style must be an object.");
63 const value = input as Record<string, unknown>;
64 for (const field of Object.keys(value))
65 if (
66 ![
67 "stroke",
68 "strokeWidth",
69 "lineInteractionWidth",
70 "opacity",
71 "cursor",
72 ].includes(field)
73 )
74 throw new TypeError(`PairedSeries: unsupported style field ${field}.`);
75 const stroke = resolveSignalValue(value.stroke) ?? "#2563eb";
76 const strokeWidth = width(
77 (resolveSignalValue(value.strokeWidth) ?? 2) as number,
78 "PairedSeries strokeWidth",
79 );
80 const lineInteractionWidth = width(
81 (resolveSignalValue(value.lineInteractionWidth) ?? strokeWidth) as number,
82 "PairedSeries lineInteractionWidth",
83 );
84 const cursor = resolveSignalValue(value.cursor);
85 if (cursor !== undefined && typeof cursor !== "string")
86 throw new TypeError("PairedSeries: cursor must be a string.");
87 return {
88 stroke: stroke as StrokeStyle,
89 strokeWidth,
90 lineInteractionWidth,
91 opacity: opacity(
92 (resolveSignalValue(value.opacity) ?? 1) as number,
93 "PairedSeries opacity",
94 ),
95 cursor,
96 };
97 }
98
99 /**
100 * Draws a connector for each complete wide-record before/after pair.
101 *
102 * The component does not manufacture point marks, calculate change, select a
103 * baseline, or impute missing observations. Compose `PointSeries` for endpoints.
104 *
105 * @param props - Wide records, endpoint accessors and coordinates, scales, and shared paint.
106 * @returns One batched ordinary Pibbl path containing source-ordered pair connectors.
107 * @see {@link PairedSeriesProps}
108 */
109 export function PairedSeries<D>({
110 data: input,
111 before: beforeInput,
112 after: afterInput,
113 beforeX,
114 afterX,
115 xScale: xId,
116 yScale: yId,
117 defined,
118 style: styleInput,
119 ...events
120 }: PairedSeriesProps<D>) {
121 const xs = useSeriesPosition(xId);
122 const ys = useNumericPosition(yId);
123 const data = pointData(resolveSignalValue(input), "PairedSeries");
124 const before = pointAccessor(beforeInput);
125 const after = pointAccessor(afterInput);
126 const startX = mapSeriesPosition(xs, beforeX);
127 const endX = mapSeriesPosition(xs, afterX);
128 const resolved = style(resolveSignalValue(styleInput) ?? {});
129 const path = new Path2D();
130 if (startX !== undefined && endX !== undefined)
131 for (let i = 0; i < data.length; i++) {
132 const datum = data[i];
133 const row = withHooksForbidden(
134 "PairedSeries: accessors cannot call hooks.",
135 () => {
136 if (defined && !defined(datum, i, data)) return undefined;
137 return {
138 before: before(datum, i, data),
139 after: after(datum, i, data),
140 };
141 },
142 );
143 if (!row) continue;
144 const startY = mapSeriesPosition(ys, row.before);
145 const endY = mapSeriesPosition(ys, row.after);
146 if (startY === undefined || endY === undefined) continue;
147 path.moveTo(startX, startY);
148 path.lineTo(endX, endY);
149 }
150 return <Path {...events} style={{ d: path, ...resolved }} />;
151 }
152
Documentation version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision dd39ce2. ALPHA — NOT FOR PRODUCTION USE.