@astryxdesign/lab 0.1.4-canary.37c19d3 → 0.1.4-canary.39ffd21

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 (142) hide show
  1. package/dist/ChartV2/Chart.d.ts +32 -0
  2. package/dist/ChartV2/Chart.d.ts.map +1 -0
  3. package/dist/ChartV2/Chart.js +281 -0
  4. package/dist/ChartV2/ChartAxis.d.ts +36 -0
  5. package/dist/ChartV2/ChartAxis.d.ts.map +1 -0
  6. package/dist/ChartV2/ChartAxis.js +161 -0
  7. package/dist/ChartV2/ChartGrid.d.ts +20 -0
  8. package/dist/ChartV2/ChartGrid.d.ts.map +1 -0
  9. package/dist/ChartV2/ChartGrid.js +76 -0
  10. package/dist/ChartV2/ChartLegend.d.ts +14 -0
  11. package/dist/ChartV2/ChartLegend.d.ts.map +1 -0
  12. package/dist/ChartV2/ChartLegend.js +54 -0
  13. package/dist/ChartV2/ChartSwatch.d.ts +36 -0
  14. package/dist/ChartV2/ChartSwatch.d.ts.map +1 -0
  15. package/dist/ChartV2/ChartSwatch.js +82 -0
  16. package/dist/ChartV2/ChartTooltip.d.ts +46 -0
  17. package/dist/ChartV2/ChartTooltip.d.ts.map +1 -0
  18. package/dist/ChartV2/ChartTooltip.js +267 -0
  19. package/dist/ChartV2/ChartV2Context.d.ts +4 -0
  20. package/dist/ChartV2/ChartV2Context.d.ts.map +1 -0
  21. package/dist/ChartV2/ChartV2Context.js +12 -0
  22. package/dist/ChartV2/index.d.ts +13 -0
  23. package/dist/ChartV2/index.d.ts.map +1 -0
  24. package/dist/ChartV2/index.js +10 -0
  25. package/dist/ChartV2/layout.d.ts +25 -0
  26. package/dist/ChartV2/layout.d.ts.map +1 -0
  27. package/dist/ChartV2/layout.js +195 -0
  28. package/dist/ChartV2/legend.d.ts +18 -0
  29. package/dist/ChartV2/legend.d.ts.map +1 -0
  30. package/dist/ChartV2/legend.js +24 -0
  31. package/dist/ChartV2/marks/area.d.ts +19 -0
  32. package/dist/ChartV2/marks/area.d.ts.map +1 -0
  33. package/dist/ChartV2/marks/area.js +109 -0
  34. package/dist/ChartV2/marks/band.d.ts +9 -0
  35. package/dist/ChartV2/marks/band.d.ts.map +1 -0
  36. package/dist/ChartV2/marks/band.js +59 -0
  37. package/dist/ChartV2/marks/bar.d.ts +17 -0
  38. package/dist/ChartV2/marks/bar.d.ts.map +1 -0
  39. package/dist/ChartV2/marks/bar.js +120 -0
  40. package/dist/ChartV2/marks/candlestick.d.ts +15 -0
  41. package/dist/ChartV2/marks/candlestick.d.ts.map +1 -0
  42. package/dist/ChartV2/marks/candlestick.js +83 -0
  43. package/dist/ChartV2/marks/dot.d.ts +15 -0
  44. package/dist/ChartV2/marks/dot.d.ts.map +1 -0
  45. package/dist/ChartV2/marks/dot.js +65 -0
  46. package/dist/ChartV2/marks/dotGL.d.ts +16 -0
  47. package/dist/ChartV2/marks/dotGL.d.ts.map +1 -0
  48. package/dist/ChartV2/marks/dotGL.js +178 -0
  49. package/dist/ChartV2/marks/dotGLInteractive.d.ts +30 -0
  50. package/dist/ChartV2/marks/dotGLInteractive.d.ts.map +1 -0
  51. package/dist/ChartV2/marks/dotGLInteractive.js +464 -0
  52. package/dist/ChartV2/marks/errorBar.d.ts +27 -0
  53. package/dist/ChartV2/marks/errorBar.d.ts.map +1 -0
  54. package/dist/ChartV2/marks/errorBar.js +87 -0
  55. package/dist/ChartV2/marks/heatmapGL.d.ts +23 -0
  56. package/dist/ChartV2/marks/heatmapGL.d.ts.map +1 -0
  57. package/dist/ChartV2/marks/heatmapGL.js +242 -0
  58. package/dist/ChartV2/marks/index.d.ts +13 -0
  59. package/dist/ChartV2/marks/index.d.ts.map +1 -0
  60. package/dist/ChartV2/marks/index.js +14 -0
  61. package/dist/ChartV2/marks/line.d.ts +18 -0
  62. package/dist/ChartV2/marks/line.d.ts.map +1 -0
  63. package/dist/ChartV2/marks/line.js +76 -0
  64. package/dist/ChartV2/marks/referenceLine.d.ts +35 -0
  65. package/dist/ChartV2/marks/referenceLine.d.ts.map +1 -0
  66. package/dist/ChartV2/marks/referenceLine.js +186 -0
  67. package/dist/ChartV2/marks/streamGL.d.ts +36 -0
  68. package/dist/ChartV2/marks/streamGL.d.ts.map +1 -0
  69. package/dist/ChartV2/marks/streamGL.js +204 -0
  70. package/dist/ChartV2/tooltip.d.ts +38 -0
  71. package/dist/ChartV2/tooltip.d.ts.map +1 -0
  72. package/dist/ChartV2/tooltip.js +54 -0
  73. package/dist/ChartV2/types.d.ts +115 -0
  74. package/dist/ChartV2/types.d.ts.map +1 -0
  75. package/dist/ChartV2/types.js +35 -0
  76. package/dist/CodeEditor/CodeEditor.d.ts +1 -4
  77. package/dist/CodeEditor/CodeEditor.d.ts.map +1 -1
  78. package/dist/CodeEditor/CodeEditor.js +0 -3
  79. package/dist/Sankey/SankeyChart.d.ts.map +1 -1
  80. package/dist/Sankey/SankeyChart.js +0 -3
  81. package/dist/Schedule/ListView.d.ts.map +1 -1
  82. package/dist/Schedule/ListView.js +4 -7
  83. package/dist/Schedule/MonthlyView.js +2 -3
  84. package/dist/Schedule/Schedule.d.ts +1 -3
  85. package/dist/Schedule/Schedule.d.ts.map +1 -1
  86. package/dist/Schedule/Schedule.js +4 -9
  87. package/dist/Schedule/TimeGridView.js +2 -3
  88. package/dist/Schedule/context.d.ts +0 -2
  89. package/dist/Schedule/context.d.ts.map +1 -1
  90. package/dist/index.d.ts +1 -1
  91. package/dist/index.d.ts.map +1 -1
  92. package/dist/index.js +2 -5
  93. package/dist/lab.css +15 -6
  94. package/package.json +2 -2
  95. package/src/ChartV2/Chart.tsx +348 -0
  96. package/src/ChartV2/ChartAxis.tsx +209 -0
  97. package/src/ChartV2/ChartGrid.tsx +99 -0
  98. package/src/ChartV2/ChartLegend.tsx +69 -0
  99. package/src/ChartV2/ChartSwatch.doc.mjs +17 -0
  100. package/src/ChartV2/ChartSwatch.tsx +86 -0
  101. package/src/ChartV2/ChartTooltip.tsx +382 -0
  102. package/src/ChartV2/ChartV2.doc.mjs +16 -0
  103. package/src/ChartV2/ChartV2Context.ts +15 -0
  104. package/src/ChartV2/index.ts +62 -0
  105. package/src/ChartV2/layout.ts +223 -0
  106. package/src/ChartV2/legend.ts +32 -0
  107. package/src/ChartV2/marks/area.tsx +122 -0
  108. package/src/ChartV2/marks/band.tsx +70 -0
  109. package/src/ChartV2/marks/bar.tsx +159 -0
  110. package/src/ChartV2/marks/candlestick.tsx +106 -0
  111. package/src/ChartV2/marks/dot.tsx +64 -0
  112. package/src/ChartV2/marks/dotGL.tsx +202 -0
  113. package/src/ChartV2/marks/dotGLInteractive.tsx +546 -0
  114. package/src/ChartV2/marks/errorBar.tsx +89 -0
  115. package/src/ChartV2/marks/heatmapGL.tsx +299 -0
  116. package/src/ChartV2/marks/index.ts +14 -0
  117. package/src/ChartV2/marks/line.tsx +94 -0
  118. package/src/ChartV2/marks/referenceLine.tsx +224 -0
  119. package/src/ChartV2/marks/streamGL.tsx +251 -0
  120. package/src/ChartV2/tooltip.ts +70 -0
  121. package/src/ChartV2/types.ts +129 -0
  122. package/src/CodeEditor/CodeEditor.tsx +0 -5
  123. package/src/Sankey/SankeyChart.tsx +0 -3
  124. package/src/Schedule/ListView.tsx +2 -5
  125. package/src/Schedule/MonthlyView.tsx +2 -2
  126. package/src/Schedule/Schedule.test.tsx +0 -39
  127. package/src/Schedule/Schedule.tsx +0 -8
  128. package/src/Schedule/TimeGridView.tsx +2 -2
  129. package/src/Schedule/context.tsx +0 -2
  130. package/src/index.ts +46 -5
  131. package/dist/InfoTip/InfoTip.d.ts +0 -58
  132. package/dist/InfoTip/InfoTip.d.ts.map +0 -1
  133. package/dist/InfoTip/InfoTip.js +0 -91
  134. package/dist/InfoTip/index.d.ts +0 -6
  135. package/dist/InfoTip/index.d.ts.map +0 -1
  136. package/dist/InfoTip/index.js +0 -8
  137. package/src/CodeEditor/CodeEditor.test.tsx +0 -21
  138. package/src/InfoTip/InfoTip.doc.mjs +0 -97
  139. package/src/InfoTip/InfoTip.test.tsx +0 -169
  140. package/src/InfoTip/InfoTip.tsx +0 -155
  141. package/src/InfoTip/index.ts +0 -10
  142. package/src/Sankey/SankeyChart.test.tsx +0 -77
