Skip to content

packages/core/src/features/viz/lib/area-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   FillStyle,
4   PibblEventHandlers,
5   PibblEventParticipationOptions,
6   SignalValue,
7 } from "@pibbl/core";
8 import {
9   useInternalValueMemoSlot,
10   validateTexturePaint,
11 } from "@pibbl/core/internal";
12 import type { SignalStyle } from "@pibbl/core/internal";
13 import type { LineAccessor, LineCoordinate, LineSeriesProps } from "./types.js";
14 import {
15   appendCurveForward,
16   curveGeometry,
17   resolveSeriesCurve,
18 } from "./curve-definition.js";
19 import { opacity } from "./paint.js";
20 import { useContinuousScale } from "./use-continuous-scale.js";
21 
22 /**
23  * Presentation properties for an {@link AreaSeries}.
24  *
25  * @see {@link FillStyle}
26  * @see {@link AreaSeriesProps}
27  */
28 export interface AreaSeriesStyle {
29   /** Paint used for the area interior. */
30   readonly fill?: FillStyle;
31   /** Opacity of the painted area, from zero through one. */
32   readonly opacity?: number;
33   /** Cursor shown while the area target owns pointer presentation. */
34   readonly cursor?: string;
35 }
36 
37 /**
38  * Authored inputs for {@link AreaSeries}.
39  *
40  * The data, coordinate accessors, scales, and defined predicate use the same
41  * contract as {@link LineSeriesProps}. Each contiguous run of at least two
42  * valid observations is closed to `baseline` in data space.
43  *
44  * @see {@link LineSeriesProps}
45  * @see {@link AreaSeriesStyle}
46  */
47 export interface AreaSeriesProps<D>
48   extends PibblEventHandlers, PibblEventParticipationOptions {
49   /** Application data supplied directly or through a signal. */
50   readonly data: LineSeriesProps<D>["data"];
51   /** Property key or callback extracting the horizontal coordinate. */
52   readonly x: LineAccessor<D>;
53   /** Property key or callback extracting the vertical coordinate. */
54   readonly y: LineAccessor<D>;
55   /** Continuous scale used to map horizontal data coordinates. */
56   readonly xScale: LineSeriesProps<D>["xScale"];
57   /** Continuous scale used to map vertical data coordinates and baseline. */
58   readonly yScale: LineSeriesProps<D>["yScale"];
59   /** Selects records that participate in an observed run. */
60   readonly defined?: LineSeriesProps<D>["defined"];
61   /** Curve used to connect successive observed samples. */
62   readonly curve?: LineSeriesProps<D>["curve"];
63   /** Finite data-space value to which each observed run closes. */
64   readonly baseline: SignalValue<number>;
65   /** Shared area presentation. Individual datum styling is not supported. */
66   readonly style?: SignalValue<SignalStyle<AreaSeriesStyle>>;
67 }
68 
69 function accessor<D>(
70   input: LineAccessor<D>,
71 ): (datum: D, index: number, data: readonly D[]) => LineCoordinate {
72   return typeof input === "function"
73     ? input
74     : (datum) => datum[input] as LineCoordinate;
75 }
76 
77 function fill(value: unknown): FillStyle {
78   if (
79     typeof value === "string" ||
80     value instanceof CanvasGradient ||
81     value instanceof CanvasPattern
82   )
83     return value;
84   try {
85     validateTexturePaint(value as never);
86     return value as FillStyle;
87   } catch {
88     throw new TypeError("AreaSeries: invalid fill.");
89   }
90 }
91 
92 /**
93  * Draws each contiguous observed run as a filled polygon closed to an explicit baseline.
94  *
95  * Missing, non-finite, and excluded coordinates split runs. A lone observation has no area.
96  * The component owns one ordinary {@link Path} target, so event participation follows the
97  * standard Path defaults; callers may wrap it in `Clip` when clipping is needed.
98  *
99  * @param props - Data, coordinate accessors, continuous scales, baseline, and presentation.
100  * @returns A Pibbl path node containing all drawable area runs.
101  * @throws When `data` is not an array, the baseline is not finite, or presentation is invalid.
102  *
103  * @see {@link AreaSeriesProps}
104  */
105 export function AreaSeries<D>({
106   data: dataInput,
107   x: xInput,
108   y: yInput,
109   xScale: xId,
110   yScale: yId,
111   baseline: baselineInput,
112   defined,
113   curve,
114   style: styleInput,
115   ...eventProps
116 }: AreaSeriesProps<D>) {
117   const xScale = useContinuousScale(xId).get();
118   const yScale = useContinuousScale(yId).get();
119   const curveDefinition = resolveSeriesCurve(curve);
120   const x = useInternalValueMemoSlot(
121     "vizAreaXAccessor",
122     () => accessor(xInput),
123     [xInput],
124   );
125   const y = useInternalValueMemoSlot(
126     "vizAreaYAccessor",
127     () => accessor(yInput),
128     [yInput],
129   );
130   const data = resolveSignalValue(dataInput);
131   if (!Array.isArray(data))
132     throw new TypeError(
133       "AreaSeries data must be an array or a signal containing an array.",
134     );
135   const baseline = resolveSignalValue(baselineInput);
136   if (typeof baseline !== "number" || !Number.isFinite(baseline))
137     throw new RangeError("AreaSeries baseline must be finite.");
138   const baselineY = yScale.map(baseline);
139   if (!Number.isFinite(baselineY))
140     throw new RangeError(
141       "AreaSeries baseline must map to a finite coordinate.",
142     );
143   const style = resolveSignalValue(styleInput) ?? {};
144   if (style === null || typeof style !== "object" || Array.isArray(style))
145     throw new TypeError("AreaSeries: style must be an object.");
146   const areaFill = fill(resolveSignalValue(style.fill) ?? "#2563eb");
147   const alpha = opacity(resolveSignalValue(style.opacity) ?? 0.2, "AreaSeries");
148   const cursor = resolveSignalValue(style.cursor);
149   if (cursor !== undefined && typeof cursor !== "string")
150     throw new TypeError("AreaSeries: cursor must be a string.");
151 
152   const path = new Path2D();
153   let run: (readonly [number, number])[] = [];
154   const closeRun = () => {
155     if (run.length >= 2) {
156       const first = run[0];
157       const last = run[run.length - 1];
158       const geometry = curveGeometry(curveDefinition, run);
159       path.moveTo(geometry.start[0], geometry.start[1]);
160       appendCurveForward(path, geometry);
161       path.lineTo(last[0], baselineY);
162       path.lineTo(first[0], baselineY);
163       path.closePath();
164     }
165     run = [];
166   };
167   for (let i = 0; i < data.length; i++) {
168     const datum = data[i];
169     if (defined && !defined(datum, i, data)) {
170       closeRun();
171       continue;
172     }
173     const xv = x(datum, i, data);
174     const yv = y(datum, i, data);
175     if (
176       typeof xv !== "number" ||
177       typeof yv !== "number" ||
178       !Number.isFinite(xv) ||
179       !Number.isFinite(yv)
180     ) {
181       closeRun();
182       continue;
183     }
184     const point = [xScale.map(xv), yScale.map(yv)] as const;
185     if (!Number.isFinite(point[0]) || !Number.isFinite(point[1])) {
186       closeRun();
187       continue;
188     }
189     run.push(point);
190   }
191   closeRun();
192   return (
193     <Path
194       {...eventProps}
195       style={{ d: path, fill: areaFill, opacity: alpha, cursor }}
196     />
197   );
198 }
199 

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