Skip to content

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