packages/core/src/features/viz/lib/point-series.tsx
This is the source snapshot used to build these API details. View this revision on GitHub.
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 version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.