Skip to content

packages/core/src/features/viz/lib/point-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 { resolveTexturePaint } from "@pibbl/core/internal";
2 import {
3   Path,
4   isSignal,
5   resolveSignalValue,
6   useCanvasContext,
7 } from "@pibbl/core";
8 import type { FillStyle, StrokeStyle } from "@pibbl/core";
9 import type {
10   PointMarkStyle,
11   PointSeriesProps,
12   PointSeriesStyle,
13 } from "./point-types.js";
14 import {
15   useSeriesPosition,
16   seriesAccessor,
17   mapSeriesPosition,
18 } from "./series-position.js";
19 import { pointData } from "./point-data.js";
20 import { appendPointTarget, pointPath } from "./point-geometry.js";
21 import { opacity, width } from "./paint.js";
22 const fields = ["radius", "fill", "stroke", "strokeWidth", "opacity", "cursor"];
23 function paint(
24   value: unknown,
25   name: string,
26 ): FillStyle | StrokeStyle | undefined {
27   if (
28     value === undefined ||
29     typeof value === "string" ||
30     value instanceof CanvasGradient ||
31     value instanceof CanvasPattern
32   )
33     return value;
34   throw new TypeError(`PointSeries: invalid ${name}.`);
35 }
36 function styleObject(input: unknown, callback: boolean): PointSeriesStyle {
37   if (input === null || typeof input !== "object" || Array.isArray(input))
38     throw new TypeError("PointSeries: style must be an object.");
39   const result: Record<string, unknown> = {};
40   for (const key of Object.keys(input)) {
41     if (!fields.includes(key) || (callback && key === "cursor"))
42       throw new TypeError(`PointSeries: unsupported style field ${key}.`);
43     const value = (input as Record<string, unknown>)[key];
44     if (callback && isSignal(value))
45       throw new TypeError(
46         "PointSeries: datum styles must contain plain values; read signals explicitly.",
47       );
48     result[key] = callback ? value : resolveSignalValue(value);
49   }
50   return result as PointSeriesStyle;
51 }
52 function normalized(style: PointSeriesStyle) {
53   const radius = width(style.radius ?? 3, "PointSeries radius");
54   if (style.cursor !== undefined && typeof style.cursor !== "string")
55     throw new TypeError("PointSeries: cursor must be a string.");
56   return {
57     radius,
58     fill: paint(style.fill ?? "#2563eb", "fill"),
59     stroke: paint(style.stroke, "stroke"),
60     strokeWidth: width(style.strokeWidth ?? 1, "PointSeries"),
61     opacity: opacity(style.opacity ?? 1, "PointSeries"),
62     cursor: style.cursor,
63   };
64 }
65 /**
66  * Batched circles in source paint order with one ordinary Pibbl target.
67  *
68  * @param props - Data, coordinate accessors, scales, and point styling. See
69  * {@link PointSeriesProps} .
70  * @returns Pibbl nodes drawing the point marks.
71  *
72  * @see {@link PointSeriesProps}
73  */
74 export function PointSeries<D>({
75   data: input,
76   x: xInput,
77   y: yInput,
78   xScale: xId,
79   yScale: yId,
80   defined,
81   style: styleInput,
82   ...events
83 }: PointSeriesProps<D>) {
84   const xs = useSeriesPosition(xId),
85     ys = useSeriesPosition(yId);
86   const data = pointData(resolveSignalValue(input), "PointSeries");
87   const x = seriesAccessor(xInput),
88     y = seriesAccessor(yInput);
89   const specified = resolveSignalValue(styleInput);
90   const shared =
91     typeof specified === "function"
92       ? undefined
93       : normalized(styleObject(specified ?? {}, false));
94   const ctx = useCanvasContext();
95   const target = new Path2D();
96   for (let i = 0; i < data.length; i++) {
97     const datum = data[i];
98     if (defined && !defined(datum, i, data)) continue;
99     const xv = x(datum, i, data),
100       yv = y(datum, i, data);
101     const mappedX = mapSeriesPosition(xs, xv),
102       mappedY = mapSeriesPosition(ys, yv);
103     if (mappedX === undefined || mappedY === undefined) continue;
104     const px = mappedX,
105       py = mappedY;
106     const style =
107       shared ??
108       normalized(
109         styleObject(
110           (specified as (d: D, i: number, a: readonly D[]) => PointMarkStyle)(
111             datum,
112             i,
113             data,
114           ),
115           true,
116         ),
117       );
118     if (style.radius === 0) continue;
119     appendPointTarget(
120       target,
121       px,
122       py,
123       style.radius,
124       !!style.fill,
125       style.stroke ? style.strokeWidth : 0,
126     );
127     const path = pointPath(px, py, style.radius);
128     ctx.save();
129     try {
130       ctx.globalAlpha *= style.opacity;
131       if (style.fill) {
132         ctx.fillStyle = resolveTexturePaint(style.fill, ctx, {
133           x: px - style.radius,
134           y: py - style.radius,
135           width: style.radius * 2,
136           height: style.radius * 2,
137         });
138         ctx.fill(path);
139       }
140       if (style.stroke && style.strokeWidth > 0) {
141         ctx.strokeStyle = resolveTexturePaint(style.stroke, ctx, {
142           x: px - style.radius,
143           y: py - style.radius,
144           width: style.radius * 2,
145           height: style.radius * 2,
146         });
147         ctx.lineWidth = style.strokeWidth;
148         ctx.stroke(path);
149       }
150     } finally {
151       ctx.restore();
152     }
153   }
154   return (
155     <Path
156       {...events}
157       style={{ d: target, fill: "transparent", cursor: shared?.cursor }}
158     />
159   );
160 }
161 

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