@pharos-ds/charts 0.0.0-stage → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/LICENSE +24 -0
  3. package/README.md +117 -2
  4. package/dist/Chart.d.ts +62 -0
  5. package/dist/Chart.d.ts.map +1 -0
  6. package/dist/Chart.js +401 -0
  7. package/dist/ChartAxis.d.ts +37 -0
  8. package/dist/ChartAxis.d.ts.map +1 -0
  9. package/dist/ChartAxis.js +233 -0
  10. package/dist/ChartContext.d.ts +4 -0
  11. package/dist/ChartContext.d.ts.map +1 -0
  12. package/dist/ChartContext.js +15 -0
  13. package/dist/ChartGrid.d.ts +26 -0
  14. package/dist/ChartGrid.d.ts.map +1 -0
  15. package/dist/ChartGrid.js +118 -0
  16. package/dist/ChartLegend.d.ts +14 -0
  17. package/dist/ChartLegend.d.ts.map +1 -0
  18. package/dist/ChartLegend.js +62 -0
  19. package/dist/ChartSwatch.d.ts +36 -0
  20. package/dist/ChartSwatch.d.ts.map +1 -0
  21. package/dist/ChartSwatch.js +85 -0
  22. package/dist/ChartTooltip.d.ts +46 -0
  23. package/dist/ChartTooltip.d.ts.map +1 -0
  24. package/dist/ChartTooltip.js +330 -0
  25. package/dist/charts.css +33 -0
  26. package/dist/dataTable.d.ts +56 -0
  27. package/dist/dataTable.d.ts.map +1 -0
  28. package/dist/dataTable.js +88 -0
  29. package/dist/formatters.d.ts +37 -0
  30. package/dist/formatters.d.ts.map +1 -0
  31. package/dist/formatters.js +156 -0
  32. package/dist/getChartColors.d.ts +67 -0
  33. package/dist/getChartColors.d.ts.map +1 -0
  34. package/dist/getChartColors.js +191 -0
  35. package/dist/index.d.ts +17 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +19 -0
  38. package/dist/layout.d.ts +40 -0
  39. package/dist/layout.d.ts.map +1 -0
  40. package/dist/layout.js +322 -0
  41. package/dist/legend.d.ts +17 -0
  42. package/dist/legend.d.ts.map +1 -0
  43. package/dist/legend.js +40 -0
  44. package/dist/markColor.d.ts +20 -0
  45. package/dist/markColor.d.ts.map +1 -0
  46. package/dist/markColor.js +56 -0
  47. package/dist/marks/area.d.ts +19 -0
  48. package/dist/marks/area.d.ts.map +1 -0
  49. package/dist/marks/area.js +121 -0
  50. package/dist/marks/band.d.ts +9 -0
  51. package/dist/marks/band.d.ts.map +1 -0
  52. package/dist/marks/band.js +64 -0
  53. package/dist/marks/bar.d.ts +21 -0
  54. package/dist/marks/bar.d.ts.map +1 -0
  55. package/dist/marks/bar.js +161 -0
  56. package/dist/marks/candlestick.d.ts +16 -0
  57. package/dist/marks/candlestick.d.ts.map +1 -0
  58. package/dist/marks/candlestick.js +110 -0
  59. package/dist/marks/dot.d.ts +19 -0
  60. package/dist/marks/dot.d.ts.map +1 -0
  61. package/dist/marks/dot.js +98 -0
  62. package/dist/marks/dotGL.d.ts +16 -0
  63. package/dist/marks/dotGL.d.ts.map +1 -0
  64. package/dist/marks/dotGL.js +231 -0
  65. package/dist/marks/dotGLInteractive.d.ts +35 -0
  66. package/dist/marks/dotGLInteractive.d.ts.map +1 -0
  67. package/dist/marks/dotGLInteractive.js +584 -0
  68. package/dist/marks/errorBar.d.ts +29 -0
  69. package/dist/marks/errorBar.d.ts.map +1 -0
  70. package/dist/marks/errorBar.js +95 -0
  71. package/dist/marks/heatmapGL.d.ts +27 -0
  72. package/dist/marks/heatmapGL.d.ts.map +1 -0
  73. package/dist/marks/heatmapGL.js +335 -0
  74. package/dist/marks/index.d.ts +13 -0
  75. package/dist/marks/index.d.ts.map +1 -0
  76. package/dist/marks/index.js +14 -0
  77. package/dist/marks/line.d.ts +18 -0
  78. package/dist/marks/line.d.ts.map +1 -0
  79. package/dist/marks/line.js +83 -0
  80. package/dist/marks/referenceLine.d.ts +37 -0
  81. package/dist/marks/referenceLine.d.ts.map +1 -0
  82. package/dist/marks/referenceLine.js +204 -0
  83. package/dist/marks/streamGL.d.ts +41 -0
  84. package/dist/marks/streamGL.d.ts.map +1 -0
  85. package/dist/marks/streamGL.js +317 -0
  86. package/dist/tooltip.d.ts +38 -0
  87. package/dist/tooltip.d.ts.map +1 -0
  88. package/dist/tooltip.js +57 -0
  89. package/dist/types.d.ts +193 -0
  90. package/dist/types.d.ts.map +1 -0
  91. package/dist/types.js +70 -0
  92. package/dist/useChartColors.d.ts +16 -0
  93. package/dist/useChartColors.d.ts.map +1 -0
  94. package/dist/useChartColors.js +37 -0
  95. package/dist/utils.d.ts +15 -0
  96. package/dist/utils.d.ts.map +1 -0
  97. package/dist/utils.js +31 -0
  98. package/dist/webgl.d.ts +77 -0
  99. package/dist/webgl.d.ts.map +1 -0
  100. package/dist/webgl.js +235 -0
  101. package/package.json +71 -3
  102. package/src/Chart.doc.mjs +174 -0
  103. package/src/Chart.test.tsx +273 -0
  104. package/src/Chart.tsx +535 -0
  105. package/src/ChartAxis.tsx +289 -0
  106. package/src/ChartContext.ts +18 -0
  107. package/src/ChartGrid.tsx +136 -0
  108. package/src/ChartLegend.tsx +77 -0
  109. package/src/ChartSwatch.doc.mjs +17 -0
  110. package/src/ChartSwatch.tsx +89 -0
  111. package/src/ChartTooltip.tsx +452 -0
  112. package/src/charts.css.d.ts +3 -0
  113. package/src/dataTable.test.ts +86 -0
  114. package/src/dataTable.ts +124 -0
  115. package/src/formatters.ts +167 -0
  116. package/src/getChartColors.test.ts +114 -0
  117. package/src/getChartColors.ts +282 -0
  118. package/src/index.ts +88 -0
  119. package/src/layout.ts +372 -0
  120. package/src/legend.ts +44 -0
  121. package/src/markColor.ts +70 -0
  122. package/src/marks/area.tsx +142 -0
  123. package/src/marks/band.tsx +86 -0
  124. package/src/marks/bar.tsx +210 -0
  125. package/src/marks/candlestick.tsx +128 -0
  126. package/src/marks/dot.tsx +119 -0
  127. package/src/marks/dotGL.tsx +274 -0
  128. package/src/marks/dotGLInteractive.tsx +660 -0
  129. package/src/marks/errorBar.tsx +119 -0
  130. package/src/marks/heatmapGL.tsx +392 -0
  131. package/src/marks/index.ts +17 -0
  132. package/src/marks/line.tsx +118 -0
  133. package/src/marks/referenceLine.tsx +252 -0
  134. package/src/marks/streamGL.tsx +381 -0
  135. package/src/tooltip.ts +73 -0
  136. package/src/types.ts +212 -0
  137. package/src/useChartColors.ts +41 -0
  138. package/src/utils.ts +38 -0
  139. package/src/webgl.ts +262 -0
