@pharos-ds/charts 0.0.0-bootstrap.0 → 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 +118 -0
  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 +72 -4
  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,584 @@
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
+ * Two GL contexts (visible + offscreen pick) plus their programs, buffers,
11
+ * framebuffer, and pick texture are created once and reused. The pick texture
12
+ * is re-allocated only when the drawing-buffer size changes, and everything is
13
+ * released (both contexts eagerly lost) on unmount. Recovers from context loss.
14
+ */
15
+
16
+ import { useRef, useEffect, useCallback, useState, useMemo } from 'react';
17
+ import { hexToGL, createProgram, registerContextLossHandlers, loseContext } from "../webgl.js";
18
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
19
+ /**
20
+ * Extra radius (CSS px) added to each point in the pick pass only, so hover
21
+ * feels "magnetic" — you don't have to land pixel-perfectly on a small dot.
22
+ * The visible pass is unaffected, so dots still render at their real size.
23
+ */
24
+ const HIT_PADDING = 16;
25
+
26
+ /** Encode a point index as an RGB color (supports up to 16.7M points) */
27
+ function indexToColor(i) {
28
+ const id = i + 1;
29
+ return [(id >> 16 & 0xff) / 255, (id >> 8 & 0xff) / 255, (id & 0xff) / 255];
30
+ }
31
+
32
+ /** Decode an RGB pixel back to a point index. Returns -1 for background. */
33
+ function colorToIndex(r, g, b) {
34
+ if (r === 0 && g === 0 && b === 0) {
35
+ return -1;
36
+ }
37
+ return (r << 16 | g << 8 | b) - 1;
38
+ }
39
+
40
+ // Visible pass
41
+ const VERT_VIS = `
42
+ attribute vec2 a_position;
43
+ uniform vec2 u_resolution;
44
+ uniform float u_size;
45
+ void main() {
46
+ vec2 clip = (a_position / u_resolution) * 2.0 - 1.0;
47
+ gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
48
+ gl_PointSize = u_size;
49
+ }
50
+ `;
51
+ const FRAG_VIS = `
52
+ precision mediump float;
53
+ uniform vec3 u_color;
54
+ uniform float u_opacity;
55
+ void main() {
56
+ vec2 coord = gl_PointCoord - vec2(0.5);
57
+ if (dot(coord, coord) > 0.25) discard;
58
+ gl_FragColor = vec4(u_color, u_opacity);
59
+ }
60
+ `;
61
+
62
+ // Pick pass — encode index as color
63
+ const VERT_PICK = `
64
+ attribute vec2 a_position;
65
+ attribute vec3 a_pickColor;
66
+ uniform vec2 u_resolution;
67
+ uniform float u_size;
68
+ varying vec3 v_pickColor;
69
+ void main() {
70
+ vec2 clip = (a_position / u_resolution) * 2.0 - 1.0;
71
+ gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
72
+ gl_PointSize = u_size;
73
+ v_pickColor = a_pickColor;
74
+ }
75
+ `;
76
+ const FRAG_PICK = `
77
+ precision mediump float;
78
+ varying vec3 v_pickColor;
79
+ void main() {
80
+ vec2 coord = gl_PointCoord - vec2(0.5);
81
+ if (dot(coord, coord) > 0.25) discard;
82
+ gl_FragColor = vec4(v_pickColor, 1.0);
83
+ }
84
+ `;
85
+
86
+ /**
87
+ * Inline component — manages visible + pick canvases and hover state.
88
+ */
89
+ function DotGLInteractiveCanvas({
90
+ resolved,
91
+ color,
92
+ size,
93
+ opacity,
94
+ width,
95
+ height,
96
+ data,
97
+ renderTooltip
98
+ }) {
99
+ const visCanvasRef = useRef(null);
100
+ const pickCanvasRef = useRef(null);
101
+ const [hoverIndex, setHoverIndex] = useState(-1);
102
+ const [mousePos, setMousePos] = useState(null);
103
+ // Bumped on context restore to force the render effect to rebuild + repaint.
104
+ const [contextEpoch, setContextEpoch] = useState(0);
105
+ const visGLRef = useRef(null);
106
+ const visBufRef = useRef(null);
107
+ const pickGLRef = useRef(null);
108
+ const pickPosBufRef = useRef(null);
109
+ const pickColBufRef = useRef(null);
110
+
111
+ // Positions and pick colors are index-aligned with `resolved` (and thus with
112
+ // `data`); do NOT drop entries here or hover decode would map to wrong rows.
113
+ // Non-finite points get NaN clip positions and simply don't rasterize.
114
+ const positions = useMemo(() => {
115
+ const pos = new Float32Array(resolved.length * 2);
116
+ for (let i = 0; i < resolved.length; i++) {
117
+ pos[i * 2] = resolved[i].px;
118
+ pos[i * 2 + 1] = resolved[i].py;
119
+ }
120
+ return pos;
121
+ }, [resolved]);
122
+ const pickColors = useMemo(() => {
123
+ const colors = new Float32Array(resolved.length * 3);
124
+ for (let i = 0; i < resolved.length; i++) {
125
+ const [r, g, b] = indexToColor(i);
126
+ colors[i * 3] = r;
127
+ colors[i * 3 + 1] = g;
128
+ colors[i * 3 + 2] = b;
129
+ }
130
+ return colors;
131
+ }, [resolved.length]);
132
+
133
+ // Render both passes. Contexts, programs, buffers, framebuffer, and pick
134
+ // texture are created once and reused; only vertex data + uniforms upload per
135
+ // run, and the pick texture re-allocates only when the buffer size changes.
136
+ useEffect(() => {
137
+ if (width <= 0 || height <= 0) {
138
+ return;
139
+ }
140
+ const dpr = window.devicePixelRatio || 1;
141
+ const bufW = Math.max(1, Math.round(width * dpr));
142
+ const bufH = Math.max(1, Math.round(height * dpr));
143
+
144
+ // ── Visible pass ──
145
+ const visCanvas = visCanvasRef.current;
146
+ if (visCanvas) {
147
+ if (visCanvas.width !== bufW) {
148
+ visCanvas.width = bufW;
149
+ }
150
+ if (visCanvas.height !== bufH) {
151
+ visCanvas.height = bufH;
152
+ }
153
+ if (!visGLRef.current) {
154
+ const gl = visCanvas.getContext('webgl', {
155
+ alpha: true,
156
+ premultipliedAlpha: false,
157
+ antialias: true
158
+ });
159
+ const prog = gl ? createProgram(gl, VERT_VIS, FRAG_VIS) : null;
160
+ if (gl && prog) {
161
+ visGLRef.current = {
162
+ gl,
163
+ prog
164
+ };
165
+ visBufRef.current = null;
166
+ }
167
+ }
168
+ const vis = visGLRef.current;
169
+ if (vis && !vis.gl.isContextLost()) {
170
+ const {
171
+ gl,
172
+ prog
173
+ } = vis;
174
+ if (!visBufRef.current) {
175
+ visBufRef.current = gl.createBuffer();
176
+ }
177
+ const buf = visBufRef.current;
178
+ if (buf) {
179
+ gl.viewport(0, 0, visCanvas.width, visCanvas.height);
180
+ gl.clearColor(0, 0, 0, 0);
181
+ gl.clear(gl.COLOR_BUFFER_BIT);
182
+ gl.enable(gl.BLEND);
183
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
184
+ gl.useProgram(prog);
185
+ gl.bindBuffer(gl.ARRAY_BUFFER, buf);
186
+ gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
187
+ const aPos = gl.getAttribLocation(prog, 'a_position');
188
+ gl.enableVertexAttribArray(aPos);
189
+ gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
190
+ const [r, g, b] = hexToGL(color);
191
+ gl.uniform2f(gl.getUniformLocation(prog, 'u_resolution'), width, height);
192
+ gl.uniform3f(gl.getUniformLocation(prog, 'u_color'), r, g, b);
193
+ gl.uniform1f(gl.getUniformLocation(prog, 'u_size'), size * dpr);
194
+ gl.uniform1f(gl.getUniformLocation(prog, 'u_opacity'), opacity);
195
+ gl.drawArrays(gl.POINTS, 0, positions.length / 2);
196
+ }
197
+ }
198
+ }
199
+
200
+ // ── Pick pass (offscreen framebuffer) ──
201
+ const pickCanvas = pickCanvasRef.current;
202
+ if (pickCanvas) {
203
+ if (pickCanvas.width !== bufW) {
204
+ pickCanvas.width = bufW;
205
+ }
206
+ if (pickCanvas.height !== bufH) {
207
+ pickCanvas.height = bufH;
208
+ }
209
+ if (!pickGLRef.current) {
210
+ const gl = pickCanvas.getContext('webgl', {
211
+ alpha: false,
212
+ premultipliedAlpha: false,
213
+ antialias: false,
214
+ preserveDrawingBuffer: true
215
+ });
216
+ if (gl) {
217
+ const prog = createProgram(gl, VERT_PICK, FRAG_PICK);
218
+ const fb = gl.createFramebuffer();
219
+ const tex = gl.createTexture();
220
+ if (prog && fb && tex) {
221
+ gl.bindTexture(gl.TEXTURE_2D, tex);
222
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
223
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
224
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
225
+ gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
226
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
227
+ pickGLRef.current = {
228
+ gl,
229
+ prog,
230
+ fb,
231
+ tex,
232
+ texW: 0,
233
+ texH: 0,
234
+ dpr
235
+ };
236
+ pickPosBufRef.current = null;
237
+ pickColBufRef.current = null;
238
+ }
239
+ }
240
+ }
241
+ const pick = pickGLRef.current;
242
+ if (pick && !pick.gl.isContextLost()) {
243
+ const {
244
+ gl,
245
+ prog,
246
+ fb,
247
+ tex
248
+ } = pick;
249
+ pick.dpr = dpr;
250
+
251
+ // (Re)allocate the pick texture only when the buffer size changes.
252
+ if (pick.texW !== pickCanvas.width || pick.texH !== pickCanvas.height) {
253
+ gl.bindTexture(gl.TEXTURE_2D, tex);
254
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, pickCanvas.width, pickCanvas.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
255
+ pick.texW = pickCanvas.width;
256
+ pick.texH = pickCanvas.height;
257
+ }
258
+ if (!pickPosBufRef.current) {
259
+ pickPosBufRef.current = gl.createBuffer();
260
+ }
261
+ if (!pickColBufRef.current) {
262
+ pickColBufRef.current = gl.createBuffer();
263
+ }
264
+ const posBuf = pickPosBufRef.current;
265
+ const colBuf = pickColBufRef.current;
266
+ if (posBuf && colBuf) {
267
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
268
+ gl.viewport(0, 0, pickCanvas.width, pickCanvas.height);
269
+ gl.clearColor(0, 0, 0, 1);
270
+ gl.clear(gl.COLOR_BUFFER_BIT);
271
+ gl.disable(gl.BLEND);
272
+ gl.useProgram(prog);
273
+ gl.bindBuffer(gl.ARRAY_BUFFER, posBuf);
274
+ gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
275
+ const aPos = gl.getAttribLocation(prog, 'a_position');
276
+ gl.enableVertexAttribArray(aPos);
277
+ gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
278
+ gl.bindBuffer(gl.ARRAY_BUFFER, colBuf);
279
+ gl.bufferData(gl.ARRAY_BUFFER, pickColors, gl.STATIC_DRAW);
280
+ const aCol = gl.getAttribLocation(prog, 'a_pickColor');
281
+ gl.enableVertexAttribArray(aCol);
282
+ gl.vertexAttribPointer(aCol, 3, gl.FLOAT, false, 0, 0);
283
+ gl.uniform2f(gl.getUniformLocation(prog, 'u_resolution'), width, height);
284
+ gl.uniform1f(gl.getUniformLocation(prog, 'u_size'), (size + HIT_PADDING) * dpr);
285
+ gl.drawArrays(gl.POINTS, 0, positions.length / 2);
286
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
287
+ }
288
+ }
289
+ }
290
+ }, [width, height, positions, pickColors, color, size, opacity, contextEpoch]);
291
+
292
+ // Register context-loss recovery for both canvases (runs once post-mount).
293
+ useEffect(() => {
294
+ const cleanups = [];
295
+ const visCanvas = visCanvasRef.current;
296
+ if (visCanvas) {
297
+ cleanups.push(registerContextLossHandlers(visCanvas, () => {
298
+ visGLRef.current = null;
299
+ visBufRef.current = null;
300
+ }, () => setContextEpoch(e => e + 1)));
301
+ }
302
+ const pickCanvas = pickCanvasRef.current;
303
+ if (pickCanvas) {
304
+ cleanups.push(registerContextLossHandlers(pickCanvas, () => {
305
+ pickGLRef.current = null;
306
+ pickPosBufRef.current = null;
307
+ pickColBufRef.current = null;
308
+ }, () => setContextEpoch(e => e + 1)));
309
+ }
310
+ return () => {
311
+ for (const c of cleanups) {
312
+ c();
313
+ }
314
+ };
315
+ }, []);
316
+
317
+ // Release both GL contexts and all their resources on unmount.
318
+ useEffect(() => {
319
+ return () => {
320
+ const vis = visGLRef.current;
321
+ if (vis) {
322
+ const {
323
+ gl,
324
+ prog
325
+ } = vis;
326
+ if (visBufRef.current) {
327
+ gl.deleteBuffer(visBufRef.current);
328
+ }
329
+ gl.deleteProgram(prog);
330
+ loseContext(gl);
331
+ }
332
+ const pick = pickGLRef.current;
333
+ if (pick) {
334
+ const {
335
+ gl,
336
+ prog,
337
+ fb,
338
+ tex
339
+ } = pick;
340
+ if (pickPosBufRef.current) {
341
+ gl.deleteBuffer(pickPosBufRef.current);
342
+ }
343
+ if (pickColBufRef.current) {
344
+ gl.deleteBuffer(pickColBufRef.current);
345
+ }
346
+ gl.deleteFramebuffer(fb);
347
+ gl.deleteTexture(tex);
348
+ gl.deleteProgram(prog);
349
+ loseContext(gl);
350
+ }
351
+ visGLRef.current = null;
352
+ visBufRef.current = null;
353
+ pickGLRef.current = null;
354
+ pickPosBufRef.current = null;
355
+ pickColBufRef.current = null;
356
+ };
357
+ }, []);
358
+ const handleMouseMove = useCallback(e => {
359
+ const pick = pickGLRef.current;
360
+ if (!pick || pick.gl.isContextLost()) {
361
+ return;
362
+ }
363
+ const svg = e.currentTarget.ownerSVGElement;
364
+ if (!svg) {
365
+ return;
366
+ }
367
+ const ctm = e.currentTarget.getScreenCTM();
368
+ if (!ctm) {
369
+ return;
370
+ }
371
+ const pt = svg.createSVGPoint();
372
+ pt.x = e.clientX;
373
+ pt.y = e.clientY;
374
+ const local = pt.matrixTransform(ctm.inverse());
375
+
376
+ // Use the DPR the pick framebuffer was built with so pixel math stays in
377
+ // sync even if devicePixelRatio changed since the last render.
378
+ const {
379
+ gl,
380
+ fb,
381
+ dpr,
382
+ texW,
383
+ texH
384
+ } = pick;
385
+ const px = Math.floor(local.x * dpr);
386
+ const py = Math.floor(local.y * dpr);
387
+ if (px < 0 || py < 0 || px >= texW || py >= texH) {
388
+ setHoverIndex(-1);
389
+ setMousePos(null);
390
+ return;
391
+ }
392
+ gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
393
+ const pixel = new Uint8Array(4);
394
+ // Flip Y from top-origin CSS space to the framebuffer's bottom-left,
395
+ // 0-indexed origin. `texH - 1 - py` (not `texH - py`) keeps the read in
396
+ // [0, texH-1] — the bare `texH - py` reads one row too high and samples
397
+ // out of bounds at the very top edge.
398
+ gl.readPixels(px, texH - 1 - py, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel);
399
+ gl.bindFramebuffer(gl.FRAMEBUFFER, null);
400
+ const idx = colorToIndex(pixel[0], pixel[1], pixel[2]);
401
+ setHoverIndex(idx);
402
+ setMousePos(idx >= 0 ? {
403
+ x: local.x,
404
+ y: local.y
405
+ } : null);
406
+ }, []);
407
+ const handleMouseLeave = useCallback(() => {
408
+ setHoverIndex(-1);
409
+ setMousePos(null);
410
+ }, []);
411
+ const datum = hoverIndex >= 0 && hoverIndex < data.length ? data[hoverIndex] : null;
412
+ const defaultTooltip = (d, i) => /*#__PURE__*/_jsxs("div", {
413
+ style: {
414
+ display: 'flex',
415
+ flexDirection: 'column',
416
+ gap: 2,
417
+ fontSize: 12
418
+ },
419
+ children: [/*#__PURE__*/_jsxs("div", {
420
+ style: {
421
+ fontWeight: 600,
422
+ color: 'var(--color-text-primary)'
423
+ },
424
+ children: ["Point ", i]
425
+ }), Object.entries(d).map(([k, v]) => /*#__PURE__*/_jsxs("div", {
426
+ children: [/*#__PURE__*/_jsxs("span", {
427
+ style: {
428
+ color: 'var(--color-text-secondary)'
429
+ },
430
+ children: [k, ":"]
431
+ }), ' ', /*#__PURE__*/_jsx("span", {
432
+ style: {
433
+ fontWeight: 500
434
+ },
435
+ children: String(v)
436
+ })]
437
+ }, k))]
438
+ });
439
+ return /*#__PURE__*/_jsxs("g", {
440
+ children: [/*#__PURE__*/_jsx("foreignObject", {
441
+ x: 0,
442
+ y: 0,
443
+ width: width,
444
+ height: height,
445
+ style: {
446
+ overflow: 'hidden'
447
+ },
448
+ children: /*#__PURE__*/_jsx("canvas", {
449
+ ref: visCanvasRef,
450
+ style: {
451
+ width,
452
+ height,
453
+ pointerEvents: 'none'
454
+ }
455
+ })
456
+ }), /*#__PURE__*/_jsx("foreignObject", {
457
+ x: 0,
458
+ y: 0,
459
+ width: 0,
460
+ height: 0,
461
+ style: {
462
+ overflow: 'hidden'
463
+ },
464
+ children: /*#__PURE__*/_jsx("canvas", {
465
+ ref: pickCanvasRef,
466
+ style: {
467
+ display: 'none'
468
+ }
469
+ })
470
+ }), /*#__PURE__*/_jsx("rect", {
471
+ x: 0,
472
+ y: 0,
473
+ width: width,
474
+ height: height,
475
+ fill: "transparent",
476
+ onMouseMove: handleMouseMove,
477
+ onMouseLeave: handleMouseLeave
478
+ }), datum && hoverIndex >= 0 && /*#__PURE__*/_jsxs("g", {
479
+ pointerEvents: "none",
480
+ children: [/*#__PURE__*/_jsx("circle", {
481
+ cx: positions[hoverIndex * 2],
482
+ cy: positions[hoverIndex * 2 + 1],
483
+ r: size / 2 + 7,
484
+ fill: "none",
485
+ stroke: color,
486
+ strokeWidth: 2,
487
+ strokeOpacity: 0.35
488
+ }), /*#__PURE__*/_jsx("circle", {
489
+ cx: positions[hoverIndex * 2],
490
+ cy: positions[hoverIndex * 2 + 1],
491
+ r: size / 2 + 2.5,
492
+ fill: color,
493
+ stroke: "var(--color-background-body, #fff)",
494
+ strokeWidth: 2
495
+ })]
496
+ }), datum && mousePos && /*#__PURE__*/_jsx("foreignObject", {
497
+ x: mousePos.x + 12,
498
+ y: Math.max(0, mousePos.y - 40),
499
+ width: 200,
500
+ height: 120,
501
+ pointerEvents: "none",
502
+ style: {
503
+ overflow: 'visible'
504
+ },
505
+ children: /*#__PURE__*/_jsx("div", {
506
+ style: {
507
+ background: 'var(--color-background-popover)',
508
+ border: '1px solid var(--color-border)',
509
+ borderRadius: 8,
510
+ padding: '8px 12px',
511
+ boxShadow: 'var(--shadow-med)',
512
+ whiteSpace: 'nowrap',
513
+ width: 'fit-content'
514
+ },
515
+ children: renderTooltip ? renderTooltip(datum, hoverIndex) : defaultTooltip(datum, hoverIndex)
516
+ })
517
+ })]
518
+ });
519
+ }
520
+
521
+ /**
522
+ * WebGL scatter with GPU color-picking hover. O(1) hit detection via offscreen
523
+ * framebuffer — no JS iteration regardless of point count.
524
+ *
525
+ * @example
526
+ * ```
527
+ * series={[dotGLInteractive('value', {
528
+ * color: '#4f46e5',
529
+ * renderTooltip: (d, i) => <span>{d.name}: {d.value}</span>,
530
+ * })]}
531
+ * ```
532
+ */
533
+ export function dotGLInteractive(dataKey, options) {
534
+ const {
535
+ color
536
+ } = options;
537
+ const size = options.size ?? 6;
538
+ const opacity = options.opacity ?? 0.8;
539
+ const renderTooltip = options.renderTooltip;
540
+ return {
541
+ type: 'dotGLInteractive',
542
+ key: dataKey,
543
+ dataKeys: [dataKey],
544
+ layout: {},
545
+ resolve(ctx) {
546
+ const {
547
+ data,
548
+ xKey,
549
+ xScale,
550
+ yScale
551
+ } = ctx;
552
+ const points = [];
553
+ for (let i = 0; i < data.length; i++) {
554
+ const d = data[i];
555
+ let px;
556
+ if ('bandwidth' in xScale) {
557
+ px = (xScale(String(d[xKey])) ?? 0) + xScale.bandwidth() / 2;
558
+ } else {
559
+ px = xScale(d[xKey]);
560
+ }
561
+ const v = typeof d[dataKey] === 'number' ? d[dataKey] : 0;
562
+ points.push({
563
+ px,
564
+ py: yScale(v),
565
+ py0: yScale(0),
566
+ dataIndex: i
567
+ });
568
+ }
569
+ return points;
570
+ },
571
+ render(resolved, ctx) {
572
+ return /*#__PURE__*/_jsx(DotGLInteractiveCanvas, {
573
+ resolved: resolved,
574
+ color: color,
575
+ size: size,
576
+ opacity: opacity,
577
+ width: ctx.width,
578
+ height: ctx.height,
579
+ data: ctx.data,
580
+ renderTooltip: renderTooltip
581
+ });
582
+ }
583
+ };
584
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @file marks/errorBar.tsx
3
+ * @output Error bar (whisker) series — vertical high/low bounds. Bounds are
4
+ * absolute (so asymmetric intervals work); a point missing either bound is
5
+ * skipped rather than drawn down to the axis.
6
+ */
7
+ import type { SeriesDef } from '../types';
8
+ export interface ErrorBarOptions {
9
+ /** Data key for the upper bound */
10
+ high: string;
11
+ /** Data key for the lower bound */
12
+ low: string;
13
+ /** Stroke color */
14
+ color?: string;
15
+ /** Stroke width */
16
+ strokeWidth?: number;
17
+ /** Width of the whisker caps in pixels */
18
+ capWidth?: number;
19
+ }
20
+ /**
21
+ * Error bars with whisker caps. Pair with bar() or dot().
22
+ *
23
+ * @example
24
+ * ```
25
+ * series={[bar('mean'), errorBar({high: 'upper95', low: 'lower95'})]}
26
+ * ```
27
+ */
28
+ export declare function errorBar(options: ErrorBarOptions): SeriesDef;
29
+ //# sourceMappingURL=errorBar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errorBar.d.ts","sourceRoot":"","sources":["../../src/marks/errorBar.tsx"],"names":[],"mappings":"AAEA;;;;;GAKG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAgB,MAAM,UAAU,CAAC;AAGvD,MAAM,WAAW,eAAe;IAC9B,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,mBAAmB;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,mBAAmB;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0CAA0C;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,eAAe,GAAG,SAAS,CAqF5D"}
@@ -0,0 +1,95 @@
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 { xPixel } from "../utils.js";
11
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
12
+ /**
13
+ * Error bars with whisker caps. Pair with bar() or dot().
14
+ *
15
+ * @example
16
+ * ```
17
+ * series={[bar('mean'), errorBar({high: 'upper95', low: 'lower95'})]}
18
+ * ```
19
+ */
20
+ export function errorBar(options) {
21
+ const {
22
+ high,
23
+ low
24
+ } = options;
25
+ const color = options.color ?? 'var(--color-text-primary)';
26
+ const strokeWidth = options.strokeWidth ?? 1.5;
27
+ const capWidth = options.capWidth ?? 8;
28
+ return {
29
+ type: 'errorBar',
30
+ key: `errorBar-${high}-${low}`,
31
+ dataKeys: [high, low],
32
+ layout: {},
33
+ resolve(ctx) {
34
+ const {
35
+ data,
36
+ xKey,
37
+ xScale,
38
+ yScale
39
+ } = ctx;
40
+ const points = [];
41
+ for (let i = 0; i < data.length; i++) {
42
+ const d = data[i];
43
+ // NaN for a missing bound so render() skips the whole whisker instead
44
+ // of anchoring a cap at the axis (a misleading zero-height error bar).
45
+ const hiVal = d[high];
46
+ const loVal = d[low];
47
+ const upper = typeof hiVal === 'number' && Number.isFinite(hiVal) ? hiVal : NaN;
48
+ const lower = typeof loVal === 'number' && Number.isFinite(loVal) ? loVal : NaN;
49
+ points.push({
50
+ px: xPixel(d, xKey, xScale),
51
+ py: yScale(upper),
52
+ py0: yScale(lower),
53
+ dataIndex: i
54
+ });
55
+ }
56
+ return points;
57
+ },
58
+ render(resolved) {
59
+ // Non-finite capWidth (NaN/±Infinity) would leak into the cap x-coords;
60
+ // treat it like a non-positive width — draw the stem, drop the caps.
61
+ const half = Number.isFinite(capWidth) ? Math.max(0, capWidth) / 2 : 0;
62
+ return /*#__PURE__*/_jsx("g", {
63
+ children: resolved.map(p => {
64
+ if (!Number.isFinite(p.px) || !Number.isFinite(p.py) || !Number.isFinite(p.py0)) {
65
+ return null;
66
+ }
67
+ return /*#__PURE__*/_jsxs("g", {
68
+ children: [/*#__PURE__*/_jsx("line", {
69
+ x1: p.px,
70
+ x2: p.px,
71
+ y1: p.py,
72
+ y2: p.py0,
73
+ stroke: color,
74
+ strokeWidth: strokeWidth
75
+ }), /*#__PURE__*/_jsx("line", {
76
+ x1: p.px - half,
77
+ x2: p.px + half,
78
+ y1: p.py,
79
+ y2: p.py,
80
+ stroke: color,
81
+ strokeWidth: strokeWidth
82
+ }), /*#__PURE__*/_jsx("line", {
83
+ x1: p.px - half,
84
+ x2: p.px + half,
85
+ y1: p.py0,
86
+ y2: p.py0,
87
+ stroke: color,
88
+ strokeWidth: strokeWidth
89
+ })]
90
+ }, p.dataIndex);
91
+ })
92
+ });
93
+ }
94
+ };
95
+ }