@@ -0,0 +1,546 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file marks/dotGLInteractive.tsx
5
+ * @output WebGL scatter with GPU color-picking for O(1) nearest-point hover
6
+ *
7
+ * Each point gets a unique color encoding its index. On hover, readPixels
8
+ * from an offscreen framebuffer decodes the hovered index — no JS iteration.
9
+ */
10
+
11
+ import {
12
+ useRef,
13
+ useEffect,
14
+ useCallback,
15
+ useState,
16
+ useMemo,
17
+ type ReactNode,
18
+ } from 'react';
19
+ import type {SeriesDef, ResolvedPoint} from '../types';
20
+ import type {ScaleBand} from 'd3-scale';
21
+ import {hexToGL} from '../../Chart/webgl';
22
+
23
+ export interface DotGLInteractiveOptions {
24
+ color: string;
25
+ size?: number;
26
+ opacity?: number;
27
+ /** Custom tooltip renderer. Receives datum and index. */
28
+ renderTooltip?: (datum: Record<string, unknown>, index: number) => ReactNode;
29
+ }
30
+
31
+ /** Encode a point index as an RGB color (supports up to 16.7M points) */
32
+ function indexToColor(i: number): [number, number, number] {
33
+ const id = i + 1;
34
+ return [
35
+ ((id >> 16) & 0xff) / 255,
36
+ ((id >> 8) & 0xff) / 255,
37
+ (id & 0xff) / 255,
38
+ ];
39
+ }
40
+
41
+ /** Decode an RGB pixel back to a point index. Returns -1 for background. */
42
+ function colorToIndex(r: number, g: number, b: number): number {
43
+ if (r === 0 && g === 0 && b === 0) {
44
+ return -1;
45
+ }
46
+ return ((r << 16) | (g << 8) | b) - 1;
47
+ }
48
+
49
+ function compileShader(
50
+ gl: WebGLRenderingContext,
51
+ type: number,
52
+ src: string,
53
+ ): WebGLShader | null {
54
+ const s = gl.createShader(type);
55
+ if (!s) {
56
+ return null;
57
+ }
58
+ gl.shaderSource(s, src);
59
+ gl.compileShader(s);
60
+ if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
61
+ gl.deleteShader(s);
62
+ return null;
63
+ }
64
+ return s;
65
+ }
66
+
67
+ function linkProgram(
68
+ gl: WebGLRenderingContext,
69
+ vert: string,
70
+ frag: string,
71
+ ): WebGLProgram | null {
72
+ const vs = compileShader(gl, gl.VERTEX_SHADER, vert);
73
+ const fs = compileShader(gl, gl.FRAGMENT_SHADER, frag);
74
+ if (!vs || !fs) {
75
+ return null;
76
+ }
77
+ const p = gl.createProgram();
78
+ if (!p) {
79
+ return null;
80
+ }
81
+ gl.attachShader(p, vs);
82
+ gl.attachShader(p, fs);
83
+ gl.linkProgram(p);
84
+ if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {
85
+ gl.deleteProgram(p);
86
+ return null;
87
+ }
88
+ return p;
89
+ }
90
+
91
+ // Visible pass
92
+ const VERT_VIS = `
93
+ attribute vec2 a_position;
94
+ uniform vec2 u_resolution;
95
+ uniform float u_size;
96
+ void main() {
97
+ vec2 clip = (a_position / u_resolution) * 2.0 - 1.0;
98
+ gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
99
+ gl_PointSize = u_size;
100
+ }
101
+ `;
102
+
103
+ const FRAG_VIS = `
104
+ precision mediump float;
105
+ uniform vec3 u_color;
106
+ uniform float u_opacity;
107
+ void main() {
108
+ vec2 coord = gl_PointCoord - vec2(0.5);
109
+ if (dot(coord, coord) > 0.25) discard;
110
+ gl_FragColor = vec4(u_color, u_opacity);
111
+ }
112
+ `;
113
+
114
+ // Pick pass — encode index as color
115
+ const VERT_PICK = `
116
+ attribute vec2 a_position;
117
+ attribute vec3 a_pickColor;
118
+ uniform vec2 u_resolution;
119
+ uniform float u_size;
120
+ varying vec3 v_pickColor;
121
+ void main() {
122
+ vec2 clip = (a_position / u_resolution) * 2.0 - 1.0;
123
+ gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
124
+ gl_PointSize = u_size + 4.0;
125
+ v_pickColor = a_pickColor;
126
+ }
127
+ `;
128
+
129
+ const FRAG_PICK = `
130
+ precision mediump float;
131
+ varying vec3 v_pickColor;
132
+ void main() {
133
+ vec2 coord = gl_PointCoord - vec2(0.5);
134
+ if (dot(coord, coord) > 0.25) discard;
135
+ gl_FragColor = vec4(v_pickColor, 1.0);
136
+ }
137
+ `;
138
+
139
+ /**
140
+ * Inline component — manages visible + pick canvases and hover state.
141
+ */
142
+ function DotGLInteractiveCanvas({
143
+ resolved,
144
+ color,
145
+ size,
146
+ opacity,
147
+ width,
148
+ height,
149
+ data,
150
+ renderTooltip,
151
+ }: {
152
+ resolved: ResolvedPoint[];
153
+ color: string;
154
+ size: number;
155
+ opacity: number;
156
+ width: number;
157
+ height: number;
158
+ data: Record<string, unknown>[];
159
+ renderTooltip?: (datum: Record<string, unknown>, index: number) => ReactNode;
160
+ }) {
161
+ const visCanvasRef = useRef<HTMLCanvasElement>(null);
162
+ const pickCanvasRef = useRef<HTMLCanvasElement>(null);
163
+ const [hoverIndex, setHoverIndex] = useState<number>(-1);
164
+ const [mousePos, setMousePos] = useState<{x: number; y: number} | null>(null);
165
+
166
+ const visGLRef = useRef<{
167
+ gl: WebGLRenderingContext;
168
+ prog: WebGLProgram;
169
+ } | null>(null);
170
+ const pickGLRef = useRef<{
171
+ gl: WebGLRenderingContext;
172
+ prog: WebGLProgram;
173
+ fb: WebGLFramebuffer;
174
+ tex: WebGLTexture;
175
+ } | null>(null);
176
+
177
+ const positions = useMemo(() => {
178
+ const pos: number[] = [];
179
+ for (const p of resolved) {
180
+ pos.push(p.px, p.py);
181
+ }
182
+ return new Float32Array(pos);
183
+ }, [resolved]);
184
+
185
+ const pickColors = useMemo(() => {
186
+ const colors: number[] = [];
187
+ for (let i = 0; i < resolved.length; i++) {
188
+ const [r, g, b] = indexToColor(i);
189
+ colors.push(r, g, b);
190
+ }
191
+ return new Float32Array(colors);
192
+ }, [resolved.length]);
193
+
194
+ // Render both passes
195
+ useEffect(() => {
196
+ if (width <= 0 || height <= 0) {
197
+ return;
198
+ }
199
+ const dpr = window.devicePixelRatio || 1;
200
+
201
+ // Visible canvas
202
+ const visCanvas = visCanvasRef.current;
203
+ if (visCanvas) {
204
+ visCanvas.width = width * dpr;
205
+ visCanvas.height = height * dpr;
206
+
207
+ if (!visGLRef.current) {
208
+ const gl = visCanvas.getContext('webgl', {
209
+ alpha: true,
210
+ premultipliedAlpha: false,
211
+ antialias: true,
212
+ });
213
+ const prog = gl ? linkProgram(gl, VERT_VIS, FRAG_VIS) : null;
214
+ if (gl && prog) {
215
+ visGLRef.current = {gl, prog};
216
+ }
217
+ }
218
+
219
+ const vis = visGLRef.current;
220
+ if (vis) {
221
+ const {gl, prog} = vis;
222
+ gl.viewport(0, 0, visCanvas.width, visCanvas.height);
223
+ gl.clearColor(0, 0, 0, 0);
224
+ gl.clear(gl.COLOR_BUFFER_BIT);
225
+ gl.enable(gl.BLEND);
226
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
227
+ gl.useProgram(prog);
228
+
229
+ const buf = gl.createBuffer();
230
+ gl.bindBuffer(gl.ARRAY_BUFFER, buf);
231
+ gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
232
+ const aPos = gl.getAttribLocation(prog, 'a_position');
233
+ gl.enableVertexAttribArray(aPos);
234
+ gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
235
+
236
+ const [r, g, b] = hexToGL(color);
237
+ gl.uniform2f(
238
+ gl.getUniformLocation(prog, 'u_resolution'),
239
+ width,
240
+ height,
241
+ );
242
+ gl.uniform3f(gl.getUniformLocation(prog, 'u_color'), r, g, b);
243
+ gl.uniform1f(gl.getUniformLocation(prog, 'u_size'), size * dpr);
244
+ gl.uniform1f(gl.getUniformLocation(prog, 'u_opacity'), opacity);
245
+
246
+ gl.drawArrays(gl.POINTS, 0, positions.length / 2);
247
+ gl.deleteBuffer(buf);
248
+ }
249
+ }
250
+
251
+ // Pick canvas (offscreen)
252
+ const pickCanvas = pickCanvasRef.current;
253
+ if (pickCanvas) {
254
+ pickCanvas.width = width * dpr;
255
+ pickCanvas.height = height * dpr;
256
+
257
+ if (!pickGLRef.current) {
258
+ const gl = pickCanvas.getContext('webgl', {
259
+ alpha: false,
260
+ premultipliedAlpha: false,
261
+ antialias: false,
262
+ preserveDrawingBuffer: true,
263
+ });
264
+ if (gl) {
265
+ const prog = linkProgram(gl, VERT_PICK, FRAG_PICK);
266
+ const fb = gl.createFramebuffer();
267
+ const tex = gl.createTexture();
268
+ if (prog && fb && tex) {
269
+ gl.bindTexture(gl.TEXTURE_2D, tex);
270
+ gl.texImage2D(
271
+ gl.TEXTURE_2D,
272
+ 0,
273
+ gl.RGBA,
274
+ pickCanvas.width,
275
+ pickCanvas.height,
276
+ 0,
277
+ gl.RGBA,
278
+ gl.UNSIGNED_BYTE,
279
+ null,
280
+ );
281
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
282
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
283
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
284
+ gl.framebufferTexture2D(
285
+ gl.FRAMEBUFFER,
286
+ gl.COLOR_ATTACHMENT0,
287
+ gl.TEXTURE_2D,
288
+ tex,
289
+ 0,
290
+ );
291
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
292
+ pickGLRef.current = {gl, prog, fb, tex};
293
+ }
294
+ }
295
+ }
296
+
297
+ const pick = pickGLRef.current;
298
+ if (pick) {
299
+ const {gl, prog, fb, tex} = pick;
300
+
301
+ gl.bindTexture(gl.TEXTURE_2D, tex);
302
+ gl.texImage2D(
303
+ gl.TEXTURE_2D,
304
+ 0,
305
+ gl.RGBA,
306
+ pickCanvas.width,
307
+ pickCanvas.height,
308
+ 0,
309
+ gl.RGBA,
310
+ gl.UNSIGNED_BYTE,
311
+ null,
312
+ );
313
+
314
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
315
+ gl.viewport(0, 0, pickCanvas.width, pickCanvas.height);
316
+ gl.clearColor(0, 0, 0, 1);
317
+ gl.clear(gl.COLOR_BUFFER_BIT);
318
+ gl.disable(gl.BLEND);
319
+ gl.useProgram(prog);
320
+
321
+ const posBuf = gl.createBuffer();
322
+ gl.bindBuffer(gl.ARRAY_BUFFER, posBuf);
323
+ gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
324
+ const aPos = gl.getAttribLocation(prog, 'a_position');
325
+ gl.enableVertexAttribArray(aPos);
326
+ gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
327
+
328
+ const colBuf = gl.createBuffer();
329
+ gl.bindBuffer(gl.ARRAY_BUFFER, colBuf);
330
+ gl.bufferData(gl.ARRAY_BUFFER, pickColors, gl.STATIC_DRAW);
331
+ const aCol = gl.getAttribLocation(prog, 'a_pickColor');
332
+ gl.enableVertexAttribArray(aCol);
333
+ gl.vertexAttribPointer(aCol, 3, gl.FLOAT, false, 0, 0);
334
+
335
+ gl.uniform2f(
336
+ gl.getUniformLocation(prog, 'u_resolution'),
337
+ width,
338
+ height,
339
+ );
340
+ gl.uniform1f(gl.getUniformLocation(prog, 'u_size'), (size + 4) * dpr);
341
+
342
+ gl.drawArrays(gl.POINTS, 0, positions.length / 2);
343
+ gl.deleteBuffer(posBuf);
344
+ gl.deleteBuffer(colBuf);
345
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
346
+ }
347
+ }
348
+ }, [width, height, positions, pickColors, color, size, opacity]);
349
+
350
+ const handleMouseMove = useCallback((e: React.MouseEvent<SVGRectElement>) => {
351
+ const pick = pickGLRef.current;
352
+ if (!pick) {
353
+ return;
354
+ }
355
+
356
+ const svg = e.currentTarget.ownerSVGElement;
357
+ if (!svg) {
358
+ return;
359
+ }
360
+ const pt = svg.createSVGPoint();
361
+ pt.x = e.clientX;
362
+ pt.y = e.clientY;
363
+ const local = pt.matrixTransform(e.currentTarget.getScreenCTM()?.inverse());
364
+
365
+ const dpr = window.devicePixelRatio || 1;
366
+ const px = Math.floor(local.x * dpr);
367
+ const py = Math.floor(local.y * dpr);
368
+
369
+ const {gl, fb} = pick;
370
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
371
+ const pixel = new Uint8Array(4);
372
+ gl.readPixels(
373
+ px,
374
+ gl.canvas.height - py,
375
+ 1,
376
+ 1,
377
+ gl.RGBA,
378
+ gl.UNSIGNED_BYTE,
379
+ pixel,
380
+ );
381
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
382
+
383
+ const idx = colorToIndex(pixel[0], pixel[1], pixel[2]);
384
+ setHoverIndex(idx);
385
+ setMousePos(idx >= 0 ? {x: local.x, y: local.y} : null);
386
+ }, []);
387
+
388
+ const handleMouseLeave = useCallback(() => {
389
+ setHoverIndex(-1);
390
+ setMousePos(null);
391
+ }, []);
392
+
393
+ const datum =
394
+ hoverIndex >= 0 && hoverIndex < data.length ? data[hoverIndex] : null;
395
+
396
+ const defaultTooltip = (d: Record<string, unknown>, i: number) => (
397
+ <div
398
+ style={{display: 'flex', flexDirection: 'column', gap: 2, fontSize: 12}}>
399
+ <div style={{fontWeight: 600, color: 'var(--color-text-primary)'}}>
400
+ Point {i}
401
+ </div>
402
+ {Object.entries(d).map(([k, v]) => (
403
+ <div key={k}>
404
+ <span style={{color: 'var(--color-text-secondary)'}}>{k}:</span>{' '}
405
+ <span style={{fontWeight: 500}}>{String(v)}</span>
406
+ </div>
407
+ ))}
408
+ </div>
409
+ );
410
+
411
+ return (
412
+ <g>
413
+ <foreignObject
414
+ x={0}
415
+ y={0}
416
+ width={width}
417
+ height={height}
418
+ style={{overflow: 'hidden'}}>
419
+ <canvas
420
+ ref={visCanvasRef}
421
+ style={{width, height, pointerEvents: 'none'}}
422
+ />
423
+ </foreignObject>
424
+
425
+ <foreignObject
426
+ x={0}
427
+ y={0}
428
+ width={0}
429
+ height={0}
430
+ style={{overflow: 'hidden'}}>
431
+ <canvas ref={pickCanvasRef} style={{display: 'none'}} />
432
+ </foreignObject>
433
+
434
+ <rect
435
+ x={0}
436
+ y={0}
437
+ width={width}
438
+ height={height}
439
+ fill="transparent"
440
+ onMouseMove={handleMouseMove}
441
+ onMouseLeave={handleMouseLeave}
442
+ />
443
+
444
+ {datum && hoverIndex >= 0 && (
445
+ <circle
446
+ cx={positions[hoverIndex * 2]}
447
+ cy={positions[hoverIndex * 2 + 1]}
448
+ r={size / 2 + 3}
449
+ fill="none"
450
+ stroke={color}
451
+ strokeWidth={2}
452
+ strokeOpacity={0.8}
453
+ pointerEvents="none"
454
+ />
455
+ )}
456
+
457
+ {datum && mousePos && (
458
+ <foreignObject
459
+ x={mousePos.x + 12}
460
+ y={Math.max(0, mousePos.y - 40)}
461
+ width={200}
462
+ height={120}
463
+ pointerEvents="none"
464
+ style={{overflow: 'visible'}}>
465
+ <div
466
+ style={{
467
+ background: 'var(--color-background-popover)',
468
+ border: '1px solid var(--color-border)',
469
+ borderRadius: 8,
470
+ padding: '8px 12px',
471
+ boxShadow: 'var(--shadow-med)',
472
+ whiteSpace: 'nowrap',
473
+ width: 'fit-content',
474
+ }}>
475
+ {renderTooltip
476
+ ? renderTooltip(datum, hoverIndex)
477
+ : defaultTooltip(datum, hoverIndex)}
478
+ </div>
479
+ </foreignObject>
480
+ )}
481
+ </g>
482
+ );
483
+ }
484
+
485
+ /**
486
+ * WebGL scatter with GPU color-picking hover. O(1) hit detection via offscreen
487
+ * framebuffer — no JS iteration regardless of point count.
488
+ *
489
+ * @example
490
+ * ```
491
+ * series={[dotGLInteractive('value', {
492
+ * color: '#4f46e5',
493
+ * renderTooltip: (d, i) => <span>{d.name}: {d.value}</span>,
494
+ * })]}
495
+ * ```
496
+ */
497
+ export function dotGLInteractive(
498
+ dataKey: string,
499
+ options: DotGLInteractiveOptions,
500
+ ): SeriesDef {
501
+ const {color} = options;
502
+ const size = options.size ?? 6;
503
+ const opacity = options.opacity ?? 0.8;
504
+ const renderTooltip = options.renderTooltip;
505
+
506
+ return {
507
+ type: 'dotGLInteractive',
508
+ key: dataKey,
509
+ dataKeys: [dataKey],
510
+ layout: {},
511
+
512
+ resolve(ctx) {
513
+ const {data, xKey, xScale, yScale} = ctx;
514
+ const points: ResolvedPoint[] = [];
515
+ for (let i = 0; i < data.length; i++) {
516
+ const d = data[i];
517
+ let px: number;
518
+ if ('bandwidth' in xScale) {
519
+ px =
520
+ ((xScale as ScaleBand<string>)(String(d[xKey])) ?? 0) +
521
+ (xScale as ScaleBand<string>).bandwidth() / 2;
522
+ } else {
523
+ px = xScale(d[xKey] as number);
524
+ }
525
+ const v = typeof d[dataKey] === 'number' ? (d[dataKey] as number) : 0;
526
+ points.push({px, py: yScale(v), py0: yScale(0), dataIndex: i});
527
+ }
528
+ return points;
529
+ },
530
+
531
+ render(resolved, ctx) {
532
+ return (
533
+ <DotGLInteractiveCanvas
534
+ resolved={resolved}
535
+ color={color}
536
+ size={size}
537
+ opacity={opacity}
538
+ width={ctx.width}
539
+ height={ctx.height}
540
+ data={ctx.data}
541
+ renderTooltip={renderTooltip}
542
+ />
543
+ );
544
+ },
545
+ };
546
+ }
@@ -0,0 +1,89 @@
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
6
+ */
7
+
8
+ import type {SeriesDef, ResolvedPoint} from '../types';
9
+ import type {ScaleBand} from 'd3-scale';
10
+
11
+ export interface ErrorBarOptions {
12
+ /** Data key for the upper bound */
13
+ high: string;
14
+ /** Data key for the lower bound */
15
+ low: string;
16
+ /** Stroke color */
17
+ color?: string;
18
+ /** Stroke width */
19
+ strokeWidth?: number;
20
+ /** Width of the whisker caps in pixels */
21
+ capWidth?: number;
22
+ }
23
+
24
+ /**
25
+ * Error bars with whisker caps. Pair with bar() or dot().
26
+ *
27
+ * @example
28
+ * ```
29
+ * series={[bar('mean'), errorBar({high: 'upper95', low: 'lower95'})]}
30
+ * ```
31
+ */
32
+ export function errorBar(options: ErrorBarOptions): SeriesDef {
33
+ const {high, low} = options;
34
+ const color = options.color ?? 'var(--color-text-primary)';
35
+ const strokeWidth = options.strokeWidth ?? 1.5;
36
+ const capWidth = options.capWidth ?? 8;
37
+
38
+ return {
39
+ type: 'errorBar',
40
+ key: `errorBar-${high}-${low}`,
41
+ dataKeys: [high, low],
42
+ layout: {},
43
+
44
+ resolve(ctx) {
45
+ const {data, xKey, xScale, yScale} = ctx;
46
+ const points: ResolvedPoint[] = [];
47
+ for (let i = 0; i < data.length; i++) {
48
+ const d = data[i];
49
+ let px: number;
50
+ if ('bandwidth' in xScale) {
51
+ px = ((xScale as ScaleBand<string>)(String(d[xKey])) ?? 0) + (xScale as ScaleBand<string>).bandwidth() / 2;
52
+ } else {
53
+ px = xScale(d[xKey] as number);
54
+ }
55
+ const upper = typeof d[high] === 'number' ? (d[high] as number) : 0;
56
+ const lower = typeof d[low] === 'number' ? (d[low] as number) : 0;
57
+ points.push({px, py: yScale(upper), py0: yScale(lower), dataIndex: i});
58
+ }
59
+ return points;
60
+ },
61
+
62
+ render(resolved) {
63
+ const half = capWidth / 2;
64
+ return (
65
+ <g>
66
+ {resolved.map((p, i) => (
67
+ <g key={i}>
68
+ {/* Vertical stem */}
69
+ <line
70
+ x1={p.px} x2={p.px} y1={p.py} y2={p.py0}
71
+ stroke={color} strokeWidth={strokeWidth}
72
+ />
73
+ {/* Upper cap */}
74
+ <line
75
+ x1={p.px - half} x2={p.px + half} y1={p.py} y2={p.py}
76
+ stroke={color} strokeWidth={strokeWidth}
77
+ />
78
+ {/* Lower cap */}
79
+ <line
80
+ x1={p.px - half} x2={p.px + half} y1={p.py0} y2={p.py0}
81
+ stroke={color} strokeWidth={strokeWidth}
82
+ />
83
+ </g>
84
+ ))}
85
+ </g>
86
+ );
87
+ },
88
+ };
89
+ }