@@ -0,0 +1,119 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file marks/errorBar.tsx
5
+ * @output Error bar (whisker) series — vertical high/low bounds. Bounds are
6
+ * absolute (so asymmetric intervals work); a point missing either bound is
7
+ * skipped rather than drawn down to the axis.
8
+ */
9
+
10
+ import type {SeriesDef, ResolvedPoint} from '../types';
11
+ import {xPixel} from '../utils';
12
+
13
+ export interface ErrorBarOptions {
14
+ /** Data key for the upper bound */
15
+ high: string;
16
+ /** Data key for the lower bound */
17
+ low: string;
18
+ /** Stroke color */
19
+ color?: string;
20
+ /** Stroke width */
21
+ strokeWidth?: number;
22
+ /** Width of the whisker caps in pixels */
23
+ capWidth?: number;
24
+ }
25
+
26
+ /**
27
+ * Error bars with whisker caps. Pair with bar() or dot().
28
+ *
29
+ * @example
30
+ * ```
31
+ * series={[bar('mean'), errorBar({high: 'upper95', low: 'lower95'})]}
32
+ * ```
33
+ */
34
+ export function errorBar(options: ErrorBarOptions): SeriesDef {
35
+ const {high, low} = options;
36
+ const color = options.color ?? 'var(--color-text-primary)';
37
+ const strokeWidth = options.strokeWidth ?? 1.5;
38
+ const capWidth = options.capWidth ?? 8;
39
+
40
+ return {
41
+ type: 'errorBar',
42
+ key: `errorBar-${high}-${low}`,
43
+ dataKeys: [high, low],
44
+ layout: {},
45
+
46
+ resolve(ctx) {
47
+ const {data, xKey, xScale, yScale} = ctx;
48
+ const points: ResolvedPoint[] = [];
49
+ for (let i = 0; i < data.length; i++) {
50
+ const d = data[i];
51
+ // NaN for a missing bound so render() skips the whole whisker instead
52
+ // of anchoring a cap at the axis (a misleading zero-height error bar).
53
+ const hiVal = d[high];
54
+ const loVal = d[low];
55
+ const upper =
56
+ typeof hiVal === 'number' && Number.isFinite(hiVal) ? hiVal : NaN;
57
+ const lower =
58
+ typeof loVal === 'number' && Number.isFinite(loVal) ? loVal : NaN;
59
+ points.push({
60
+ px: xPixel(d, xKey, xScale),
61
+ py: yScale(upper),
62
+ py0: yScale(lower),
63
+ dataIndex: i,
64
+ });
65
+ }
66
+ return points;
67
+ },
68
+
69
+ render(resolved) {
70
+ // Non-finite capWidth (NaN/±Infinity) would leak into the cap x-coords;
71
+ // treat it like a non-positive width — draw the stem, drop the caps.
72
+ const half = Number.isFinite(capWidth) ? Math.max(0, capWidth) / 2 : 0;
73
+ return (
74
+ <g>
75
+ {resolved.map(p => {
76
+ if (
77
+ !Number.isFinite(p.px) ||
78
+ !Number.isFinite(p.py) ||
79
+ !Number.isFinite(p.py0)
80
+ ) {
81
+ return null;
82
+ }
83
+ return (
84
+ <g key={p.dataIndex}>
85
+ {/* Vertical stem */}
86
+ <line
87
+ x1={p.px}
88
+ x2={p.px}
89
+ y1={p.py}
90
+ y2={p.py0}
91
+ stroke={color}
92
+ strokeWidth={strokeWidth}
93
+ />
94
+ {/* Upper cap */}
95
+ <line
96
+ x1={p.px - half}
97
+ x2={p.px + half}
98
+ y1={p.py}
99
+ y2={p.py}
100
+ stroke={color}
101
+ strokeWidth={strokeWidth}
102
+ />
103
+ {/* Lower cap */}
104
+ <line
105
+ x1={p.px - half}
106
+ x2={p.px + half}
107
+ y1={p.py0}
108
+ y2={p.py0}
109
+ stroke={color}
110
+ strokeWidth={strokeWidth}
111
+ />
112
+ </g>
113
+ );
114
+ })}
115
+ </g>
116
+ );
117
+ },
118
+ };
119
+ }
@@ -0,0 +1,392 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file marks/heatmapGL.tsx
5
+ * @output WebGL heatmap — 2D grid of colored cells on canvas
6
+ *
7
+ * Cell geometry is memoized; GL context, program, and the position/color
8
+ * buffers are created once and reused across redraws, then released (context
9
+ * eagerly lost) on unmount. Handles context loss/restore and empty/NaN data.
10
+ */
11
+
12
+ import {useRef, useEffect, useMemo, useState} from 'react';
13
+ import type {SeriesDef, ResolvedPoint} from '../types';
14
+ import type {ScaleBand} from 'd3-scale';
15
+ import {
16
+ hexToGL,
17
+ getWebGLContext,
18
+ setupGLState,
19
+ sizeCanvas,
20
+ mountCanvasOverSVG,
21
+ createProgram,
22
+ registerContextLossHandlers,
23
+ loseContext,
24
+ } from '../webgl';
25
+
26
+ export interface HeatmapGLOptions {
27
+ xKey: string;
28
+ yKey: string;
29
+ valueKey: string;
30
+ colorRange: string[];
31
+ domain?: [number, number];
32
+ cellGap?: number;
33
+ }
34
+
35
+ const VERT = `
36
+ attribute vec2 a_position;
37
+ attribute vec3 a_color;
38
+ uniform vec2 u_resolution;
39
+ varying vec3 v_color;
40
+ void main() {
41
+ vec2 clip = (a_position / u_resolution) * 2.0 - 1.0;
42
+ gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
43
+ v_color = a_color;
44
+ }
45
+ `;
46
+
47
+ const FRAG = `
48
+ precision mediump float;
49
+ varying vec3 v_color;
50
+ void main() {
51
+ gl_FragColor = vec4(v_color, 1.0);
52
+ }
53
+ `;
54
+
55
+ function sampleRamp(
56
+ ramp: [number, number, number][],
57
+ t: number,
58
+ ): [number, number, number] {
59
+ if (ramp.length === 0) {
60
+ return [0, 0, 0];
61
+ }
62
+ const c = Math.max(0, Math.min(1, Number.isFinite(t) ? t : 0));
63
+ if (ramp.length === 1) {
64
+ return ramp[0];
65
+ }
66
+ const s = c * (ramp.length - 1);
67
+ const lo = Math.floor(s);
68
+ const hi = Math.min(lo + 1, ramp.length - 1);
69
+ const f = s - lo;
70
+ return [
71
+ ramp[lo][0] + f * (ramp[hi][0] - ramp[lo][0]),
72
+ ramp[lo][1] + f * (ramp[hi][1] - ramp[lo][1]),
73
+ ramp[lo][2] + f * (ramp[hi][2] - ramp[lo][2]),
74
+ ];
75
+ }
76
+
77
+ type HeatmapGLLocations = {
78
+ aPosition: number;
79
+ aColor: number;
80
+ uResolution: WebGLUniformLocation | null;
81
+ };
82
+
83
+ /**
84
+ * Inline component — manages the heatmap WebGL canvas. Rows use the chart's
85
+ * shared y band scale so cells stay aligned with the left axis' category labels.
86
+ */
87
+ function HeatmapGLCanvas({
88
+ data,
89
+ xKey,
90
+ yKey,
91
+ valueKey,
92
+ xScale,
93
+ yBandScale,
94
+ colorRange,
95
+ domain: domainProp,
96
+ cellGap,
97
+ width,
98
+ height,
99
+ }: {
100
+ data: Record<string, unknown>[];
101
+ xKey: string;
102
+ yKey: string;
103
+ valueKey: string;
104
+ xScale: ScaleBand<string>;
105
+ yBandScale: ScaleBand<string>;
106
+ colorRange: string[];
107
+ domain?: [number, number];
108
+ cellGap: number;
109
+ width: number;
110
+ height: number;
111
+ }) {
112
+ const canvasRef = useRef<HTMLCanvasElement | null>(null);
113
+ const glRef = useRef<WebGLRenderingContext | null>(null);
114
+ const programRef = useRef<WebGLProgram | null>(null);
115
+ const posBufferRef = useRef<WebGLBuffer | null>(null);
116
+ const colBufferRef = useRef<WebGLBuffer | null>(null);
117
+ const locsRef = useRef<HeatmapGLLocations | null>(null);
118
+ const markerRef = useRef<SVGGElement>(null);
119
+ // Bumped on context restore to force the draw effect to rebuild + repaint.
120
+ const [contextEpoch, setContextEpoch] = useState(0);
121
+
122
+ const ramp = useMemo(() => colorRange.map(hexToGL), [colorRange]);
123
+
124
+ const domain = useMemo((): [number, number] => {
125
+ if (domainProp) {
126
+ return domainProp;
127
+ }
128
+ let min = Infinity,
129
+ max = -Infinity;
130
+ for (const d of data) {
131
+ const v = d[valueKey];
132
+ if (typeof v === 'number' && Number.isFinite(v)) {
133
+ if (v < min) {
134
+ min = v;
135
+ }
136
+ if (v > max) {
137
+ max = v;
138
+ }
139
+ }
140
+ }
141
+ // No finite values → neutral [0, 1] so t/ramp math never yields NaN.
142
+ if (!Number.isFinite(min) || !Number.isFinite(max)) {
143
+ return [0, 1];
144
+ }
145
+ return [min, max];
146
+ }, [data, valueKey, domainProp]);
147
+
148
+ // Build interleaved triangle geometry (2 triangles/cell) + per-vertex colors.
149
+ // Memoized so redraws (resize, context restore) don't re-walk the dataset.
150
+ const geometry = useMemo(() => {
151
+ const positions: number[] = [];
152
+ const colors: number[] = [];
153
+ const [dMin, dMax] = domain;
154
+ const range = dMax - dMin || 1;
155
+ const gap = cellGap;
156
+ const xBW = xScale.bandwidth();
157
+ const yBW = yBandScale.bandwidth();
158
+ // Heatmap cells fill their full slot (step), not just the band width. The
159
+ // shared x-scale carries bar-style padding (~0.2), so sizing cells to the
160
+ // bandwidth leaves gaps between columns while the local y-scale stays tight —
161
+ // an asymmetric, sparse grid. Sizing to the step and centering on the band
162
+ // keeps cells contiguous (a real heatmap); cellGap alone controls spacing.
163
+ const xStep = typeof xScale.step === 'function' ? xScale.step() : xBW;
164
+ const yStep = yBandScale.step();
165
+
166
+ for (const d of data) {
167
+ const xVal = xScale(String(d[xKey]));
168
+ const yVal = yBandScale(String(d[yKey]));
169
+ if (xVal == null || yVal == null) {
170
+ continue;
171
+ }
172
+
173
+ const raw = d[valueKey];
174
+ const v = typeof raw === 'number' && Number.isFinite(raw) ? raw : 0;
175
+ const t = (v - dMin) / range;
176
+ const [r, g, b] = sampleRamp(ramp, t);
177
+
178
+ const xc = xVal + xBW / 2;
179
+ const yc = yVal + yBW / 2;
180
+ const x0 = xc - xStep / 2 + gap / 2;
181
+ const x1 = xc + xStep / 2 - gap / 2;
182
+ const y0 = yc - yStep / 2 + gap / 2;
183
+ const y1 = yc + yStep / 2 - gap / 2;
184
+
185
+ positions.push(x0, y0, x1, y0, x0, y1, x1, y0, x1, y1, x0, y1);
186
+ for (let i = 0; i < 6; i++) {
187
+ colors.push(r, g, b);
188
+ }
189
+ }
190
+
191
+ return {
192
+ positions: new Float32Array(positions),
193
+ colors: new Float32Array(colors),
194
+ vertexCount: positions.length / 2,
195
+ };
196
+ }, [data, xKey, yKey, valueKey, xScale, yBandScale, domain, ramp, cellGap]);
197
+
198
+ // Mount the canvas outside the SVG and wire context-loss recovery.
199
+ useEffect(() => {
200
+ const marker = markerRef.current;
201
+ if (!marker) {
202
+ return;
203
+ }
204
+ if (!canvasRef.current) {
205
+ canvasRef.current = document.createElement('canvas');
206
+ }
207
+ const canvas = canvasRef.current;
208
+ const detach = mountCanvasOverSVG(marker, canvas, width, height);
209
+ const unregister = registerContextLossHandlers(
210
+ canvas,
211
+ () => {
212
+ glRef.current = null;
213
+ programRef.current = null;
214
+ posBufferRef.current = null;
215
+ colBufferRef.current = null;
216
+ locsRef.current = null;
217
+ },
218
+ () => setContextEpoch(e => e + 1),
219
+ );
220
+ return () => {
221
+ unregister();
222
+ detach?.();
223
+ };
224
+ }, [width, height]);
225
+
226
+ // Draw. Context, program, and both buffers are created once and reused.
227
+ useEffect(() => {
228
+ const canvas = canvasRef.current;
229
+ if (!canvas || width <= 0 || height <= 0) {
230
+ return;
231
+ }
232
+
233
+ sizeCanvas(canvas, width, height);
234
+
235
+ if (!glRef.current) {
236
+ glRef.current = getWebGLContext(canvas);
237
+ }
238
+ const gl = glRef.current;
239
+ if (!gl || gl.isContextLost()) {
240
+ return;
241
+ }
242
+
243
+ if (!programRef.current) {
244
+ programRef.current = createProgram(gl, VERT, FRAG);
245
+ locsRef.current = null;
246
+ }
247
+ const program = programRef.current;
248
+ if (!program) {
249
+ return;
250
+ }
251
+
252
+ if (!posBufferRef.current) {
253
+ posBufferRef.current = gl.createBuffer();
254
+ }
255
+ if (!colBufferRef.current) {
256
+ colBufferRef.current = gl.createBuffer();
257
+ }
258
+ const posBuffer = posBufferRef.current;
259
+ const colBuffer = colBufferRef.current;
260
+ if (!posBuffer || !colBuffer) {
261
+ return;
262
+ }
263
+
264
+ if (!locsRef.current) {
265
+ locsRef.current = {
266
+ aPosition: gl.getAttribLocation(program, 'a_position'),
267
+ aColor: gl.getAttribLocation(program, 'a_color'),
268
+ uResolution: gl.getUniformLocation(program, 'u_resolution'),
269
+ };
270
+ }
271
+ const locs = locsRef.current;
272
+
273
+ gl.viewport(0, 0, canvas.width, canvas.height);
274
+ setupGLState(gl);
275
+ gl.useProgram(program);
276
+
277
+ gl.bindBuffer(gl.ARRAY_BUFFER, posBuffer);
278
+ gl.bufferData(gl.ARRAY_BUFFER, geometry.positions, gl.STATIC_DRAW);
279
+ gl.enableVertexAttribArray(locs.aPosition);
280
+ gl.vertexAttribPointer(locs.aPosition, 2, gl.FLOAT, false, 0, 0);
281
+
282
+ gl.bindBuffer(gl.ARRAY_BUFFER, colBuffer);
283
+ gl.bufferData(gl.ARRAY_BUFFER, geometry.colors, gl.STATIC_DRAW);
284
+ gl.enableVertexAttribArray(locs.aColor);
285
+ gl.vertexAttribPointer(locs.aColor, 3, gl.FLOAT, false, 0, 0);
286
+
287
+ gl.uniform2f(locs.uResolution, width, height);
288
+ gl.drawArrays(gl.TRIANGLES, 0, geometry.vertexCount);
289
+ }, [geometry, width, height, contextEpoch]);
290
+
291
+ // Release all GL resources on unmount.
292
+ useEffect(() => {
293
+ return () => {
294
+ const gl = glRef.current;
295
+ if (gl) {
296
+ if (posBufferRef.current) {
297
+ gl.deleteBuffer(posBufferRef.current);
298
+ }
299
+ if (colBufferRef.current) {
300
+ gl.deleteBuffer(colBufferRef.current);
301
+ }
302
+ if (programRef.current) {
303
+ gl.deleteProgram(programRef.current);
304
+ }
305
+ loseContext(gl);
306
+ }
307
+ glRef.current = null;
308
+ programRef.current = null;
309
+ posBufferRef.current = null;
310
+ colBufferRef.current = null;
311
+ locsRef.current = null;
312
+ };
313
+ }, []);
314
+
315
+ if (width <= 0 || height <= 0) {
316
+ return null;
317
+ }
318
+ return <g ref={markerRef} />;
319
+ }
320
+
321
+ /**
322
+ * WebGL heatmap. Renders a 2D grid of colored cells using triangles.
323
+ *
324
+ * The rows are categorical: declaring `layout.yBandKey` makes the chart build a
325
+ * shared y band scale over `yKey`, so a left `<ChartAxis>` renders the row
326
+ * labels aligned to each cell (instead of a meaningless linear value axis).
327
+ * Columns use the chart's x band scale.
328
+ *
329
+ * @example
330
+ * ```
331
+ * series={[heatmapGL({
332
+ * xKey: 'hour', yKey: 'day', valueKey: 'count',
333
+ * colorRange: ['#f0f9ff', '#1e40af'],
334
+ * })]}
335
+ * ```
336
+ */
337
+ export function heatmapGL(options: HeatmapGLOptions): SeriesDef {
338
+ const {xKey, yKey, valueKey, colorRange} = options;
339
+ const cellGap = options.cellGap ?? 1;
340
+
341
+ return {
342
+ type: 'heatmapGL',
343
+ key: `heatmap-${valueKey}`,
344
+ // The value drives cell COLOR, not a linear y-domain — no data keys keeps the
345
+ // heatmap out of the shared linear y-scale / left axis / value tooltip.
346
+ // `yBandKey` makes the chart build a categorical y band scale over the rows
347
+ // so the left axis shows the row labels aligned to each cell.
348
+ dataKeys: [],
349
+ layout: {yBandKey: yKey},
350
+
351
+ resolve(ctx) {
352
+ // The canvas handles its own cell layout; resolve only exposes row/column
353
+ // centers (for hover) using the shared band scales.
354
+ const {data, xScale, yBandScale} = ctx;
355
+ const points: ResolvedPoint[] = [];
356
+ if (!('bandwidth' in xScale) || !yBandScale) {
357
+ return points;
358
+ }
359
+ const xs = xScale as ScaleBand<string>;
360
+ for (let i = 0; i < data.length; i++) {
361
+ const d = data[i];
362
+ const px = (xs(String(d[xKey])) ?? 0) + xs.bandwidth() / 2;
363
+ const py =
364
+ (yBandScale(String(d[yKey])) ?? 0) + yBandScale.bandwidth() / 2;
365
+ points.push({px, py, py0: py, dataIndex: i});
366
+ }
367
+ return points;
368
+ },
369
+
370
+ render(_resolved, ctx) {
371
+ // Needs a categorical x (columns) and the shared y band scale (rows).
372
+ if (!('bandwidth' in ctx.xScale) || !ctx.yBandScale) {
373
+ return null;
374
+ }
375
+ return (
376
+ <HeatmapGLCanvas
377
+ data={ctx.data}
378
+ xKey={xKey}
379
+ yKey={yKey}
380
+ valueKey={valueKey}
381
+ xScale={ctx.xScale as ScaleBand<string>}
382
+ yBandScale={ctx.yBandScale}
383
+ colorRange={colorRange}
384
+ domain={options.domain}
385
+ cellGap={cellGap}
386
+ width={ctx.width}
387
+ height={ctx.height}
388
+ />
389
+ );
390
+ },
391
+ };
392
+ }
@@ -0,0 +1,17 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ export {bar, type BarOptions} from './bar';
4
+ export {line, type LineOptions} from './line';
5
+ export {dot, type DotOptions} from './dot';
6
+ export {area, type AreaOptions} from './area';
7
+ export {band, type BandOptions} from './band';
8
+ export {candlestick, type CandlestickOptions} from './candlestick';
9
+ export {errorBar, type ErrorBarOptions} from './errorBar';
10
+ export {referenceLine, type ReferenceLineOptions} from './referenceLine';
11
+ export {dotGL, type DotGLOptions} from './dotGL';
12
+ export {
13
+ dotGLInteractive,
14
+ type DotGLInteractiveOptions,
15
+ } from './dotGLInteractive';
16
+ export {heatmapGL, type HeatmapGLOptions} from './heatmapGL';
17
+ export {streamGL, type StreamGLOptions, type StreamGLHandle} from './streamGL';
@@ -0,0 +1,118 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file marks/line.tsx
5
+ * @output Line series — self-contained resolve + render
6
+ *
7
+ * Missing / non-finite values become gaps (via d3's `.defined()`) instead of a
8
+ * misleading drop to the baseline, and per-point dots are only drawn where the
9
+ * point is finite.
10
+ */
11
+
12
+ import {
13
+ line as d3Line,
14
+ curveLinear,
15
+ curveMonotoneX,
16
+ curveNatural,
17
+ curveStep,
18
+ } from 'd3-shape';
19
+ import type {SeriesDef, ResolvedPoint} from '../types';
20
+ import {seriesFill} from '../markColor';
21
+ import {xPixel} from '../utils';
22
+
23
+ const CURVES = {
24
+ linear: curveLinear,
25
+ monotone: curveMonotoneX,
26
+ natural: curveNatural,
27
+ step: curveStep,
28
+ } as const;
29
+ export type CurveType = keyof typeof CURVES;
30
+
31
+ export interface LineOptions {
32
+ color?: string;
33
+ curve?: CurveType;
34
+ strokeWidth?: number;
35
+ dots?: boolean;
36
+ label?: string;
37
+ }
38
+
39
+ /** A point is drawable only when both of its plotted coordinates are finite. */
40
+ function isFinitePoint(p: ResolvedPoint): boolean {
41
+ return Number.isFinite(p.px) && Number.isFinite(p.py);
42
+ }
43
+
44
+ export function line(dataKey: string, options: LineOptions = {}): SeriesDef {
45
+ const color = options.color;
46
+ const curve = options.curve ?? 'monotone';
47
+ const strokeWidth =
48
+ typeof options.strokeWidth === 'number' &&
49
+ Number.isFinite(options.strokeWidth)
50
+ ? Math.max(0, options.strokeWidth)
51
+ : 2;
52
+ const dots = options.dots ?? false;
53
+
54
+ const seriesDef: SeriesDef = {
55
+ type: 'line',
56
+ key: dataKey,
57
+ dataKeys: [dataKey],
58
+ color,
59
+ label: options.label ?? dataKey,
60
+ layout: {},
61
+
62
+ resolve(ctx) {
63
+ const {data, xKey, xScale, yScale} = ctx;
64
+ const points: ResolvedPoint[] = [];
65
+ for (let i = 0; i < data.length; i++) {
66
+ const d = data[i];
67
+ const raw = d[dataKey];
68
+ const v = typeof raw === 'number' && Number.isFinite(raw) ? raw : NaN;
69
+ points.push({
70
+ px: xPixel(d, xKey, xScale),
71
+ py: yScale(v),
72
+ py0: yScale(0),
73
+ dataIndex: i,
74
+ });
75
+ }
76
+ return points;
77
+ },
78
+
79
+ render(resolved) {
80
+ if (resolved.length === 0) {
81
+ return null;
82
+ }
83
+ const curveFactory = CURVES[curve] ?? curveMonotoneX;
84
+ const pathGen = d3Line<ResolvedPoint>()
85
+ .defined(isFinitePoint)
86
+ .x(d => d.px)
87
+ .y(d => d.py)
88
+ .curve(curveFactory);
89
+
90
+ const pathD = pathGen(resolved) ?? '';
91
+ const strokeColor = seriesFill(seriesDef, color);
92
+ return (
93
+ <g>
94
+ <path
95
+ d={pathD}
96
+ fill="none"
97
+ stroke={strokeColor}
98
+ strokeWidth={strokeWidth}
99
+ />
100
+ {dots &&
101
+ resolved
102
+ .filter(isFinitePoint)
103
+ .map(p => (
104
+ <circle
105
+ key={p.dataIndex}
106
+ cx={p.px}
107
+ cy={p.py}
108
+ r={3}
109
+ fill={strokeColor}
110
+ />
111
+ ))}
112
+ </g>
113
+ );
114
+ },
115
+ };
116
+
117
+ return seriesDef;
118
+ }