packages/core/src/features/viz/lib/area-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 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 version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.