@biohub/scatterplot 0.1.4

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.
@@ -0,0 +1,985 @@
1
+ import { jsxs as k, jsx as G } from "react/jsx-runtime";
2
+ import { useState as I, useCallback as Z, useRef as x, useEffect as M, useLayoutEffect as ze, useMemo as q } from "react";
3
+ function De() {
4
+ const [e, t] = I(
5
+ /* @__PURE__ */ new Set()
6
+ ), r = Z((s) => {
7
+ t(s === null ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([s]));
8
+ }, []), n = Z((s) => {
9
+ t(new Set(s));
10
+ }, []), i = Z(() => {
11
+ t(/* @__PURE__ */ new Set());
12
+ }, []), c = Z(
13
+ (s) => e.has(s),
14
+ [e]
15
+ );
16
+ return {
17
+ selectedIndices: e,
18
+ handlePointClick: r,
19
+ setSelection: n,
20
+ clearSelection: i,
21
+ isSelected: c
22
+ };
23
+ }
24
+ const fe = {
25
+ zoom: 1,
26
+ pan: { x: 0, y: 0 }
27
+ }, Me = 1, Ie = 2, Oe = 4;
28
+ function Ue(e, t, r) {
29
+ let n = 0;
30
+ return e && (n |= Me), t && (n |= Ie), r && (n |= Oe), n;
31
+ }
32
+ function de(e, t, r, n) {
33
+ const i = new Uint8Array(e), c = t !== void 0 && t.size > 0;
34
+ for (let s = 0; s < e; s++) {
35
+ const a = c ? t.has(s) : !0, u = r?.has(s) ?? !1, d = n !== void 0 ? n.has(s) : c && !a && !u;
36
+ i[s] = Ue(a, d, u);
37
+ }
38
+ return i;
39
+ }
40
+ const ge = "#3498db", ke = [52, 152, 219], he = 255;
41
+ function ae(e, t, r) {
42
+ const n = e.createShader(t);
43
+ return n ? (e.shaderSource(n, r), e.compileShader(n), e.getShaderParameter(n, e.COMPILE_STATUS) ? n : (console.error("Shader compile error:", e.getShaderInfoLog(n)), e.deleteShader(n), null)) : (console.error("Failed to create WebGL shader"), null);
44
+ }
45
+ function Ye(e, t, r) {
46
+ const n = e.createProgram();
47
+ return n ? (e.attachShader(n, t), e.attachShader(n, r), e.linkProgram(n), e.getProgramParameter(n, e.LINK_STATUS) ? n : (console.error("Program link error:", e.getProgramInfoLog(n)), e.deleteProgram(n), null)) : (console.error("Failed to create WebGL program"), null);
48
+ }
49
+ function me(e) {
50
+ if (!e || !/^#?[0-9A-Fa-f]{3,6}$/.test(e))
51
+ return console.warn(
52
+ `Invalid hex color: "${e}", using fallback (${ge})`
53
+ ), ke;
54
+ const t = e.replace("#", ""), r = t.length === 3 ? t.split("").map((a) => a + a).join("") : t, n = parseInt(r, 16), i = n >> 16 & 255, c = n >> 8 & 255, s = n & 255;
55
+ return [i, c, s];
56
+ }
57
+ function Ne(e, t = he) {
58
+ const [r, n, i] = me(e);
59
+ return [r, n, i, t];
60
+ }
61
+ function He(e, t = ge) {
62
+ const r = new Float32Array(e.length * 2), n = new Uint8Array(e.length * 4);
63
+ for (let i = 0; i < e.length; i++) {
64
+ const c = e[i];
65
+ r[i * 2] = c.x, r[i * 2 + 1] = c.y;
66
+ const [s, a, u, d] = Ne(c.color || t, he);
67
+ n[i * 4] = s, n[i * 4 + 1] = a, n[i * 4 + 2] = u, n[i * 4 + 3] = d;
68
+ }
69
+ return { positions: r, colors: n };
70
+ }
71
+ function Ge({
72
+ canvas: e,
73
+ width: t,
74
+ height: r,
75
+ onCameraChange: n,
76
+ enabled: i = !0
77
+ }) {
78
+ const c = x(!1);
79
+ return M(() => {
80
+ if (!e || !i) return;
81
+ const s = (a) => {
82
+ a.preventDefault();
83
+ const u = e.getBoundingClientRect(), d = a.clientX - u.left, S = a.clientY - u.top, m = d / t * 2 - 1, v = -(S / r * 2 - 1), p = a.deltaY < 0 ? 1.1 : 1 / 1.1;
84
+ n((f) => {
85
+ const A = f.zoom, _ = f.pan, L = A * p, h = (m - _.x) / A, g = (v - _.y) / A, B = m - h * L, F = v - g * L;
86
+ return {
87
+ zoom: L,
88
+ pan: { x: B, y: F }
89
+ };
90
+ });
91
+ };
92
+ return e.addEventListener("wheel", s, { passive: !1 }), () => e.removeEventListener("wheel", s);
93
+ }, [e, t, r, n, i]), M(() => {
94
+ if (!e || !i) return;
95
+ let s = !1, a = !1, u = { x: 0, y: 0 };
96
+ const d = (p) => {
97
+ s = !0, a = !1, c.current = !1, u = { x: p.clientX, y: p.clientY };
98
+ }, S = (p) => {
99
+ if (!s) return;
100
+ const f = p.clientX - u.x, A = p.clientY - u.y;
101
+ (Math.abs(f) > 2 || Math.abs(A) > 2) && (a = !0);
102
+ const _ = f / t * 2, L = -(A / r) * 2;
103
+ n((h) => ({
104
+ zoom: h.zoom,
105
+ pan: {
106
+ x: h.pan.x + _,
107
+ y: h.pan.y + L
108
+ }
109
+ })), u = { x: p.clientX, y: p.clientY };
110
+ }, m = () => {
111
+ a && (c.current = !0), s = !1, a = !1;
112
+ }, v = (p) => {
113
+ (p.buttons & 1) === 1 ? (s = !0, u = { x: p.clientX, y: p.clientY }) : s = !1;
114
+ };
115
+ return e.addEventListener("mousedown", d), e.addEventListener("mousemove", S), e.addEventListener("mouseup", m), e.addEventListener("mouseenter", v), () => {
116
+ e.removeEventListener("mousedown", d), e.removeEventListener("mousemove", S), e.removeEventListener("mouseup", m), e.removeEventListener("mouseenter", v);
117
+ };
118
+ }, [e, t, r, n, i]), { justFinishedPanningRef: c };
119
+ }
120
+ function Xe(e) {
121
+ const [t, r] = I({ width: 0, height: 0 }), n = x(null);
122
+ return M(() => {
123
+ const i = e.current;
124
+ if (!i) return;
125
+ const c = new ResizeObserver((u) => {
126
+ const d = u[0];
127
+ if (!d) return;
128
+ const { width: S, height: m } = d.contentRect;
129
+ n.current && cancelAnimationFrame(n.current), n.current = requestAnimationFrame(() => {
130
+ r({ width: S, height: m }), n.current = null;
131
+ });
132
+ });
133
+ c.observe(i);
134
+ const { clientWidth: s, clientHeight: a } = i;
135
+ return s > 0 && a > 0 && r({ width: s, height: a }), () => {
136
+ n.current && cancelAnimationFrame(n.current), c.disconnect();
137
+ };
138
+ }, [e]), t;
139
+ }
140
+ function le(e) {
141
+ const t = x(e);
142
+ return t.current = e, t;
143
+ }
144
+ const $e = 10;
145
+ function We(e) {
146
+ const {
147
+ canvas: t,
148
+ enabled: r,
149
+ onLassoComplete: n,
150
+ onLassoUpdate: i,
151
+ minDisplacement: c = $e
152
+ } = e, [s, a] = I([]), [u, d] = I(!1), S = le(n), m = le(i), v = x(!1);
153
+ return M(() => {
154
+ if (!r || !t)
155
+ return;
156
+ let p = !1, f = [], A = null;
157
+ const _ = (g) => {
158
+ if (g.button !== 0) return;
159
+ const F = g.currentTarget.getBoundingClientRect(), C = g.clientX - F.left, P = g.clientY - F.top, T = [[C, P]];
160
+ A = [C, P], f = T, p = !0, d(!0), a(T);
161
+ }, L = (g) => {
162
+ if (!p || v.current) return;
163
+ v.current = !0, requestAnimationFrame(() => {
164
+ v.current = !1;
165
+ });
166
+ const F = g.currentTarget.getBoundingClientRect(), C = g.clientX - F.left, P = g.clientY - F.top, T = [...f, [C, P]];
167
+ f = T, a(T), m.current && m.current(T);
168
+ }, h = () => {
169
+ if (!p) return;
170
+ p = !1, d(!1);
171
+ let g = 0;
172
+ if (A && f.length > 0) {
173
+ const F = f[f.length - 1], C = F[0] - A[0], P = F[1] - A[1];
174
+ g = Math.sqrt(C * C + P * P);
175
+ }
176
+ const B = g >= c;
177
+ S.current(B ? f : []), f = [], A = null, a([]);
178
+ };
179
+ return t.addEventListener("mousedown", _), t.addEventListener("mousemove", L), t.addEventListener("mouseup", h), t.addEventListener("mouseleave", h), () => {
180
+ t.removeEventListener("mousedown", _), t.removeEventListener("mousemove", L), t.removeEventListener("mouseup", h), t.removeEventListener("mouseleave", h);
181
+ };
182
+ }, [r, t, c, S, m]), {
183
+ lassoPath: s,
184
+ isDrawing: u
185
+ };
186
+ }
187
+ const je = `#version 300 es
188
+ precision mediump float;
189
+
190
+ uniform float u_opacity;
191
+ uniform float u_backgroundOpacity;
192
+ uniform float u_highlightBrightness;
193
+
194
+ in vec4 v_color;
195
+ in float v_isSelected;
196
+ in float v_isBackground;
197
+ in float v_isHighlight;
198
+
199
+ out vec4 fragColor;
200
+
201
+ // Constants for circular point rendering
202
+ const vec2 CENTER = vec2(0.5, 0.5);
203
+ const float RADIUS = 0.5;
204
+
205
+
206
+ void main() {
207
+ // gl_PointCoord gives us the pixel coordinate within the point (0,0 to 1,1)
208
+ // Calculate distance from center
209
+ float dist = length(gl_PointCoord - CENTER);
210
+
211
+ // Discard pixels outside the circle (hard edge, no AA)
212
+ // This approach avoids depth buffer artifacts when points overlap
213
+ if (dist > RADIUS) {
214
+ discard;
215
+ }
216
+
217
+ // Apply flag-based visual changes
218
+ vec3 finalColor = v_color.rgb;
219
+ float finalAlpha = v_color.a * u_opacity;
220
+
221
+ // Background points: reduce opacity (de-emphasize when something else is selected)
222
+ if (v_isBackground > 0.5) {
223
+ finalAlpha *= u_backgroundOpacity;
224
+ }
225
+
226
+ // Highlighted points: brighten color (temporary during lasso drawing)
227
+ if (v_isHighlight > 0.5) {
228
+ finalColor = min(v_color.rgb * u_highlightBrightness, vec3(1.0));
229
+ }
230
+
231
+ // Output premultiplied alpha to avoid edge halos with transparency
232
+ // Requires blendFunc(ONE, ONE_MINUS_SRC_ALPHA) in WebGL setup
233
+ fragColor = vec4(finalColor * finalAlpha, finalAlpha);
234
+ }
235
+ `, Ze = `#version 300 es
236
+ precision mediump float;
237
+
238
+ in vec2 a_position;
239
+ in vec4 a_color;
240
+ in uint a_flag;
241
+ uniform mat3 u_matrix;
242
+ uniform float u_pointSize;
243
+ uniform float u_highlightSizeScale;
244
+ uniform float u_unselectedSizeScale;
245
+
246
+ out vec4 v_color;
247
+ out float v_isSelected;
248
+ out float v_isBackground;
249
+ out float v_isHighlight;
250
+
251
+ // Flag constants (must match JavaScript constants)
252
+ const uint FLAG_SELECTED = 1u;
253
+ const uint FLAG_BACKGROUND = 2u;
254
+ const uint FLAG_HIGHLIGHT = 4u;
255
+
256
+ // Z-depth constants for layering (WebGL clip space: -1 near, +1 far)
257
+ const float Z_BACKGROUND = 0.99; // Background points (farthest back)
258
+ const float Z_NORMAL = 0.0; // Normal points (middle layer)
259
+ const float Z_TOP = -1.0; // Selected/highlighted points (front)
260
+
261
+ void main() {
262
+ // Apply transformation matrix to position
263
+ vec3 transformedPosition = u_matrix * vec3(a_position, 1.0);
264
+
265
+ // Decode flags using bitwise operations (WebGL 2.0 feature)
266
+ bool isSelected = (a_flag & FLAG_SELECTED) != 0u;
267
+ bool isBackground = (a_flag & FLAG_BACKGROUND) != 0u;
268
+ bool isHighlight = (a_flag & FLAG_HIGHLIGHT) != 0u;
269
+
270
+ // Calculate z-depth based on flags (determines render order)
271
+ float z = isBackground ? Z_BACKGROUND : ((isSelected || isHighlight) ? Z_TOP : Z_NORMAL);
272
+
273
+ // Adjust point size based on flags
274
+ float pointSize = u_pointSize;
275
+ if (isHighlight) {
276
+ pointSize *= u_highlightSizeScale;
277
+ } else if (!isSelected) {
278
+ pointSize *= u_unselectedSizeScale;
279
+ }
280
+
281
+ gl_Position = vec4(transformedPosition.xy, z, 1.0);
282
+ gl_PointSize = pointSize;
283
+
284
+ // Pass color and flags to fragment shader
285
+ // Convert bools to floats for varying variables
286
+ v_color = a_color;
287
+ v_isSelected = isSelected ? 1.0 : 0.0;
288
+ v_isBackground = isBackground ? 1.0 : 0.0;
289
+ v_isHighlight = isHighlight ? 1.0 : 0.0;
290
+ }
291
+ `, Ke = "--scatterplot", Ve = ["lasso", "debug"];
292
+ function qe(e) {
293
+ return e.replace(/[A-Z]/g, (t) => `-${t.toLowerCase()}`);
294
+ }
295
+ function Je(e) {
296
+ const t = {};
297
+ for (const r of Ve) {
298
+ const n = e[r];
299
+ for (const [i, c] of Object.entries(n))
300
+ t[`${Ke}-${r}-${qe(i)}`] = String(c);
301
+ }
302
+ return t;
303
+ }
304
+ const pe = {
305
+ canvas: {
306
+ background: "#ffffff",
307
+ dataPadding: 20
308
+ },
309
+ points: {
310
+ defaultColor: "#3498db",
311
+ size: 5,
312
+ opacity: 1,
313
+ backgroundOpacity: 0.5,
314
+ highlightBrightness: 1.4,
315
+ highlightSizeScale: 1.3,
316
+ unselectedSizeScale: 0.2
317
+ },
318
+ lasso: {
319
+ fill: "rgba(59, 130, 246, 0.1)",
320
+ stroke: "rgb(59, 130, 246)",
321
+ strokeWidth: 2,
322
+ strokeDasharray: "5,5"
323
+ },
324
+ debug: {
325
+ background: "rgba(0, 0, 0, 0.8)",
326
+ color: "#00ff00",
327
+ fontFamily: "monospace",
328
+ fontSize: "12px"
329
+ }
330
+ };
331
+ function Se(e, t = pe) {
332
+ return {
333
+ canvas: { ...t.canvas, ...e.canvas },
334
+ points: { ...t.points, ...e.points },
335
+ lasso: { ...t.lasso, ...e.lasso },
336
+ debug: { ...t.debug, ...e.debug }
337
+ };
338
+ }
339
+ const Qe = pe, pt = Se({
340
+ canvas: {
341
+ background: "#1a1a2e"
342
+ },
343
+ points: {
344
+ defaultColor: "#5dade2",
345
+ backgroundOpacity: 0.4,
346
+ highlightBrightness: 1.5
347
+ },
348
+ lasso: {
349
+ fill: "rgba(93, 173, 226, 0.15)",
350
+ stroke: "rgb(93, 173, 226)"
351
+ },
352
+ debug: {
353
+ background: "rgba(0, 0, 0, 0.9)"
354
+ }
355
+ }), St = Se({
356
+ canvas: {
357
+ background: "#000000"
358
+ },
359
+ points: {
360
+ defaultColor: "#ffffff",
361
+ size: 6,
362
+ backgroundOpacity: 0.3,
363
+ highlightBrightness: 1.6,
364
+ highlightSizeScale: 1.5
365
+ },
366
+ lasso: {
367
+ fill: "rgba(255, 255, 0, 0.2)",
368
+ stroke: "#ffff00",
369
+ strokeWidth: 3,
370
+ strokeDasharray: "none"
371
+ },
372
+ debug: {
373
+ background: "#000000",
374
+ color: "#ffffff",
375
+ fontSize: "14px"
376
+ }
377
+ }), ne = Qe;
378
+ function et(e, t) {
379
+ if (t === 0)
380
+ return { xMin: 0, xMax: 1, yMin: 0, yMax: 1 };
381
+ if (e.length < t * 2)
382
+ throw new Error(
383
+ `positions buffer too small: expected at least ${t * 2} elements for ${t} points, got ${e.length}`
384
+ );
385
+ let r = e[0], n = e[0], i = e[1], c = e[1];
386
+ for (let s = 0; s < t; s++) {
387
+ const a = e[s * 2], u = e[s * 2 + 1];
388
+ a < r && (r = a), a > n && (n = a), u < i && (i = u), u > c && (c = u);
389
+ }
390
+ return { xMin: r, xMax: n, yMin: i, yMax: c };
391
+ }
392
+ function _e(e, t, r, n) {
393
+ const i = t - e, c = n - r, s = Math.max(i, c) || 1, a = 0.5 - i / s / 2, u = 0.5 - c / s / 2;
394
+ return { scale: s, xOffset: a, yOffset: u };
395
+ }
396
+ function Ae(e, t, r, n) {
397
+ const i = e / t, c = Math.min(e, t), a = 1 - 2 * (c > 0 ? n / c : 0);
398
+ let u = r * a, d = r * a;
399
+ return i > 1 ? u = r * a / i : i < 1 && (d = r * a * i), { scaleX: u, scaleY: d };
400
+ }
401
+ function tt(e, t, r, n, i, c) {
402
+ const s = new Float32Array(t * 2), { scale: a, xOffset: u, yOffset: d } = _e(
403
+ r,
404
+ n,
405
+ i,
406
+ c
407
+ );
408
+ for (let S = 0; S < t; S++) {
409
+ const m = e[S * 2], v = e[S * 2 + 1], p = (m - r) / a, f = (v - i) / a, A = p + u, _ = f + d;
410
+ s[S * 2] = -1 + 2 * A, s[S * 2 + 1] = -1 + 2 * _;
411
+ }
412
+ return s;
413
+ }
414
+ function ve(e, t, r, n, i, c, s, a, u, d, S, m, v) {
415
+ const p = (e - r) / i, f = (t - n) / i, A = p + c, _ = f + s, L = -1 + 2 * A, h = -1 + 2 * _, g = L * a + d, B = h * u + S, F = (g + 1) * m / 2, C = (1 - B) * v / 2;
416
+ return { screenX: F, screenY: C };
417
+ }
418
+ function nt(e, t, r) {
419
+ if (r.length < 3) return !1;
420
+ let n = !1;
421
+ for (let i = 0, c = r.length - 1; i < r.length; c = i++) {
422
+ const s = r[i][0], a = r[i][1], u = r[c][0], d = r[c][1];
423
+ a > t != d > t && e < (u - s) * (t - a) / (d - a) + s && (n = !n);
424
+ }
425
+ return n;
426
+ }
427
+ const rt = 10;
428
+ function Le(e, t, r, n, i) {
429
+ const { scale: c, xOffset: s, yOffset: a } = _e(
430
+ n.xMin,
431
+ n.xMax,
432
+ n.yMin,
433
+ n.yMax
434
+ ), { scaleX: u, scaleY: d } = Ae(
435
+ e,
436
+ t,
437
+ r,
438
+ i
439
+ );
440
+ return { scale: c, xOffset: s, yOffset: a, scaleX: u, scaleY: d };
441
+ }
442
+ function ot(e, t, r, n, i, c, s, a = rt, u, d = ne.canvas.dataPadding) {
443
+ if (n === 0) return null;
444
+ const { zoom: S, pan: m } = s, { scale: v, xOffset: p, yOffset: f, scaleX: A, scaleY: _ } = Le(
445
+ i,
446
+ c,
447
+ S,
448
+ u,
449
+ d
450
+ );
451
+ let L = null, h = a;
452
+ for (let g = 0; g < n; g++) {
453
+ const B = r[g * 2], F = r[g * 2 + 1], { screenX: C, screenY: P } = ve(
454
+ B,
455
+ F,
456
+ u.xMin,
457
+ u.yMin,
458
+ v,
459
+ p,
460
+ f,
461
+ A,
462
+ _,
463
+ m.x,
464
+ m.y,
465
+ i,
466
+ c
467
+ ), T = C - e, R = P - t, O = Math.sqrt(T * T + R * R);
468
+ O < h && (h = O, L = g);
469
+ }
470
+ return L;
471
+ }
472
+ function ue(e, t, r, n, i, c, s, a = ne.canvas.dataPadding) {
473
+ if (e.length < 3) return /* @__PURE__ */ new Set();
474
+ const { zoom: u, pan: d } = c, { scale: S, xOffset: m, yOffset: v, scaleX: p, scaleY: f } = Le(
475
+ n,
476
+ i,
477
+ u,
478
+ s,
479
+ a
480
+ ), A = /* @__PURE__ */ new Set();
481
+ for (let _ = 0; _ < r; _++) {
482
+ const L = t[_ * 2], h = t[_ * 2 + 1], { screenX: g, screenY: B } = ve(
483
+ L,
484
+ h,
485
+ s.xMin,
486
+ s.yMin,
487
+ S,
488
+ m,
489
+ v,
490
+ p,
491
+ f,
492
+ d.x,
493
+ d.y,
494
+ n,
495
+ i
496
+ );
497
+ nt(g, B, e) && A.add(_);
498
+ }
499
+ return A;
500
+ }
501
+ function it(e, t) {
502
+ return new Float32Array([e, 0, 0, 0, t, 0, 0, 0, 1]);
503
+ }
504
+ function st(e, t) {
505
+ return new Float32Array([1, 0, 0, 0, 1, 0, e, t, 1]);
506
+ }
507
+ function ct(e, t) {
508
+ return new Float32Array([
509
+ e[0] * t[0] + e[3] * t[1] + e[6] * t[2],
510
+ e[1] * t[0] + e[4] * t[1] + e[7] * t[2],
511
+ e[2] * t[0] + e[5] * t[1] + e[8] * t[2],
512
+ e[0] * t[3] + e[3] * t[4] + e[6] * t[5],
513
+ e[1] * t[3] + e[4] * t[4] + e[7] * t[5],
514
+ e[2] * t[3] + e[5] * t[4] + e[8] * t[5],
515
+ e[0] * t[6] + e[3] * t[7] + e[6] * t[8],
516
+ e[1] * t[6] + e[4] * t[7] + e[7] * t[8],
517
+ e[2] * t[6] + e[5] * t[7] + e[8] * t[8]
518
+ ]);
519
+ }
520
+ function at({ count: e, camera: t, lassoMetrics: r }) {
521
+ const { zoom: n, pan: i } = t;
522
+ return /* @__PURE__ */ k("div", { className: "scatterplot-debug-panel", children: [
523
+ /* @__PURE__ */ G("div", { className: "scatterplot-debug-panel-title", children: "Debug Info" }),
524
+ /* @__PURE__ */ k("div", { children: [
525
+ "Points: ",
526
+ e.toLocaleString()
527
+ ] }),
528
+ /* @__PURE__ */ k("div", { children: [
529
+ "Zoom: ",
530
+ n.toFixed(3),
531
+ "x"
532
+ ] }),
533
+ /* @__PURE__ */ k("div", { children: [
534
+ "Pan: (",
535
+ i.x.toFixed(3),
536
+ ", ",
537
+ i.y.toFixed(3),
538
+ ")"
539
+ ] }),
540
+ r && /* @__PURE__ */ k(
541
+ "div",
542
+ {
543
+ style: {
544
+ marginTop: "5px",
545
+ borderTop: "1px solid #444",
546
+ paddingTop: "5px"
547
+ },
548
+ children: [
549
+ /* @__PURE__ */ k("div", { children: [
550
+ "Last lasso: ",
551
+ r.timeMs.toFixed(1),
552
+ "ms"
553
+ ] }),
554
+ /* @__PURE__ */ k("div", { children: [
555
+ "Selected: ",
556
+ r.selectedCount.toLocaleString()
557
+ ] })
558
+ ]
559
+ }
560
+ )
561
+ ] });
562
+ }
563
+ function lt({
564
+ lassoPath: e,
565
+ width: t,
566
+ height: r,
567
+ isDrawing: n
568
+ }) {
569
+ if (!n || e.length < 2)
570
+ return null;
571
+ const i = e.map(([c, s]) => `${c},${s}`).join(" ");
572
+ return /* @__PURE__ */ G(
573
+ "svg",
574
+ {
575
+ "aria-hidden": "true",
576
+ style: {
577
+ position: "absolute",
578
+ top: 0,
579
+ left: 0,
580
+ width: t,
581
+ height: r,
582
+ pointerEvents: "none",
583
+ // Prevents blocking canvas mouse events (critical for lasso drawing)
584
+ zIndex: 10
585
+ },
586
+ children: /* @__PURE__ */ G("polygon", { className: "scatterplot-lasso-polygon", points: i })
587
+ }
588
+ );
589
+ }
590
+ const ut = 800, ft = 600;
591
+ function dt({
592
+ width: e,
593
+ height: t,
594
+ positions: r,
595
+ colors: n,
596
+ debug: i = !1,
597
+ pixelRatio: c = window.devicePixelRatio || 1,
598
+ flags: s,
599
+ onPointClick: a,
600
+ lassoEnabled: u = !1,
601
+ onLassoComplete: d,
602
+ onLassoUpdate: S,
603
+ camera: m,
604
+ onCameraChange: v,
605
+ panZoomEnabled: p = !0,
606
+ theme: f = ne,
607
+ className: A
608
+ }) {
609
+ const _ = x(null), L = Xe(_), h = e ?? (L.width > 0 ? L.width : ut), g = t ?? (L.height > 0 ? L.height : ft), B = x(null), [F, C] = I(null);
610
+ ze(() => {
611
+ C(B.current);
612
+ }, []);
613
+ const [P, T] = I(void 0), R = r.length / 2, O = x(null), X = x(null), y = x(-1), z = x(-1), Y = x(-1), K = x(null), J = x(null), re = x(null), oe = x(
614
+ null
615
+ ), ie = x(
616
+ null
617
+ ), se = x(
618
+ null
619
+ ), ce = x(
620
+ null
621
+ ), $ = x(null), W = x(null), V = x(null), j = q(() => et(r, R), [r, R]), Ee = q(() => Je(f), [f]), D = q(() => {
622
+ const [o, l, b] = me(f.canvas.background);
623
+ return [o / 255, l / 255, b / 255, 1];
624
+ }, [f.canvas.background]), [xe, Re] = I(fe), Q = m !== void 0, N = Q ? m : xe, ye = Z(
625
+ (o) => {
626
+ v && v(o), Q || Re(o);
627
+ },
628
+ [v, Q]
629
+ ), { justFinishedPanningRef: ee } = Ge({
630
+ canvas: F,
631
+ width: h,
632
+ height: g,
633
+ onCameraChange: ye,
634
+ enabled: p
635
+ }), { lassoPath: Be, isDrawing: Fe } = We({
636
+ canvas: F,
637
+ enabled: u,
638
+ onLassoComplete: (o) => {
639
+ if (d) {
640
+ const l = i ? performance.now() : 0, b = ue(
641
+ o,
642
+ r,
643
+ R,
644
+ h,
645
+ g,
646
+ N,
647
+ j,
648
+ f.canvas.dataPadding
649
+ );
650
+ i && T({
651
+ timeMs: performance.now() - l,
652
+ selectedCount: b.size
653
+ }), d(b);
654
+ }
655
+ },
656
+ onLassoUpdate: (o) => {
657
+ if (S) {
658
+ const l = ue(
659
+ o,
660
+ r,
661
+ R,
662
+ h,
663
+ g,
664
+ N,
665
+ j,
666
+ f.canvas.dataPadding
667
+ );
668
+ S(l);
669
+ }
670
+ }
671
+ });
672
+ return M(() => {
673
+ const o = B.current;
674
+ if (!o || !a || u) return;
675
+ const l = (b) => {
676
+ if (ee.current) {
677
+ ee.current = !1;
678
+ return;
679
+ }
680
+ const w = o.getBoundingClientRect(), E = b.clientX - w.left, H = b.clientY - w.top, U = ot(
681
+ E,
682
+ H,
683
+ r,
684
+ R,
685
+ h,
686
+ g,
687
+ N,
688
+ f.points.size,
689
+ // Use point size as max distance threshold
690
+ j,
691
+ // Pass pre-computed bounds for performance
692
+ f.canvas.dataPadding
693
+ );
694
+ a(U);
695
+ };
696
+ return o.addEventListener("click", l), () => o.removeEventListener("click", l);
697
+ }, [
698
+ r,
699
+ R,
700
+ h,
701
+ g,
702
+ N,
703
+ f.points.size,
704
+ a,
705
+ j,
706
+ f.canvas.dataPadding,
707
+ u,
708
+ ee
709
+ ]), M(() => {
710
+ const o = B.current;
711
+ if (!o) return;
712
+ const l = o.getContext("webgl2", {
713
+ preserveDrawingBuffer: !0,
714
+ antialias: !1
715
+ });
716
+ if (!l) {
717
+ console.error("WebGL 2 not supported");
718
+ return;
719
+ }
720
+ O.current = l;
721
+ const b = ae(
722
+ l,
723
+ l.VERTEX_SHADER,
724
+ Ze
725
+ ), w = ae(
726
+ l,
727
+ l.FRAGMENT_SHADER,
728
+ je
729
+ );
730
+ if (!b || !w) return;
731
+ const E = Ye(l, b, w);
732
+ if (!E) return;
733
+ if (X.current = E, y.current = l.getAttribLocation(E, "a_position"), z.current = l.getAttribLocation(E, "a_color"), Y.current = l.getAttribLocation(E, "a_flag"), K.current = l.getUniformLocation(E, "u_matrix"), J.current = l.getUniformLocation(
734
+ E,
735
+ "u_pointSize"
736
+ ), re.current = l.getUniformLocation(E, "u_opacity"), oe.current = l.getUniformLocation(
737
+ E,
738
+ "u_backgroundOpacity"
739
+ ), ie.current = l.getUniformLocation(
740
+ E,
741
+ "u_highlightBrightness"
742
+ ), se.current = l.getUniformLocation(
743
+ E,
744
+ "u_highlightSizeScale"
745
+ ), ce.current = l.getUniformLocation(
746
+ E,
747
+ "u_unselectedSizeScale"
748
+ ), y.current === -1) {
749
+ console.error("Failed to get attribute location for a_position");
750
+ return;
751
+ }
752
+ if (z.current === -1) {
753
+ console.error("Failed to get attribute location for a_color");
754
+ return;
755
+ }
756
+ if (Y.current === -1) {
757
+ console.error("Failed to get attribute location for a_flag");
758
+ return;
759
+ }
760
+ if (!K.current) {
761
+ console.error("Failed to get uniform location for u_matrix");
762
+ return;
763
+ }
764
+ if (!J.current) {
765
+ console.error("Failed to get uniform location for u_pointSize");
766
+ return;
767
+ }
768
+ const H = l.createBuffer(), U = l.createBuffer(), te = l.createBuffer();
769
+ return $.current = H, W.current = U, V.current = te, l.enable(l.BLEND), l.blendFunc(l.ONE, l.ONE_MINUS_SRC_ALPHA), l.enable(l.DEPTH_TEST), l.depthFunc(l.LESS), () => {
770
+ b && l.deleteShader(b), w && l.deleteShader(w), X.current && (l.deleteProgram(X.current), X.current = null), $.current && (l.deleteBuffer($.current), $.current = null), W.current && (l.deleteBuffer(W.current), W.current = null), V.current && (l.deleteBuffer(V.current), V.current = null);
771
+ };
772
+ }, []), M(() => {
773
+ const o = O.current, l = $.current;
774
+ if (!o || !l || R === 0) return;
775
+ const { xMin: b, xMax: w, yMin: E, yMax: H } = j, U = tt(
776
+ r,
777
+ R,
778
+ b,
779
+ w,
780
+ E,
781
+ H
782
+ );
783
+ o.bindBuffer(o.ARRAY_BUFFER, l), o.bufferData(o.ARRAY_BUFFER, U, o.STATIC_DRAW), o.enableVertexAttribArray(y.current), o.vertexAttribPointer(
784
+ y.current,
785
+ 2,
786
+ o.FLOAT,
787
+ !1,
788
+ 0,
789
+ 0
790
+ );
791
+ }, [r, R, j]), M(() => {
792
+ const o = O.current, l = W.current;
793
+ !o || !l || R === 0 || (o.bindBuffer(o.ARRAY_BUFFER, l), o.bufferData(o.ARRAY_BUFFER, n, o.STATIC_DRAW), o.enableVertexAttribArray(z.current), o.vertexAttribPointer(
794
+ z.current,
795
+ 4,
796
+ o.UNSIGNED_BYTE,
797
+ !0,
798
+ 0,
799
+ 0
800
+ ));
801
+ }, [n, R]), M(() => {
802
+ const o = O.current, l = X.current, b = $.current, w = W.current, E = V.current, { zoom: H, pan: U } = N;
803
+ if (!o || !l) return;
804
+ if (R === 0) {
805
+ o.clearColor(D[0], D[1], D[2], D[3]), o.clear(o.COLOR_BUFFER_BIT);
806
+ return;
807
+ }
808
+ if (o.useProgram(l), E) {
809
+ const we = s ?? de(R);
810
+ o.bindBuffer(o.ARRAY_BUFFER, E), o.bufferData(o.ARRAY_BUFFER, we, o.DYNAMIC_DRAW), o.enableVertexAttribArray(Y.current), o.vertexAttribIPointer(
811
+ Y.current,
812
+ 1,
813
+ o.UNSIGNED_BYTE,
814
+ 0,
815
+ 0
816
+ );
817
+ }
818
+ const { scaleX: te, scaleY: be } = Ae(
819
+ h,
820
+ g,
821
+ H,
822
+ f.canvas.dataPadding
823
+ ), Ce = it(te, be), Pe = st(U.x, U.y), Te = ct(Pe, Ce);
824
+ o.uniformMatrix3fv(K.current, !1, Te), o.uniform1f(J.current, f.points.size * c), o.uniform1f(re.current, f.points.opacity), o.uniform1f(
825
+ oe.current,
826
+ f.points.backgroundOpacity
827
+ ), o.uniform1f(
828
+ ie.current,
829
+ f.points.highlightBrightness
830
+ ), o.uniform1f(
831
+ se.current,
832
+ f.points.highlightSizeScale
833
+ ), o.uniform1f(
834
+ ce.current,
835
+ f.points.unselectedSizeScale
836
+ ), b && (o.bindBuffer(o.ARRAY_BUFFER, b), o.enableVertexAttribArray(y.current), o.vertexAttribPointer(
837
+ y.current,
838
+ 2,
839
+ o.FLOAT,
840
+ !1,
841
+ 0,
842
+ 0
843
+ )), w && (o.bindBuffer(o.ARRAY_BUFFER, w), o.enableVertexAttribArray(z.current), o.vertexAttribPointer(
844
+ z.current,
845
+ 4,
846
+ o.UNSIGNED_BYTE,
847
+ !0,
848
+ 0,
849
+ 0
850
+ )), E && (o.bindBuffer(o.ARRAY_BUFFER, E), o.enableVertexAttribArray(Y.current), o.vertexAttribIPointer(
851
+ Y.current,
852
+ 1,
853
+ o.UNSIGNED_BYTE,
854
+ 0,
855
+ 0
856
+ )), o.viewport(0, 0, h * c, g * c), o.clearColor(D[0], D[1], D[2], D[3]), o.clear(o.COLOR_BUFFER_BIT | o.DEPTH_BUFFER_BIT), o.drawArrays(o.POINTS, 0, R);
857
+ }, [
858
+ r,
859
+ n,
860
+ s,
861
+ N,
862
+ f,
863
+ c,
864
+ R,
865
+ h,
866
+ g,
867
+ D
868
+ ]), /* @__PURE__ */ k(
869
+ "div",
870
+ {
871
+ ref: _,
872
+ className: A,
873
+ style: {
874
+ position: "relative",
875
+ lineHeight: 0,
876
+ width: e ? `${e}px` : "100%",
877
+ height: t ? `${t}px` : "100%",
878
+ ...Ee
879
+ },
880
+ children: [
881
+ /* @__PURE__ */ G(
882
+ "canvas",
883
+ {
884
+ ref: B,
885
+ width: h * c,
886
+ height: g * c,
887
+ style: {
888
+ width: `${h}px`,
889
+ height: `${g}px`,
890
+ ...i ? { border: "1px dashed #c00" } : {}
891
+ }
892
+ }
893
+ ),
894
+ u && /* @__PURE__ */ G(
895
+ lt,
896
+ {
897
+ lassoPath: Be,
898
+ width: h,
899
+ height: g,
900
+ isDrawing: Fe
901
+ }
902
+ ),
903
+ i && /* @__PURE__ */ G(at, { count: R, camera: N, lassoMetrics: P })
904
+ ]
905
+ }
906
+ );
907
+ }
908
+ const gt = 1e6;
909
+ function _t({
910
+ points: e,
911
+ width: t,
912
+ height: r,
913
+ initialCamera: n = fe,
914
+ theme: i,
915
+ pixelRatio: c,
916
+ enableLasso: s = !0,
917
+ enablePanZoom: a = !0,
918
+ debug: u = !1,
919
+ onSelectionChange: d,
920
+ lassoRealtimeThreshold: S = gt,
921
+ controlled: m
922
+ }) {
923
+ const [v, p] = I(n), f = m?.camera !== void 0, A = f && m?.camera ? m.camera : v, { selectedIndices: _, handlePointClick: L, setSelection: h } = De(), [g, B] = I(
924
+ /* @__PURE__ */ new Set()
925
+ ), { positions: F, colors: C } = q(() => He(e), [e]), P = Z(
926
+ (y) => {
927
+ m?.onCameraChange && m.onCameraChange(y), f || p(y);
928
+ },
929
+ [m?.onCameraChange, f]
930
+ ), T = q(
931
+ () => de(
932
+ e.length,
933
+ _.size > 0 ? _ : void 0,
934
+ g.size > 0 ? g : void 0
935
+ ),
936
+ [e.length, _, g]
937
+ );
938
+ return /* @__PURE__ */ G(
939
+ dt,
940
+ {
941
+ width: t,
942
+ height: r,
943
+ positions: F,
944
+ colors: C,
945
+ flags: T,
946
+ camera: A,
947
+ onCameraChange: P,
948
+ onPointClick: (y) => {
949
+ L(y), d && d(y !== null ? /* @__PURE__ */ new Set([y]) : /* @__PURE__ */ new Set());
950
+ },
951
+ panZoomEnabled: a,
952
+ lassoEnabled: s,
953
+ onLassoComplete: (y) => {
954
+ B(/* @__PURE__ */ new Set()), h(y), d && d(y);
955
+ },
956
+ onLassoUpdate: (y) => {
957
+ e.length >= S || B((z) => z.size === y.size && [...z].every((K) => y.has(K)) ? z : y);
958
+ },
959
+ theme: i,
960
+ pixelRatio: c,
961
+ debug: u
962
+ }
963
+ );
964
+ }
965
+ export {
966
+ fe as DEFAULT_CAMERA,
967
+ _t as Scatterplot,
968
+ dt as ScatterplotGL,
969
+ et as calculateBounds,
970
+ de as createFlagBuffer,
971
+ Se as createTheme,
972
+ pt as darkTheme,
973
+ ve as dataPointToScreen,
974
+ ne as defaultTheme,
975
+ ot as findClosestPointRaw,
976
+ ue as findPointsInLassoRaw,
977
+ Ne as hexToRgba,
978
+ St as highContrastTheme,
979
+ Qe as lightTheme,
980
+ tt as normalizePositionsIsotropic,
981
+ He as pointsToBuffers,
982
+ Xe as useContainerSize,
983
+ De as useSelection
984
+ };
985
+ //# sourceMappingURL=scatterplot.js.map