@photonviz/core 0.1.0

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.
package/dist/index.js ADDED
@@ -0,0 +1,3859 @@
1
+ // src/axes/ticks.ts
2
+ function niceStep(rawStep) {
3
+ const mag = Math.pow(10, Math.floor(Math.log10(rawStep)));
4
+ const norm = rawStep / mag;
5
+ let nice;
6
+ if (norm < 1.5) nice = 1;
7
+ else if (norm < 3) nice = 2;
8
+ else if (norm < 7) nice = 5;
9
+ else nice = 10;
10
+ return nice * mag;
11
+ }
12
+ function autoTicks(min, max, target = 6) {
13
+ if (!isFinite(min) || !isFinite(max) || min === max) return [];
14
+ const span = max - min;
15
+ const step = niceStep(span / Math.max(1, target));
16
+ const start = Math.ceil(min / step) * step;
17
+ const ticks = [];
18
+ for (let i = 0; i < 1e3; i++) {
19
+ const v = start + i * step;
20
+ if (v > max + step * 1e-6) break;
21
+ ticks.push({ value: Math.abs(v) < step * 1e-6 ? 0 : v });
22
+ }
23
+ return ticks;
24
+ }
25
+ function withMinorTicks(major, count) {
26
+ if (count <= 0 || major.length < 2) return major;
27
+ const out = [];
28
+ for (let i = 0; i < major.length; i++) {
29
+ out.push(major[i]);
30
+ if (i < major.length - 1) {
31
+ const a = major[i].value;
32
+ const b = major[i + 1].value;
33
+ const step = (b - a) / (count + 1);
34
+ for (let k = 1; k <= count; k++) {
35
+ out.push({ value: a + step * k, minor: true, grid: false });
36
+ }
37
+ }
38
+ }
39
+ return out;
40
+ }
41
+ function normalize(entry) {
42
+ return typeof entry === "number" ? { value: entry } : entry;
43
+ }
44
+ function resolveTicks(spec, min, max) {
45
+ if (spec == null) return null;
46
+ const raw = typeof spec === "function" ? spec(min, max) : spec;
47
+ return raw.map(normalize).sort((a, b) => a.value - b.value);
48
+ }
49
+ function defaultFormat(value) {
50
+ if (value === 0) return "0";
51
+ const abs = Math.abs(value);
52
+ if (abs >= 1e6 || abs < 1e-4) return value.toExponential(1);
53
+ return parseFloat(value.toPrecision(6)).toString();
54
+ }
55
+
56
+ // src/axes/axis.ts
57
+ var Axis = class {
58
+ constructor(config = {}) {
59
+ this.config = config;
60
+ }
61
+ update(patch) {
62
+ this.config = { ...this.config, ...patch };
63
+ }
64
+ /** Resolve the concrete tick list (labels filled) for the scale's domain. */
65
+ resolve(scale) {
66
+ const [min, max] = scale.domain;
67
+ const explicit = resolveTicks(this.config.ticks, min, max);
68
+ let ticks;
69
+ if (explicit) {
70
+ ticks = explicit;
71
+ } else {
72
+ let major = scale.ticks();
73
+ const minor = this.config.minorTicks;
74
+ if (minor && scale.type === "linear") {
75
+ major = withMinorTicks(major, minor === true ? 4 : minor);
76
+ }
77
+ if (this.config.addTicks?.length) {
78
+ const extra = this.config.addTicks.map(
79
+ (e) => typeof e === "number" ? { value: e } : e
80
+ );
81
+ major = [...major, ...extra].sort((a, b) => a.value - b.value);
82
+ }
83
+ ticks = major;
84
+ }
85
+ const fmt = this.config.format ?? ((v) => scale.formatTick(v));
86
+ return ticks.filter((t) => t.value >= min && t.value <= max).map((t) => ({
87
+ value: t.value,
88
+ label: t.minor ? "" : t.label ?? fmt(t.value),
89
+ minor: t.minor ?? false,
90
+ grid: t.grid ?? !t.minor
91
+ }));
92
+ }
93
+ };
94
+
95
+ // src/gl/shared.ts
96
+ var shared = null;
97
+ function getSharedGL() {
98
+ if (shared) return shared;
99
+ const canvas = document.createElement("canvas");
100
+ const gl = canvas.getContext("webgl2", {
101
+ antialias: true,
102
+ premultipliedAlpha: true,
103
+ alpha: true,
104
+ // Required so the rendered frame survives long enough to be blitted out.
105
+ preserveDrawingBuffer: true,
106
+ depth: true
107
+ });
108
+ if (!gl) throw new Error("WebGL2 is not supported in this environment");
109
+ shared = { canvas, gl };
110
+ return shared;
111
+ }
112
+ function begin2D(gl) {
113
+ gl.disable(gl.DEPTH_TEST);
114
+ gl.enable(gl.BLEND);
115
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
116
+ }
117
+ function begin3D(gl) {
118
+ gl.enable(gl.DEPTH_TEST);
119
+ gl.enable(gl.BLEND);
120
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
121
+ }
122
+ function sizeShared(gl, w, h) {
123
+ const c = gl.canvas;
124
+ if (c.width !== w) c.width = w;
125
+ if (c.height !== h) c.height = h;
126
+ }
127
+
128
+ // src/gl/context.ts
129
+ function parseColor(input) {
130
+ const s = input.trim();
131
+ if (s.startsWith("#")) {
132
+ let hex = s.slice(1);
133
+ if (hex.length === 3) hex = hex.split("").map((c) => c + c).join("");
134
+ const r = parseInt(hex.slice(0, 2), 16) / 255;
135
+ const g = parseInt(hex.slice(2, 4), 16) / 255;
136
+ const b = parseInt(hex.slice(4, 6), 16) / 255;
137
+ const a = hex.length >= 8 ? parseInt(hex.slice(6, 8), 16) / 255 : 1;
138
+ return [r, g, b, a];
139
+ }
140
+ const m = s.match(/rgba?\(([^)]+)\)/);
141
+ if (m) {
142
+ const parts = m[1].split(",").map((p) => parseFloat(p.trim()));
143
+ return [(parts[0] ?? 0) / 255, (parts[1] ?? 0) / 255, (parts[2] ?? 0) / 255, parts[3] ?? 1];
144
+ }
145
+ return [0, 0, 0, 1];
146
+ }
147
+ function toColorCss(c) {
148
+ const to255 = (v) => Math.round(v * 255);
149
+ return `rgba(${to255(c[0])},${to255(c[1])},${to255(c[2])},${c[3]})`;
150
+ }
151
+
152
+ // src/gl/program.ts
153
+ function compileShader(gl, type, source) {
154
+ const shader = gl.createShader(type);
155
+ if (!shader) throw new Error("Failed to create shader");
156
+ gl.shaderSource(shader, source);
157
+ gl.compileShader(shader);
158
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
159
+ const log = gl.getShaderInfoLog(shader);
160
+ gl.deleteShader(shader);
161
+ throw new Error(`Shader compile error:
162
+ ${log}
163
+ ---
164
+ ${source}`);
165
+ }
166
+ return shader;
167
+ }
168
+ function createProgram(gl, vert, frag) {
169
+ const vs = compileShader(gl, gl.VERTEX_SHADER, vert);
170
+ const fs = compileShader(gl, gl.FRAGMENT_SHADER, frag);
171
+ const program = gl.createProgram();
172
+ if (!program) throw new Error("Failed to create program");
173
+ gl.attachShader(program, vs);
174
+ gl.attachShader(program, fs);
175
+ gl.linkProgram(program);
176
+ gl.deleteShader(vs);
177
+ gl.deleteShader(fs);
178
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
179
+ const log = gl.getProgramInfoLog(program);
180
+ gl.deleteProgram(program);
181
+ throw new Error(`Program link error:
182
+ ${log}`);
183
+ }
184
+ return program;
185
+ }
186
+ function uniformLocations(gl, program, names) {
187
+ const out = {};
188
+ for (const name of names) out[name] = gl.getUniformLocation(program, name);
189
+ return out;
190
+ }
191
+
192
+ // src/gl/transform.ts
193
+ var TRANSFORM_GLSL = (
194
+ /* glsl */
195
+ `
196
+ uniform vec2 uDomainX; // linear: (lo-ref, hi-ref) ; log: (log10 lo, log10 hi)
197
+ uniform vec2 uDomainY;
198
+ uniform float uXRef;
199
+ uniform float uYRef;
200
+ uniform float uLogX; // >0.5 => log axis
201
+ uniform float uLogY;
202
+
203
+ vec2 dataToNorm(vec2 p) {
204
+ float cx = (uLogX > 0.5) ? log(p.x + uXRef) / 2.302585092994046 : p.x;
205
+ float cy = (uLogY > 0.5) ? log(p.y + uYRef) / 2.302585092994046 : p.y;
206
+ float nx = (cx - uDomainX.x) / (uDomainX.y - uDomainX.x);
207
+ float ny = (cy - uDomainY.x) / (uDomainY.y - uDomainY.x);
208
+ return vec2(nx, ny);
209
+ }
210
+
211
+ vec2 dataToClip(vec2 p) {
212
+ return dataToNorm(p) * 2.0 - 1.0;
213
+ }
214
+ `
215
+ );
216
+ var TRANSFORM_UNIFORMS = [
217
+ "uDomainX",
218
+ "uDomainY",
219
+ "uXRef",
220
+ "uYRef",
221
+ "uLogX",
222
+ "uLogY"
223
+ ];
224
+ function setTransformUniforms(gl, u, x, y, xRef, yRef) {
225
+ const [lox, hix] = x.log ? [Math.log10(x.lo), Math.log10(x.hi)] : [x.lo - xRef, x.hi - xRef];
226
+ const [loy, hiy] = y.log ? [Math.log10(y.lo), Math.log10(y.hi)] : [y.lo - yRef, y.hi - yRef];
227
+ gl.uniform2f(u.uDomainX, lox, hix);
228
+ gl.uniform2f(u.uDomainY, loy, hiy);
229
+ gl.uniform1f(u.uXRef, xRef);
230
+ gl.uniform1f(u.uYRef, yRef);
231
+ gl.uniform1f(u.uLogX, x.log ? 1 : 0);
232
+ gl.uniform1f(u.uLogY, y.log ? 1 : 0);
233
+ }
234
+
235
+ // src/layers/area.ts
236
+ var VERT = (
237
+ /* glsl */
238
+ `#version 300 es
239
+ precision highp float;
240
+ layout(location = 0) in vec2 aPos; // offset data space
241
+ ${TRANSFORM_GLSL}
242
+ void main() { gl_Position = vec4(dataToClip(aPos), 0.0, 1.0); }`
243
+ );
244
+ var FRAG = (
245
+ /* glsl */
246
+ `#version 300 es
247
+ precision highp float;
248
+ uniform vec4 uColor;
249
+ out vec4 outColor;
250
+ void main() { outColor = vec4(uColor.rgb * uColor.a, uColor.a); }`
251
+ );
252
+ var programCache = /* @__PURE__ */ new WeakMap();
253
+ function getProgram(gl) {
254
+ let p = programCache.get(gl);
255
+ if (!p) {
256
+ p = createProgram(gl, VERT, FRAG);
257
+ programCache.set(gl, p);
258
+ }
259
+ return p;
260
+ }
261
+ var counter = 0;
262
+ var AreaLayer = class {
263
+ constructor(gl, opts) {
264
+ this.xRef = 0;
265
+ this.yRef = 0;
266
+ this.xBounds = [0, 0];
267
+ this.yBounds = [0, 0];
268
+ this.id = `area-${counter++}`;
269
+ this.gl = gl;
270
+ this.program = getProgram(gl);
271
+ const colorInput = opts.color ?? "rgba(59,130,246,0.4)";
272
+ this.color = Array.isArray(colorInput) ? colorInput : parseColor(colorInput);
273
+ this.colorCss = typeof colorInput === "string" ? colorInput : toColorCss(this.color);
274
+ this.name = opts.name ?? this.id;
275
+ this.yAxis = opts.yAxis ?? "y";
276
+ const n = Math.min(opts.x.length, opts.y.length);
277
+ this.vertexCount = n * 2;
278
+ const baseAt = (i) => opts.base == null ? 0 : typeof opts.base === "number" ? opts.base : opts.base[i];
279
+ this.xRef = n > 0 ? opts.x[0] : 0;
280
+ this.yRef = n > 0 ? opts.y[0] : 0;
281
+ const data = new Float32Array(n * 4);
282
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
283
+ for (let i = 0; i < n; i++) {
284
+ const x = opts.x[i], b = baseAt(i), top = opts.y[i];
285
+ data[i * 4] = x - this.xRef;
286
+ data[i * 4 + 1] = b - this.yRef;
287
+ data[i * 4 + 2] = x - this.xRef;
288
+ data[i * 4 + 3] = top - this.yRef;
289
+ minX = Math.min(minX, x);
290
+ maxX = Math.max(maxX, x);
291
+ minY = Math.min(minY, b, top);
292
+ maxY = Math.max(maxY, b, top);
293
+ }
294
+ this.xBounds = [minX, maxX];
295
+ this.yBounds = [minY, maxY];
296
+ const vao = gl.createVertexArray();
297
+ const buffer = gl.createBuffer();
298
+ this.vao = vao;
299
+ this.buffer = buffer;
300
+ gl.bindVertexArray(vao);
301
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
302
+ gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
303
+ gl.enableVertexAttribArray(0);
304
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
305
+ gl.bindVertexArray(null);
306
+ this.uniforms = uniformLocations(gl, this.program, [...TRANSFORM_UNIFORMS, "uColor"]);
307
+ }
308
+ /** Replace the area data and re-upload (for streaming). */
309
+ setData(x, y, base) {
310
+ const n = Math.min(x.length, y.length);
311
+ this.vertexCount = n * 2;
312
+ const baseAt = (i) => base == null ? 0 : typeof base === "number" ? base : base[i];
313
+ this.xRef = n > 0 ? x[0] : 0;
314
+ this.yRef = n > 0 ? y[0] : 0;
315
+ const data = new Float32Array(n * 4);
316
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
317
+ for (let i = 0; i < n; i++) {
318
+ const vx = x[i], b = baseAt(i), top = y[i];
319
+ data[i * 4] = vx - this.xRef;
320
+ data[i * 4 + 1] = b - this.yRef;
321
+ data[i * 4 + 2] = vx - this.xRef;
322
+ data[i * 4 + 3] = top - this.yRef;
323
+ if (vx < minX) minX = vx;
324
+ if (vx > maxX) maxX = vx;
325
+ minY = Math.min(minY, b, top);
326
+ maxY = Math.max(maxY, b, top);
327
+ }
328
+ this.xBounds = [minX, maxX];
329
+ this.yBounds = [minY, maxY];
330
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
331
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, data, this.gl.DYNAMIC_DRAW);
332
+ }
333
+ bounds() {
334
+ if (this.vertexCount === 0) return null;
335
+ return { x: this.xBounds, y: this.yBounds };
336
+ }
337
+ draw(state) {
338
+ if (this.vertexCount < 4) return;
339
+ const gl = state.gl;
340
+ gl.useProgram(this.program);
341
+ setTransformUniforms(gl, this.uniforms, state.x, state.y, this.xRef, this.yRef);
342
+ gl.uniform4f(this.uniforms.uColor, this.color[0], this.color[1], this.color[2], this.color[3]);
343
+ gl.bindVertexArray(this.vao);
344
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, this.vertexCount);
345
+ gl.bindVertexArray(null);
346
+ }
347
+ dispose() {
348
+ this.gl.deleteVertexArray(this.vao);
349
+ this.gl.deleteBuffer(this.buffer);
350
+ }
351
+ };
352
+
353
+ // src/layers/bar.ts
354
+ var VERT2 = (
355
+ /* glsl */
356
+ `#version 300 es
357
+ precision highp float;
358
+ layout(location = 0) in vec2 aCorner; // unit rect [0,1]^2
359
+ layout(location = 1) in vec4 aRect; // (x0,y0,x1,y1) offset data space
360
+ ${TRANSFORM_GLSL}
361
+ void main() {
362
+ vec2 p = mix(aRect.xy, aRect.zw, aCorner);
363
+ gl_Position = vec4(dataToClip(p), 0.0, 1.0);
364
+ }`
365
+ );
366
+ var FRAG2 = (
367
+ /* glsl */
368
+ `#version 300 es
369
+ precision highp float;
370
+ uniform vec4 uColor;
371
+ out vec4 outColor;
372
+ void main() { outColor = vec4(uColor.rgb * uColor.a, uColor.a); }`
373
+ );
374
+ var CORNERS = new Float32Array([0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1]);
375
+ var programCache2 = /* @__PURE__ */ new WeakMap();
376
+ function getProgram2(gl) {
377
+ let p = programCache2.get(gl);
378
+ if (!p) {
379
+ p = createProgram(gl, VERT2, FRAG2);
380
+ programCache2.set(gl, p);
381
+ }
382
+ return p;
383
+ }
384
+ function medianSpacing(x, n) {
385
+ if (n < 2) return 1;
386
+ const diffs = [];
387
+ for (let i = 1; i < n; i++) diffs.push(Math.abs(x[i] - x[i - 1]));
388
+ diffs.sort((a, b) => a - b);
389
+ return diffs[Math.floor(diffs.length / 2)] || 1;
390
+ }
391
+ var counter2 = 0;
392
+ var BarLayer = class {
393
+ constructor(gl, opts) {
394
+ this.buffers = [];
395
+ this.xRef = 0;
396
+ this.yRef = 0;
397
+ this.xBounds = [0, 0];
398
+ this.yBounds = [0, 0];
399
+ this.id = `bar-${counter2++}`;
400
+ this.gl = gl;
401
+ this.program = getProgram2(gl);
402
+ const colorInput = opts.color ?? "#3b82f6";
403
+ this.color = Array.isArray(colorInput) ? colorInput : parseColor(colorInput);
404
+ this.colorCss = typeof colorInput === "string" ? colorInput : toColorCss(this.color);
405
+ this.name = opts.name ?? this.id;
406
+ this.yAxis = opts.yAxis ?? "y";
407
+ const n = Math.min(opts.x.length, opts.y.length);
408
+ this.count = n;
409
+ this.barWidth = opts.width ?? medianSpacing(opts.x, n) * 0.8;
410
+ this.offset = opts.offset ?? 0;
411
+ const rects = this.buildRects(opts.x, opts.y, opts.base, n);
412
+ const vao = gl.createVertexArray();
413
+ this.vao = vao;
414
+ gl.bindVertexArray(vao);
415
+ const cornerBuf = gl.createBuffer();
416
+ gl.bindBuffer(gl.ARRAY_BUFFER, cornerBuf);
417
+ gl.bufferData(gl.ARRAY_BUFFER, CORNERS, gl.STATIC_DRAW);
418
+ gl.enableVertexAttribArray(0);
419
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
420
+ const rectBuf = gl.createBuffer();
421
+ gl.bindBuffer(gl.ARRAY_BUFFER, rectBuf);
422
+ gl.bufferData(gl.ARRAY_BUFFER, rects, gl.STATIC_DRAW);
423
+ gl.enableVertexAttribArray(1);
424
+ gl.vertexAttribPointer(1, 4, gl.FLOAT, false, 0, 0);
425
+ gl.vertexAttribDivisor(1, 1);
426
+ gl.bindVertexArray(null);
427
+ this.buffers = [cornerBuf, rectBuf];
428
+ this.uniforms = uniformLocations(gl, this.program, [...TRANSFORM_UNIFORMS, "uColor"]);
429
+ }
430
+ buildRects(x, y, base, n) {
431
+ const width = this.barWidth, off = this.offset;
432
+ const baseAt = (i) => base == null ? 0 : typeof base === "number" ? base : base[i];
433
+ this.xRef = n > 0 ? x[0] : 0;
434
+ this.yRef = n > 0 ? y[0] : 0;
435
+ const rects = new Float32Array(n * 4);
436
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
437
+ for (let i = 0; i < n; i++) {
438
+ const cx = x[i] + off;
439
+ const x0 = cx - width / 2, x1 = cx + width / 2;
440
+ const b = baseAt(i), top = y[i];
441
+ rects[i * 4] = x0 - this.xRef;
442
+ rects[i * 4 + 1] = b - this.yRef;
443
+ rects[i * 4 + 2] = x1 - this.xRef;
444
+ rects[i * 4 + 3] = top - this.yRef;
445
+ minX = Math.min(minX, x0);
446
+ maxX = Math.max(maxX, x1);
447
+ minY = Math.min(minY, b, top);
448
+ maxY = Math.max(maxY, b, top);
449
+ }
450
+ this.xBounds = [minX, maxX];
451
+ this.yBounds = [minY, maxY];
452
+ return rects;
453
+ }
454
+ /** Replace bar values and re-upload (for streaming). */
455
+ setData(x, y, base) {
456
+ const n = Math.min(x.length, y.length);
457
+ this.count = n;
458
+ const rects = this.buildRects(x, y, base, n);
459
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffers[1]);
460
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, rects, this.gl.DYNAMIC_DRAW);
461
+ }
462
+ bounds() {
463
+ if (this.count === 0) return null;
464
+ return { x: this.xBounds, y: this.yBounds };
465
+ }
466
+ draw(state) {
467
+ if (this.count === 0) return;
468
+ const gl = state.gl;
469
+ gl.useProgram(this.program);
470
+ setTransformUniforms(gl, this.uniforms, state.x, state.y, this.xRef, this.yRef);
471
+ gl.uniform4f(this.uniforms.uColor, this.color[0], this.color[1], this.color[2], this.color[3]);
472
+ gl.bindVertexArray(this.vao);
473
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this.count);
474
+ gl.bindVertexArray(null);
475
+ }
476
+ dispose() {
477
+ this.gl.deleteVertexArray(this.vao);
478
+ for (const b of this.buffers) this.gl.deleteBuffer(b);
479
+ }
480
+ };
481
+
482
+ // src/stats/index.ts
483
+ function histogram(values, opts = {}) {
484
+ const n = values.length;
485
+ let edges;
486
+ if (opts.edges) {
487
+ edges = Float64Array.from(opts.edges);
488
+ } else {
489
+ let lo2 = opts.range?.[0] ?? Infinity;
490
+ let hi2 = opts.range?.[1] ?? -Infinity;
491
+ if (!opts.range) {
492
+ for (let i = 0; i < n; i++) {
493
+ const v = values[i];
494
+ if (v < lo2) lo2 = v;
495
+ if (v > hi2) hi2 = v;
496
+ }
497
+ }
498
+ if (!isFinite(lo2) || !isFinite(hi2) || lo2 === hi2) {
499
+ lo2 = (lo2 || 0) - 0.5;
500
+ hi2 = (hi2 || 0) + 0.5;
501
+ }
502
+ const bins2 = opts.bins ?? Math.max(1, Math.ceil(Math.log2(n || 1) + 1));
503
+ edges = new Float64Array(bins2 + 1);
504
+ for (let i = 0; i <= bins2; i++) edges[i] = lo2 + (hi2 - lo2) * i / bins2;
505
+ }
506
+ const bins = edges.length - 1;
507
+ const counts = new Float64Array(bins);
508
+ const lo = edges[0], hi = edges[bins];
509
+ const width = (hi - lo) / bins;
510
+ for (let i = 0; i < n; i++) {
511
+ const v = values[i];
512
+ if (v < lo || v > hi) continue;
513
+ let b = Math.floor((v - lo) / width);
514
+ if (b >= bins) b = bins - 1;
515
+ counts[b] += 1;
516
+ }
517
+ const centers = new Float64Array(bins);
518
+ for (let i = 0; i < bins; i++) centers[i] = (edges[i] + edges[i + 1]) / 2;
519
+ return { edges, counts, centers, binWidth: width };
520
+ }
521
+ function quantileSorted(sorted, q) {
522
+ const n = sorted.length;
523
+ if (n === 0) return NaN;
524
+ if (n === 1) return sorted[0];
525
+ const pos = (n - 1) * q;
526
+ const lo = Math.floor(pos);
527
+ const frac = pos - lo;
528
+ const a = sorted[lo];
529
+ const b = sorted[Math.min(n - 1, lo + 1)];
530
+ return a + (b - a) * frac;
531
+ }
532
+ function boxStats(values) {
533
+ const sorted = Float64Array.from(values).sort();
534
+ const n = sorted.length;
535
+ const q1 = quantileSorted(sorted, 0.25);
536
+ const median = quantileSorted(sorted, 0.5);
537
+ const q3 = quantileSorted(sorted, 0.75);
538
+ const iqr = q3 - q1;
539
+ const fenceLo = q1 - 1.5 * iqr;
540
+ const fenceHi = q3 + 1.5 * iqr;
541
+ let whiskerLo = q1, whiskerHi = q3;
542
+ const outliers = [];
543
+ for (let i = 0; i < n; i++) {
544
+ const v = sorted[i];
545
+ if (v < fenceLo || v > fenceHi) outliers.push(v);
546
+ else {
547
+ if (v < whiskerLo) whiskerLo = v;
548
+ if (v > whiskerHi) whiskerHi = v;
549
+ }
550
+ }
551
+ return {
552
+ min: n ? sorted[0] : NaN,
553
+ q1,
554
+ median,
555
+ q3,
556
+ max: n ? sorted[n - 1] : NaN,
557
+ whiskerLo,
558
+ whiskerHi,
559
+ outliers
560
+ };
561
+ }
562
+ function stddev(values, mean) {
563
+ const n = values.length;
564
+ let s = 0;
565
+ for (let i = 0; i < n; i++) {
566
+ const d = values[i] - mean;
567
+ s += d * d;
568
+ }
569
+ return Math.sqrt(s / Math.max(1, n - 1));
570
+ }
571
+ function fft(re, im) {
572
+ const n = re.length;
573
+ for (let i = 1, j = 0; i < n; i++) {
574
+ let bit = n >> 1;
575
+ for (; j & bit; bit >>= 1) j ^= bit;
576
+ j ^= bit;
577
+ if (i < j) {
578
+ [re[i], re[j]] = [re[j], re[i]];
579
+ [im[i], im[j]] = [im[j], im[i]];
580
+ }
581
+ }
582
+ for (let len = 2; len <= n; len <<= 1) {
583
+ const ang = -2 * Math.PI / len;
584
+ const wRe = Math.cos(ang), wIm = Math.sin(ang);
585
+ for (let i = 0; i < n; i += len) {
586
+ let curRe = 1, curIm = 0;
587
+ for (let k = 0; k < len / 2; k++) {
588
+ const a = i + k, b = i + k + len / 2;
589
+ const tRe = re[b] * curRe - im[b] * curIm;
590
+ const tIm = re[b] * curIm + im[b] * curRe;
591
+ re[b] = re[a] - tRe;
592
+ im[b] = im[a] - tIm;
593
+ re[a] = re[a] + tRe;
594
+ im[a] = im[a] + tIm;
595
+ const nRe = curRe * wRe - curIm * wIm;
596
+ curIm = curRe * wIm + curIm * wRe;
597
+ curRe = nRe;
598
+ }
599
+ }
600
+ }
601
+ }
602
+ function spectrogram(signal, opts = {}) {
603
+ const fftSize = opts.fftSize ?? 256;
604
+ const hop = opts.hop ?? fftSize >> 1;
605
+ const sr = opts.sampleRate ?? 1;
606
+ const N = signal.length;
607
+ const frames = Math.max(1, Math.floor((N - fftSize) / hop) + 1);
608
+ const bins = fftSize >> 1;
609
+ const values = new Float64Array(bins * frames);
610
+ const re = new Float64Array(fftSize);
611
+ const im = new Float64Array(fftSize);
612
+ for (let f = 0; f < frames; f++) {
613
+ const start = f * hop;
614
+ for (let i = 0; i < fftSize; i++) {
615
+ const w = 0.5 - 0.5 * Math.cos(2 * Math.PI * i / (fftSize - 1));
616
+ re[i] = (signal[start + i] ?? 0) * w;
617
+ im[i] = 0;
618
+ }
619
+ fft(re, im);
620
+ for (let b = 0; b < bins; b++) {
621
+ const mag = Math.hypot(re[b], im[b]) / fftSize;
622
+ values[b * frames + f] = 20 * Math.log10(mag + 1e-9);
623
+ }
624
+ }
625
+ return {
626
+ values,
627
+ cols: frames,
628
+ rows: bins,
629
+ extent: { x: [0, N / sr], y: [0, sr / 2] }
630
+ };
631
+ }
632
+ function kde(values, lo, hi, points = 64, bandwidth) {
633
+ const n = values.length;
634
+ let mean = 0;
635
+ for (let i = 0; i < n; i++) mean += values[i];
636
+ mean /= Math.max(1, n);
637
+ const sd = stddev(values, mean) || 1;
638
+ const h = bandwidth ?? 1.06 * sd * Math.pow(Math.max(1, n), -0.2);
639
+ const xs = new Float64Array(points);
640
+ const ys = new Float64Array(points);
641
+ const norm = 1 / (n * h * Math.sqrt(2 * Math.PI));
642
+ for (let p = 0; p < points; p++) {
643
+ const x = lo + (hi - lo) * p / (points - 1);
644
+ let sum = 0;
645
+ for (let i = 0; i < n; i++) {
646
+ const u = (x - values[i]) / h;
647
+ sum += Math.exp(-0.5 * u * u);
648
+ }
649
+ xs[p] = x;
650
+ ys[p] = sum * norm;
651
+ }
652
+ return { xs, ys };
653
+ }
654
+
655
+ // src/layers/box.ts
656
+ var VERT3 = (
657
+ /* glsl */
658
+ `#version 300 es
659
+ precision highp float;
660
+ layout(location = 0) in vec2 aPos;
661
+ layout(location = 1) in vec4 aColor;
662
+ uniform float uPointSize;
663
+ ${TRANSFORM_GLSL}
664
+ out vec4 vColor;
665
+ void main() {
666
+ vColor = aColor;
667
+ gl_PointSize = uPointSize;
668
+ gl_Position = vec4(dataToClip(aPos), 0.0, 1.0);
669
+ }`
670
+ );
671
+ var FRAG3 = (
672
+ /* glsl */
673
+ `#version 300 es
674
+ precision highp float;
675
+ in vec4 vColor;
676
+ uniform float uIsPoint;
677
+ out vec4 outColor;
678
+ void main() {
679
+ if (uIsPoint > 0.5) {
680
+ vec2 d = gl_PointCoord - 0.5;
681
+ if (length(d) > 0.5) discard;
682
+ }
683
+ outColor = vec4(vColor.rgb * vColor.a, vColor.a);
684
+ }`
685
+ );
686
+ var programCache3 = /* @__PURE__ */ new WeakMap();
687
+ function getProgram3(gl) {
688
+ let p = programCache3.get(gl);
689
+ if (!p) {
690
+ p = createProgram(gl, VERT3, FRAG3);
691
+ programCache3.set(gl, p);
692
+ }
693
+ return p;
694
+ }
695
+ var DEFAULT_COLOR = "#3b82f6";
696
+ var counter3 = 0;
697
+ var Mesh = class {
698
+ constructor() {
699
+ this.data = [];
700
+ }
701
+ push(x, y, c) {
702
+ this.data.push(x, y, c[0], c[1], c[2], c[3]);
703
+ }
704
+ get count() {
705
+ return this.data.length / 6;
706
+ }
707
+ };
708
+ var BoxLayer = class {
709
+ constructor(gl, opts) {
710
+ this.xRef = 0;
711
+ this.yRef = 0;
712
+ this.xBounds = [0, 0];
713
+ this.yBounds = [0, 0];
714
+ this.id = `box-${counter3++}`;
715
+ this.gl = gl;
716
+ this.program = getProgram3(gl);
717
+ this.yAxis = opts.yAxis ?? "y";
718
+ const groups = opts.groups;
719
+ const width = opts.width ?? 0.6;
720
+ const hw = width / 2;
721
+ const showBox = opts.box !== false;
722
+ const showViolin = opts.violin === true;
723
+ this.xRef = groups.length ? groups[0].position : 0;
724
+ this.yRef = groups.length && groups[0].values.length ? groups[0].values[0] : 0;
725
+ const tris = new Mesh();
726
+ const lines = new Mesh();
727
+ const points = new Mesh();
728
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
729
+ const track = (x, y) => {
730
+ if (x < minX) minX = x;
731
+ if (x > maxX) maxX = x;
732
+ if (y < minY) minY = y;
733
+ if (y > maxY) maxY = y;
734
+ };
735
+ const ox = (v) => v - this.xRef;
736
+ const oy = (v) => v - this.yRef;
737
+ for (const g of groups) {
738
+ const cx = g.position;
739
+ const base = Array.isArray(g.color) ? g.color : parseColor(g.color ?? DEFAULT_COLOR);
740
+ const fill = [base[0], base[1], base[2], 0.35];
741
+ const stroke = [base[0], base[1], base[2], 1];
742
+ const s = boxStats(g.values);
743
+ track(cx - hw, s.whiskerLo);
744
+ track(cx + hw, s.whiskerHi);
745
+ if (showViolin) {
746
+ const lo = s.min, hi = s.max;
747
+ const d = kde(g.values, lo, hi, 48);
748
+ let maxD = 0;
749
+ for (let i = 0; i < d.ys.length; i++) maxD = Math.max(maxD, d.ys[i]);
750
+ maxD = maxD || 1;
751
+ for (let i = 0; i < d.xs.length - 1; i++) {
752
+ const w0 = d.ys[i] / maxD * hw;
753
+ const w1 = d.ys[i + 1] / maxD * hw;
754
+ const y0 = d.xs[i], y1 = d.xs[i + 1];
755
+ tris.push(ox(cx - w0), oy(y0), fill);
756
+ tris.push(ox(cx + w0), oy(y0), fill);
757
+ tris.push(ox(cx + w1), oy(y1), fill);
758
+ tris.push(ox(cx - w0), oy(y0), fill);
759
+ tris.push(ox(cx + w1), oy(y1), fill);
760
+ tris.push(ox(cx - w1), oy(y1), fill);
761
+ }
762
+ track(cx - hw, lo);
763
+ track(cx + hw, hi);
764
+ }
765
+ if (showBox) {
766
+ const x0 = ox(cx - hw), x1 = ox(cx + hw);
767
+ const yq1 = oy(s.q1), yq3 = oy(s.q3);
768
+ if (!showViolin) {
769
+ tris.push(x0, yq1, fill);
770
+ tris.push(x1, yq1, fill);
771
+ tris.push(x1, yq3, fill);
772
+ tris.push(x0, yq1, fill);
773
+ tris.push(x1, yq3, fill);
774
+ tris.push(x0, yq3, fill);
775
+ }
776
+ const edge = [
777
+ [x0, yq1, x1, yq1],
778
+ [x1, yq1, x1, yq3],
779
+ [x1, yq3, x0, yq3],
780
+ [x0, yq3, x0, yq1]
781
+ ];
782
+ for (const [ax, ay, bx, by] of edge) {
783
+ lines.push(ax, ay, stroke);
784
+ lines.push(bx, by, stroke);
785
+ }
786
+ const ym = oy(s.median);
787
+ lines.push(x0, ym, stroke);
788
+ lines.push(x1, ym, stroke);
789
+ const cxo = ox(cx);
790
+ lines.push(cxo, oy(s.q3), stroke);
791
+ lines.push(cxo, oy(s.whiskerHi), stroke);
792
+ lines.push(cxo, oy(s.q1), stroke);
793
+ lines.push(cxo, oy(s.whiskerLo), stroke);
794
+ const capHw = hw * 0.5;
795
+ lines.push(ox(cx - capHw), oy(s.whiskerHi), stroke);
796
+ lines.push(ox(cx + capHw), oy(s.whiskerHi), stroke);
797
+ lines.push(ox(cx - capHw), oy(s.whiskerLo), stroke);
798
+ lines.push(ox(cx + capHw), oy(s.whiskerLo), stroke);
799
+ for (const v of s.outliers) {
800
+ points.push(cxo, oy(v), stroke);
801
+ track(cx, v);
802
+ }
803
+ }
804
+ }
805
+ this.xBounds = [minX, maxX];
806
+ this.yBounds = [minY, maxY];
807
+ const all = [...tris.data, ...lines.data, ...points.data];
808
+ this.triCount = tris.count;
809
+ this.lineStart = tris.count;
810
+ this.lineCount = lines.count;
811
+ this.pointStart = tris.count + lines.count;
812
+ this.pointCount = points.count;
813
+ const vao = gl.createVertexArray();
814
+ const buffer = gl.createBuffer();
815
+ this.vao = vao;
816
+ this.buffer = buffer;
817
+ gl.bindVertexArray(vao);
818
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
819
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(all), gl.STATIC_DRAW);
820
+ gl.enableVertexAttribArray(0);
821
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 24, 0);
822
+ gl.enableVertexAttribArray(1);
823
+ gl.vertexAttribPointer(1, 4, gl.FLOAT, false, 24, 8);
824
+ gl.bindVertexArray(null);
825
+ this.uniforms = uniformLocations(gl, this.program, [...TRANSFORM_UNIFORMS, "uPointSize", "uIsPoint"]);
826
+ }
827
+ bounds() {
828
+ if (this.triCount + this.lineCount + this.pointCount === 0) return null;
829
+ return { x: this.xBounds, y: this.yBounds };
830
+ }
831
+ draw(state) {
832
+ const gl = state.gl;
833
+ gl.useProgram(this.program);
834
+ setTransformUniforms(gl, this.uniforms, state.x, state.y, this.xRef, this.yRef);
835
+ gl.uniform1f(this.uniforms.uPointSize, 5 * state.dpr);
836
+ gl.bindVertexArray(this.vao);
837
+ if (this.triCount > 0) {
838
+ gl.uniform1f(this.uniforms.uIsPoint, 0);
839
+ gl.drawArrays(gl.TRIANGLES, 0, this.triCount);
840
+ }
841
+ if (this.lineCount > 0) {
842
+ gl.uniform1f(this.uniforms.uIsPoint, 0);
843
+ gl.drawArrays(gl.LINES, this.lineStart, this.lineCount);
844
+ }
845
+ if (this.pointCount > 0) {
846
+ gl.uniform1f(this.uniforms.uIsPoint, 1);
847
+ gl.drawArrays(gl.POINTS, this.pointStart, this.pointCount);
848
+ }
849
+ gl.bindVertexArray(null);
850
+ }
851
+ dispose() {
852
+ this.gl.deleteVertexArray(this.vao);
853
+ this.gl.deleteBuffer(this.buffer);
854
+ }
855
+ };
856
+
857
+ // src/color/colormap.ts
858
+ var ANCHORS = {
859
+ viridis: [
860
+ [0.267, 5e-3, 0.329],
861
+ [0.283, 0.141, 0.458],
862
+ [0.254, 0.265, 0.53],
863
+ [0.207, 0.372, 0.553],
864
+ [0.164, 0.471, 0.558],
865
+ [0.128, 0.567, 0.551],
866
+ [0.135, 0.659, 0.518],
867
+ [0.267, 0.749, 0.441],
868
+ [0.478, 0.821, 0.318],
869
+ [0.741, 0.873, 0.15],
870
+ [0.993, 0.906, 0.144]
871
+ ],
872
+ plasma: [
873
+ [0.05, 0.03, 0.53],
874
+ [0.29, 0.01, 0.63],
875
+ [0.49, 0.01, 0.66],
876
+ [0.66, 0.13, 0.59],
877
+ [0.8, 0.28, 0.47],
878
+ [0.9, 0.43, 0.35],
879
+ [0.97, 0.6, 0.24],
880
+ [0.99, 0.78, 0.15],
881
+ [0.94, 0.98, 0.13]
882
+ ],
883
+ coolwarm: [
884
+ [0.23, 0.3, 0.75],
885
+ [0.55, 0.69, 0.98],
886
+ [0.87, 0.87, 0.87],
887
+ [0.96, 0.6, 0.48],
888
+ [0.71, 0.02, 0.15]
889
+ ],
890
+ grayscale: [
891
+ [0.05, 0.05, 0.05],
892
+ [0.95, 0.95, 0.95]
893
+ ]
894
+ };
895
+ function colormap(name = "viridis") {
896
+ const anchors = ANCHORS[name];
897
+ const last = anchors.length - 1;
898
+ return (t) => {
899
+ const clamped = t <= 0 ? 0 : t >= 1 ? 1 : t;
900
+ const pos = clamped * last;
901
+ const i = Math.min(last - 1, Math.floor(pos));
902
+ const f = pos - i;
903
+ const a = anchors[i];
904
+ const b = anchors[i + 1];
905
+ return [a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f, a[2] + (b[2] - a[2]) * f];
906
+ };
907
+ }
908
+
909
+ // src/layers/contour.ts
910
+ var VERT4 = (
911
+ /* glsl */
912
+ `#version 300 es
913
+ precision highp float;
914
+ layout(location = 0) in vec2 aPos;
915
+ layout(location = 1) in vec4 aColor;
916
+ ${TRANSFORM_GLSL}
917
+ out vec4 vColor;
918
+ void main() { vColor = aColor; gl_Position = vec4(dataToClip(aPos), 0.0, 1.0); }`
919
+ );
920
+ var FRAG4 = (
921
+ /* glsl */
922
+ `#version 300 es
923
+ precision highp float;
924
+ in vec4 vColor;
925
+ out vec4 outColor;
926
+ void main() { outColor = vec4(vColor.rgb * vColor.a, vColor.a); }`
927
+ );
928
+ var CASES = [
929
+ [],
930
+ [[3, 0]],
931
+ [[0, 1]],
932
+ [[3, 1]],
933
+ [[1, 2]],
934
+ [[3, 0], [1, 2]],
935
+ [[0, 2]],
936
+ [[3, 2]],
937
+ [[2, 3]],
938
+ [[2, 0]],
939
+ [[0, 1], [2, 3]],
940
+ [[2, 1]],
941
+ [[1, 3]],
942
+ [[1, 0]],
943
+ [[0, 3]],
944
+ []
945
+ ];
946
+ var programCache4 = /* @__PURE__ */ new WeakMap();
947
+ function getProgram4(gl) {
948
+ let p = programCache4.get(gl);
949
+ if (!p) {
950
+ p = createProgram(gl, VERT4, FRAG4);
951
+ programCache4.set(gl, p);
952
+ }
953
+ return p;
954
+ }
955
+ var counter4 = 0;
956
+ var ContourLayer = class {
957
+ constructor(gl, opts) {
958
+ this.id = `contour-${counter4++}`;
959
+ this.gl = gl;
960
+ this.program = getProgram4(gl);
961
+ this.yAxis = opts.yAxis ?? "y";
962
+ this.ext = opts.extent;
963
+ const { cols, rows, values } = opts;
964
+ const [x0, x1] = opts.extent.x, [y0, y1] = opts.extent.y;
965
+ this.xRef = x0;
966
+ this.yRef = y0;
967
+ let vmin = Infinity, vmax = -Infinity;
968
+ for (let i = 0; i < values.length; i++) {
969
+ const v = values[i];
970
+ if (v < vmin) vmin = v;
971
+ if (v > vmax) vmax = v;
972
+ }
973
+ const count = typeof opts.levels === "number" ? opts.levels : 8;
974
+ const levels = Array.isArray(opts.levels) ? opts.levels : Array.from({ length: count }, (_, i) => vmin + (vmax - vmin) * (i + 1) / (count + 1));
975
+ const cmap = colormap(opts.colormap ?? "viridis");
976
+ const fixed = opts.color != null ? Array.isArray(opts.color) ? opts.color : parseColor(opts.color) : null;
977
+ const gx = (c) => x0 + c / (cols - 1) * (x1 - x0) - this.xRef;
978
+ const gy = (r) => y0 + r / (rows - 1) * (y1 - y0) - this.yRef;
979
+ const at = (c, r) => values[r * cols + c];
980
+ const data = [];
981
+ const lspan = vmax - vmin || 1;
982
+ for (let li = 0; li < levels.length; li++) {
983
+ const L = levels[li];
984
+ const col = fixed ?? [...cmap((L - vmin) / lspan), 1];
985
+ for (let r = 0; r < rows - 1; r++) {
986
+ for (let c = 0; c < cols - 1; c++) {
987
+ const v0 = at(c, r), v1 = at(c + 1, r), v2 = at(c + 1, r + 1), v3 = at(c, r + 1);
988
+ const idx = (v0 >= L ? 1 : 0) | (v1 >= L ? 2 : 0) | (v2 >= L ? 4 : 0) | (v3 >= L ? 8 : 0);
989
+ const segs = CASES[idx];
990
+ if (segs.length === 0) continue;
991
+ const lerp = (t, ax, ay, bx, by) => [ax + (bx - ax) * t, ay + (by - ay) * t];
992
+ const edge = (e) => {
993
+ if (e === 0) return lerp((L - v0) / (v1 - v0 || 1e-9), gx(c), gy(r), gx(c + 1), gy(r));
994
+ if (e === 1) return lerp((L - v1) / (v2 - v1 || 1e-9), gx(c + 1), gy(r), gx(c + 1), gy(r + 1));
995
+ if (e === 2) return lerp((L - v2) / (v3 - v2 || 1e-9), gx(c + 1), gy(r + 1), gx(c), gy(r + 1));
996
+ return lerp((L - v3) / (v0 - v3 || 1e-9), gx(c), gy(r + 1), gx(c), gy(r));
997
+ };
998
+ for (const [ea, eb] of segs) {
999
+ const pa = edge(ea), pb = edge(eb);
1000
+ data.push(pa[0], pa[1], col[0], col[1], col[2], col[3]);
1001
+ data.push(pb[0], pb[1], col[0], col[1], col[2], col[3]);
1002
+ }
1003
+ }
1004
+ }
1005
+ }
1006
+ this.vertexCount = data.length / 6;
1007
+ const vao = gl.createVertexArray();
1008
+ const buffer = gl.createBuffer();
1009
+ this.vao = vao;
1010
+ this.buffer = buffer;
1011
+ gl.bindVertexArray(vao);
1012
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
1013
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
1014
+ gl.enableVertexAttribArray(0);
1015
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 24, 0);
1016
+ gl.enableVertexAttribArray(1);
1017
+ gl.vertexAttribPointer(1, 4, gl.FLOAT, false, 24, 8);
1018
+ gl.bindVertexArray(null);
1019
+ this.uniforms = uniformLocations(gl, this.program, [...TRANSFORM_UNIFORMS]);
1020
+ }
1021
+ bounds() {
1022
+ return { x: this.ext.x, y: this.ext.y };
1023
+ }
1024
+ draw(state) {
1025
+ if (this.vertexCount === 0) return;
1026
+ const gl = state.gl;
1027
+ gl.useProgram(this.program);
1028
+ setTransformUniforms(gl, this.uniforms, state.x, state.y, this.xRef, this.yRef);
1029
+ gl.bindVertexArray(this.vao);
1030
+ gl.drawArrays(gl.LINES, 0, this.vertexCount);
1031
+ gl.bindVertexArray(null);
1032
+ }
1033
+ dispose() {
1034
+ this.gl.deleteVertexArray(this.vao);
1035
+ this.gl.deleteBuffer(this.buffer);
1036
+ }
1037
+ };
1038
+
1039
+ // src/layers/heatmap.ts
1040
+ var VERT5 = (
1041
+ /* glsl */
1042
+ `#version 300 es
1043
+ precision highp float;
1044
+ layout(location = 0) in vec2 aPos; // offset data space
1045
+ layout(location = 1) in vec2 aUV;
1046
+ ${TRANSFORM_GLSL}
1047
+ out vec2 vUV;
1048
+ void main() {
1049
+ vUV = aUV;
1050
+ gl_Position = vec4(dataToClip(aPos), 0.0, 1.0);
1051
+ }`
1052
+ );
1053
+ var FRAG5 = (
1054
+ /* glsl */
1055
+ `#version 300 es
1056
+ precision highp float;
1057
+ in vec2 vUV;
1058
+ uniform sampler2D uTex;
1059
+ out vec4 outColor;
1060
+ void main() {
1061
+ vec4 c = texture(uTex, vUV);
1062
+ outColor = vec4(c.rgb * c.a, c.a);
1063
+ }`
1064
+ );
1065
+ var programCache5 = /* @__PURE__ */ new WeakMap();
1066
+ function getProgram5(gl) {
1067
+ let p = programCache5.get(gl);
1068
+ if (!p) {
1069
+ p = createProgram(gl, VERT5, FRAG5);
1070
+ programCache5.set(gl, p);
1071
+ }
1072
+ return p;
1073
+ }
1074
+ var counter5 = 0;
1075
+ var HeatmapLayer = class {
1076
+ constructor(gl, opts) {
1077
+ this.id = `heatmap-${counter5++}`;
1078
+ this.gl = gl;
1079
+ this.program = getProgram5(gl);
1080
+ this.yAxis = opts.yAxis ?? "y";
1081
+ this.ext = opts.extent;
1082
+ const [x0, x1] = opts.extent.x;
1083
+ const [y0, y1] = opts.extent.y;
1084
+ this.xRef = x0;
1085
+ this.yRef = y0;
1086
+ const { cols, rows, values } = opts;
1087
+ const cmap = colormap(opts.colormap ?? "viridis");
1088
+ let lo = opts.domain?.[0] ?? Infinity;
1089
+ let hi = opts.domain?.[1] ?? -Infinity;
1090
+ if (!opts.domain) {
1091
+ for (let i = 0; i < values.length; i++) {
1092
+ const v = values[i];
1093
+ if (v < lo) lo = v;
1094
+ if (v > hi) hi = v;
1095
+ }
1096
+ }
1097
+ const span = hi - lo || 1;
1098
+ const pixels = new Uint8Array(cols * rows * 4);
1099
+ for (let i = 0; i < cols * rows; i++) {
1100
+ const [r, g, b] = cmap((values[i] - lo) / span);
1101
+ pixels[i * 4] = Math.round(r * 255);
1102
+ pixels[i * 4 + 1] = Math.round(g * 255);
1103
+ pixels[i * 4 + 2] = Math.round(b * 255);
1104
+ pixels[i * 4 + 3] = 255;
1105
+ }
1106
+ const texture = gl.createTexture();
1107
+ this.texture = texture;
1108
+ gl.bindTexture(gl.TEXTURE_2D, texture);
1109
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
1110
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, cols, rows, 0, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
1111
+ const filter = opts.smooth === false ? gl.NEAREST : gl.LINEAR;
1112
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
1113
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
1114
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1115
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1116
+ gl.bindTexture(gl.TEXTURE_2D, null);
1117
+ const data = new Float32Array([
1118
+ x0 - this.xRef,
1119
+ y0 - this.yRef,
1120
+ 0,
1121
+ 0,
1122
+ x1 - this.xRef,
1123
+ y0 - this.yRef,
1124
+ 1,
1125
+ 0,
1126
+ x1 - this.xRef,
1127
+ y1 - this.yRef,
1128
+ 1,
1129
+ 1,
1130
+ x0 - this.xRef,
1131
+ y0 - this.yRef,
1132
+ 0,
1133
+ 0,
1134
+ x1 - this.xRef,
1135
+ y1 - this.yRef,
1136
+ 1,
1137
+ 1,
1138
+ x0 - this.xRef,
1139
+ y1 - this.yRef,
1140
+ 0,
1141
+ 1
1142
+ ]);
1143
+ const vao = gl.createVertexArray();
1144
+ const buffer = gl.createBuffer();
1145
+ this.vao = vao;
1146
+ this.buffer = buffer;
1147
+ gl.bindVertexArray(vao);
1148
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
1149
+ gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
1150
+ gl.enableVertexAttribArray(0);
1151
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 16, 0);
1152
+ gl.enableVertexAttribArray(1);
1153
+ gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 16, 8);
1154
+ gl.bindVertexArray(null);
1155
+ this.uniforms = uniformLocations(gl, this.program, [...TRANSFORM_UNIFORMS, "uTex"]);
1156
+ }
1157
+ bounds() {
1158
+ return { x: this.ext.x, y: this.ext.y };
1159
+ }
1160
+ draw(state) {
1161
+ const gl = state.gl;
1162
+ gl.useProgram(this.program);
1163
+ setTransformUniforms(gl, this.uniforms, state.x, state.y, this.xRef, this.yRef);
1164
+ gl.activeTexture(gl.TEXTURE0);
1165
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
1166
+ gl.uniform1i(this.uniforms.uTex, 0);
1167
+ gl.bindVertexArray(this.vao);
1168
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
1169
+ gl.bindVertexArray(null);
1170
+ }
1171
+ dispose() {
1172
+ this.gl.deleteVertexArray(this.vao);
1173
+ this.gl.deleteBuffer(this.buffer);
1174
+ this.gl.deleteTexture(this.texture);
1175
+ }
1176
+ };
1177
+
1178
+ // src/layers/hexbin.ts
1179
+ var VERT6 = (
1180
+ /* glsl */
1181
+ `#version 300 es
1182
+ precision highp float;
1183
+ layout(location = 0) in vec2 aCorner; // unit hexagon (data-unit offsets)
1184
+ layout(location = 1) in vec2 aCenter; // offset data space
1185
+ layout(location = 2) in vec4 aColor;
1186
+ uniform float uRadius;
1187
+ ${TRANSFORM_GLSL}
1188
+ out vec4 vColor;
1189
+ void main() {
1190
+ vColor = aColor;
1191
+ gl_Position = vec4(dataToClip(aCenter + aCorner * uRadius), 0.0, 1.0);
1192
+ }`
1193
+ );
1194
+ var FRAG6 = (
1195
+ /* glsl */
1196
+ `#version 300 es
1197
+ precision highp float;
1198
+ in vec4 vColor;
1199
+ out vec4 outColor;
1200
+ void main() { outColor = vec4(vColor.rgb * vColor.a, vColor.a); }`
1201
+ );
1202
+ var HEX = (() => {
1203
+ const verts = [];
1204
+ const pt = (a) => [Math.cos(a), Math.sin(a)];
1205
+ for (let i = 0; i < 6; i++) {
1206
+ const a0 = Math.PI / 3 * i + Math.PI / 6;
1207
+ const a1 = Math.PI / 3 * (i + 1) + Math.PI / 6;
1208
+ const [x0, y0] = pt(a0), [x1, y1] = pt(a1);
1209
+ verts.push(0, 0, x0, y0, x1, y1);
1210
+ }
1211
+ return new Float32Array(verts);
1212
+ })();
1213
+ var programCache6 = /* @__PURE__ */ new WeakMap();
1214
+ function getProgram6(gl) {
1215
+ let p = programCache6.get(gl);
1216
+ if (!p) {
1217
+ p = createProgram(gl, VERT6, FRAG6);
1218
+ programCache6.set(gl, p);
1219
+ }
1220
+ return p;
1221
+ }
1222
+ var counter6 = 0;
1223
+ var HexbinLayer = class {
1224
+ constructor(gl, opts) {
1225
+ this.buffers = [];
1226
+ this.xRef = 0;
1227
+ this.yRef = 0;
1228
+ this.xBounds = [0, 0];
1229
+ this.yBounds = [0, 0];
1230
+ this.id = `hexbin-${counter6++}`;
1231
+ this.gl = gl;
1232
+ this.program = getProgram6(gl);
1233
+ this.yAxis = opts.yAxis ?? "y";
1234
+ const n = Math.min(opts.x.length, opts.y.length);
1235
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
1236
+ for (let i = 0; i < n; i++) {
1237
+ const x = opts.x[i], y = opts.y[i];
1238
+ if (x < minX) minX = x;
1239
+ if (x > maxX) maxX = x;
1240
+ if (y < minY) minY = y;
1241
+ if (y > maxY) maxY = y;
1242
+ }
1243
+ this.xBounds = [minX, maxX];
1244
+ this.yBounds = [minY, maxY];
1245
+ const r = opts.radius ?? ((maxX - minX) / 30 || 1);
1246
+ this.radius = r;
1247
+ const dx = r * 2 * Math.sin(Math.PI / 3);
1248
+ const dy = r * 1.5;
1249
+ const cells = /* @__PURE__ */ new Map();
1250
+ let maxCount = 1;
1251
+ for (let i = 0; i < n; i++) {
1252
+ const px = opts.x[i], py = opts.y[i];
1253
+ const pj = Math.round(py / dy);
1254
+ const pi = Math.round(px / dx - (pj & 1) / 2);
1255
+ const key = `${pi},${pj}`;
1256
+ let cell = cells.get(key);
1257
+ if (!cell) {
1258
+ cell = { cx: (pi + (pj & 1) / 2) * dx, cy: pj * dy, count: 0 };
1259
+ cells.set(key, cell);
1260
+ }
1261
+ cell.count++;
1262
+ if (cell.count > maxCount) maxCount = cell.count;
1263
+ }
1264
+ this.cellCount = cells.size;
1265
+ this.xRef = minX;
1266
+ this.yRef = minY;
1267
+ const centers = new Float32Array(this.cellCount * 2);
1268
+ const colors = new Float32Array(this.cellCount * 4);
1269
+ const cmap = colormap(opts.colormap ?? "viridis");
1270
+ const lo = opts.domain?.[0] ?? 1;
1271
+ const hi = opts.domain?.[1] ?? maxCount;
1272
+ const span = hi - lo || 1;
1273
+ let k = 0;
1274
+ for (const cell of cells.values()) {
1275
+ centers[k * 2] = cell.cx - this.xRef;
1276
+ centers[k * 2 + 1] = cell.cy - this.yRef;
1277
+ const [cr, cg, cb] = cmap((cell.count - lo) / span);
1278
+ colors[k * 4] = cr;
1279
+ colors[k * 4 + 1] = cg;
1280
+ colors[k * 4 + 2] = cb;
1281
+ colors[k * 4 + 3] = 1;
1282
+ k++;
1283
+ }
1284
+ const vao = gl.createVertexArray();
1285
+ this.vao = vao;
1286
+ gl.bindVertexArray(vao);
1287
+ const hexBuf = gl.createBuffer();
1288
+ gl.bindBuffer(gl.ARRAY_BUFFER, hexBuf);
1289
+ gl.bufferData(gl.ARRAY_BUFFER, HEX, gl.STATIC_DRAW);
1290
+ gl.enableVertexAttribArray(0);
1291
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
1292
+ const centerBuf = gl.createBuffer();
1293
+ gl.bindBuffer(gl.ARRAY_BUFFER, centerBuf);
1294
+ gl.bufferData(gl.ARRAY_BUFFER, centers, gl.STATIC_DRAW);
1295
+ gl.enableVertexAttribArray(1);
1296
+ gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 0, 0);
1297
+ gl.vertexAttribDivisor(1, 1);
1298
+ const colorBuf = gl.createBuffer();
1299
+ gl.bindBuffer(gl.ARRAY_BUFFER, colorBuf);
1300
+ gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
1301
+ gl.enableVertexAttribArray(2);
1302
+ gl.vertexAttribPointer(2, 4, gl.FLOAT, false, 0, 0);
1303
+ gl.vertexAttribDivisor(2, 1);
1304
+ gl.bindVertexArray(null);
1305
+ this.buffers = [hexBuf, centerBuf, colorBuf];
1306
+ this.uniforms = uniformLocations(gl, this.program, [...TRANSFORM_UNIFORMS, "uRadius"]);
1307
+ }
1308
+ bounds() {
1309
+ if (this.cellCount === 0) return null;
1310
+ return { x: this.xBounds, y: this.yBounds };
1311
+ }
1312
+ draw(state) {
1313
+ if (this.cellCount === 0) return;
1314
+ const gl = state.gl;
1315
+ gl.useProgram(this.program);
1316
+ setTransformUniforms(gl, this.uniforms, state.x, state.y, this.xRef, this.yRef);
1317
+ gl.uniform1f(this.uniforms.uRadius, this.radius);
1318
+ gl.bindVertexArray(this.vao);
1319
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 18, this.cellCount);
1320
+ gl.bindVertexArray(null);
1321
+ }
1322
+ dispose() {
1323
+ this.gl.deleteVertexArray(this.vao);
1324
+ for (const b of this.buffers) this.gl.deleteBuffer(b);
1325
+ }
1326
+ };
1327
+
1328
+ // src/layers/line.ts
1329
+ var VERT7 = (
1330
+ /* glsl */
1331
+ `#version 300 es
1332
+ precision highp float;
1333
+ layout(location = 0) in vec2 aCorner; // (along 0..1, side -1..1)
1334
+ layout(location = 1) in vec2 aP0;
1335
+ layout(location = 2) in vec2 aP1;
1336
+ uniform vec2 uResolution;
1337
+ uniform float uWidth;
1338
+ uniform float uRound;
1339
+ ${TRANSFORM_GLSL}
1340
+ out vec2 vPix;
1341
+ out vec2 vS0;
1342
+ out vec2 vS1;
1343
+ void main() {
1344
+ vec2 s0 = (dataToClip(aP0) * 0.5 + 0.5) * uResolution;
1345
+ vec2 s1 = (dataToClip(aP1) * 0.5 + 0.5) * uResolution;
1346
+ vec2 d = s1 - s0;
1347
+ float len = length(d);
1348
+ vec2 dir = len > 1e-6 ? d / len : vec2(1.0, 0.0);
1349
+ vec2 nrm = vec2(-dir.y, dir.x);
1350
+ float hw = uWidth * 0.5 + 1.5; // half width + AA margin
1351
+ float ext = uRound > 0.5 ? hw : 0.0; // extend for round caps
1352
+ vec2 endpoint = mix(s0, s1, aCorner.x);
1353
+ vec2 outward = (aCorner.x < 0.5 ? -dir : dir) * ext;
1354
+ vec2 pos = endpoint + outward + nrm * (aCorner.y * hw);
1355
+ vPix = pos; vS0 = s0; vS1 = s1;
1356
+ gl_Position = vec4((pos / uResolution) * 2.0 - 1.0, 0.0, 1.0);
1357
+ }`
1358
+ );
1359
+ var FRAG7 = (
1360
+ /* glsl */
1361
+ `#version 300 es
1362
+ precision highp float;
1363
+ in vec2 vPix;
1364
+ in vec2 vS0;
1365
+ in vec2 vS1;
1366
+ uniform vec4 uColor;
1367
+ uniform float uWidth;
1368
+ uniform float uRound;
1369
+ out vec4 outColor;
1370
+ void main() {
1371
+ vec2 pa = vPix - vS0;
1372
+ vec2 ba = vS1 - vS0;
1373
+ float bb = dot(ba, ba);
1374
+ float t = bb > 1e-6 ? dot(pa, ba) / bb : 0.0;
1375
+ float d;
1376
+ if (uRound > 0.5) {
1377
+ d = length(pa - ba * clamp(t, 0.0, 1.0)); // round caps/joins
1378
+ } else {
1379
+ if (t < 0.0 || t > 1.0) discard; // butt caps
1380
+ d = length(pa - ba * t);
1381
+ }
1382
+ float hw = uWidth * 0.5;
1383
+ float alpha = 1.0 - smoothstep(hw - 1.0, hw + 1.0, d);
1384
+ if (alpha <= 0.0) discard;
1385
+ outColor = vec4(uColor.rgb * uColor.a, uColor.a) * alpha;
1386
+ }`
1387
+ );
1388
+ var CORNERS2 = new Float32Array([0, -1, 1, -1, 1, 1, 0, -1, 1, 1, 0, 1]);
1389
+ var programCache7 = /* @__PURE__ */ new WeakMap();
1390
+ function getProgram7(gl) {
1391
+ let p = programCache7.get(gl);
1392
+ if (!p) {
1393
+ p = createProgram(gl, VERT7, FRAG7);
1394
+ programCache7.set(gl, p);
1395
+ }
1396
+ return p;
1397
+ }
1398
+ function stepExpand(xs, ys, n, mode) {
1399
+ const ox = [], oy = [];
1400
+ for (let i = 0; i < n - 1; i++) {
1401
+ const x0 = xs[i], y0 = ys[i], x1 = xs[i + 1], y1 = ys[i + 1];
1402
+ if (mode === "center") {
1403
+ const xm = (x0 + x1) / 2;
1404
+ ox.push(x0, xm, xm);
1405
+ oy.push(y0, y0, y1);
1406
+ } else if (mode === "after") {
1407
+ ox.push(x0, x1);
1408
+ oy.push(y0, y0);
1409
+ } else {
1410
+ ox.push(x0, x0);
1411
+ oy.push(y0, y1);
1412
+ }
1413
+ }
1414
+ ox.push(xs[n - 1]);
1415
+ oy.push(ys[n - 1]);
1416
+ return { xs: Float64Array.from(ox), ys: Float64Array.from(oy) };
1417
+ }
1418
+ function upperBound(a, v) {
1419
+ let lo = 0, hi = a.length;
1420
+ while (lo < hi) {
1421
+ const m = lo + hi >> 1;
1422
+ if (a[m] <= v) lo = m + 1;
1423
+ else hi = m;
1424
+ }
1425
+ return lo;
1426
+ }
1427
+ var counter7 = 0;
1428
+ var LineLayer = class {
1429
+ constructor(gl, opts) {
1430
+ this.xRef = 0;
1431
+ this.yRef = 0;
1432
+ this.xBounds = [0, 0];
1433
+ this.yBounds = [0, 0];
1434
+ this.decKey = "";
1435
+ this.decSegments = 0;
1436
+ this.id = `line-${counter7++}`;
1437
+ this.gl = gl;
1438
+ this.program = getProgram7(gl);
1439
+ this.width = opts.width ?? 1.5;
1440
+ this.round = (opts.join ?? "round") === "round";
1441
+ this.decimateOn = opts.decimate !== false;
1442
+ this.step = opts.step;
1443
+ const colorInput = opts.color ?? "#3b82f6";
1444
+ this.color = Array.isArray(colorInput) ? colorInput : parseColor(colorInput);
1445
+ this.colorCss = typeof colorInput === "string" ? colorInput : toColorCss(this.color);
1446
+ this.name = opts.name ?? this.id;
1447
+ this.yAxis = opts.yAxis ?? "y";
1448
+ let n = Math.min(opts.x.length, opts.y.length);
1449
+ let xs = opts.x, ys = opts.y;
1450
+ if (opts.step && n >= 2) {
1451
+ const e = stepExpand(opts.x, opts.y, n, opts.step);
1452
+ xs = e.xs;
1453
+ ys = e.ys;
1454
+ n = e.xs.length;
1455
+ }
1456
+ this.xs = new Float64Array(n);
1457
+ this.ys = new Float64Array(n);
1458
+ this.xRef = n > 0 ? xs[0] : 0;
1459
+ this.yRef = n > 0 ? ys[0] : 0;
1460
+ const data = new Float32Array(n * 2);
1461
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity, mono = true;
1462
+ for (let i = 0; i < n; i++) {
1463
+ const x = xs[i], y = ys[i];
1464
+ this.xs[i] = x;
1465
+ this.ys[i] = y;
1466
+ data[i * 2] = x - this.xRef;
1467
+ data[i * 2 + 1] = y - this.yRef;
1468
+ if (i > 0 && x < xs[i - 1]) mono = false;
1469
+ if (x < minX) minX = x;
1470
+ if (x > maxX) maxX = x;
1471
+ if (y < minY) minY = y;
1472
+ if (y > maxY) maxY = y;
1473
+ }
1474
+ this.xBounds = [minX, maxX];
1475
+ this.yBounds = [minY, maxY];
1476
+ this.count = n;
1477
+ this.monotonic = mono;
1478
+ this.cornerBuf = gl.createBuffer();
1479
+ this.posBuf = gl.createBuffer();
1480
+ this.decBuf = gl.createBuffer();
1481
+ this.fullVao = gl.createVertexArray();
1482
+ this.decVao = gl.createVertexArray();
1483
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.cornerBuf);
1484
+ gl.bufferData(gl.ARRAY_BUFFER, CORNERS2, gl.STATIC_DRAW);
1485
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.posBuf);
1486
+ gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
1487
+ this.configureVao(this.fullVao, this.posBuf);
1488
+ this.configureVao(this.decVao, this.decBuf);
1489
+ this.uniforms = uniformLocations(gl, this.program, [
1490
+ ...TRANSFORM_UNIFORMS,
1491
+ "uColor",
1492
+ "uResolution",
1493
+ "uWidth",
1494
+ "uRound"
1495
+ ]);
1496
+ }
1497
+ configureVao(vao, pointBuf) {
1498
+ const gl = this.gl;
1499
+ gl.bindVertexArray(vao);
1500
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.cornerBuf);
1501
+ gl.enableVertexAttribArray(0);
1502
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
1503
+ gl.bindBuffer(gl.ARRAY_BUFFER, pointBuf);
1504
+ gl.enableVertexAttribArray(1);
1505
+ gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 8, 0);
1506
+ gl.vertexAttribDivisor(1, 1);
1507
+ gl.enableVertexAttribArray(2);
1508
+ gl.vertexAttribPointer(2, 2, gl.FLOAT, false, 8, 8);
1509
+ gl.vertexAttribDivisor(2, 1);
1510
+ gl.bindVertexArray(null);
1511
+ }
1512
+ bounds() {
1513
+ if (this.count === 0) return null;
1514
+ return { x: this.xBounds, y: this.yBounds };
1515
+ }
1516
+ nearestByX(x) {
1517
+ if (this.count === 0) return null;
1518
+ let best = 0, bestDist = Infinity;
1519
+ for (let i = 0; i < this.count; i++) {
1520
+ const d = Math.abs(this.xs[i] - x);
1521
+ if (d < bestDist) {
1522
+ bestDist = d;
1523
+ best = i;
1524
+ }
1525
+ }
1526
+ return { x: this.xs[best], y: this.ys[best], index: best };
1527
+ }
1528
+ /** Replace the series data and re-upload the GPU buffer (for streaming). */
1529
+ setData(x, y) {
1530
+ let n = Math.min(x.length, y.length);
1531
+ let xs = x, ys = y;
1532
+ if (this.step && n >= 2) {
1533
+ const e = stepExpand(x, y, n, this.step);
1534
+ xs = e.xs;
1535
+ ys = e.ys;
1536
+ n = e.xs.length;
1537
+ }
1538
+ this.xs = new Float64Array(n);
1539
+ this.ys = new Float64Array(n);
1540
+ this.xRef = n > 0 ? xs[0] : 0;
1541
+ this.yRef = n > 0 ? ys[0] : 0;
1542
+ const data = new Float32Array(n * 2);
1543
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity, mono = true;
1544
+ for (let i = 0; i < n; i++) {
1545
+ const vx = xs[i], vy = ys[i];
1546
+ this.xs[i] = vx;
1547
+ this.ys[i] = vy;
1548
+ data[i * 2] = vx - this.xRef;
1549
+ data[i * 2 + 1] = vy - this.yRef;
1550
+ if (i > 0 && vx < xs[i - 1]) mono = false;
1551
+ if (vx < minX) minX = vx;
1552
+ if (vx > maxX) maxX = vx;
1553
+ if (vy < minY) minY = vy;
1554
+ if (vy > maxY) maxY = vy;
1555
+ }
1556
+ this.xBounds = [minX, maxX];
1557
+ this.yBounds = [minY, maxY];
1558
+ this.count = n;
1559
+ this.monotonic = mono;
1560
+ this.decKey = "";
1561
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.posBuf);
1562
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, data, this.gl.DYNAMIC_DRAW);
1563
+ }
1564
+ /**
1565
+ * Rebuild the min/max-decimated buffer for the visible x-window if it changed.
1566
+ * Returns the segment count to draw from `decVao`, or null to draw everything.
1567
+ */
1568
+ decimate(x, cols) {
1569
+ if (!this.decimateOn || !this.monotonic || this.count < 4 * cols) return null;
1570
+ const target = Math.max(2, cols * 2);
1571
+ let i0 = upperBound(this.xs, x.lo) - 1;
1572
+ let i1 = upperBound(this.xs, x.hi);
1573
+ i0 = Math.max(0, i0);
1574
+ i1 = Math.min(this.count - 1, i1);
1575
+ const visN = i1 - i0;
1576
+ if (visN <= target * 1.5) return null;
1577
+ const key = `${i0}:${i1}:${target}`;
1578
+ if (key === this.decKey) return this.decSegments;
1579
+ this.decKey = key;
1580
+ const out = [];
1581
+ const push = (i) => out.push(this.xs[i] - this.xRef, this.ys[i] - this.yRef);
1582
+ push(i0);
1583
+ for (let b = 0; b < cols; b++) {
1584
+ const lo = i0 + Math.floor(visN * b / cols);
1585
+ const hi = i0 + Math.floor(visN * (b + 1) / cols);
1586
+ if (hi <= lo) continue;
1587
+ let iMin = lo, iMax = lo;
1588
+ for (let i = lo; i < hi; i++) {
1589
+ if (this.ys[i] < this.ys[iMin]) iMin = i;
1590
+ if (this.ys[i] > this.ys[iMax]) iMax = i;
1591
+ }
1592
+ if (iMin < iMax) {
1593
+ push(iMin);
1594
+ push(iMax);
1595
+ } else {
1596
+ push(iMax);
1597
+ push(iMin);
1598
+ }
1599
+ }
1600
+ push(i1);
1601
+ const gl = this.gl;
1602
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.decBuf);
1603
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(out), gl.DYNAMIC_DRAW);
1604
+ this.decSegments = out.length / 2 - 1;
1605
+ return this.decSegments;
1606
+ }
1607
+ draw(state) {
1608
+ if (this.count < 2) return;
1609
+ const gl = state.gl;
1610
+ const decSegs = this.decimate(state.x, Math.max(1, Math.round(state.pixelWidth)));
1611
+ const vao = decSegs != null ? this.decVao : this.fullVao;
1612
+ const segments = decSegs != null ? decSegs : this.count - 1;
1613
+ if (segments < 1) return;
1614
+ gl.useProgram(this.program);
1615
+ setTransformUniforms(gl, this.uniforms, state.x, state.y, this.xRef, this.yRef);
1616
+ gl.uniform4f(this.uniforms.uColor, this.color[0], this.color[1], this.color[2], this.color[3]);
1617
+ gl.uniform2f(this.uniforms.uResolution, state.pixelWidth, state.pixelHeight);
1618
+ gl.uniform1f(this.uniforms.uWidth, this.width * state.dpr);
1619
+ gl.uniform1f(this.uniforms.uRound, this.round ? 1 : 0);
1620
+ gl.bindVertexArray(vao);
1621
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, segments);
1622
+ gl.bindVertexArray(null);
1623
+ }
1624
+ dispose() {
1625
+ const gl = this.gl;
1626
+ gl.deleteVertexArray(this.fullVao);
1627
+ gl.deleteVertexArray(this.decVao);
1628
+ gl.deleteBuffer(this.cornerBuf);
1629
+ gl.deleteBuffer(this.posBuf);
1630
+ gl.deleteBuffer(this.decBuf);
1631
+ }
1632
+ };
1633
+
1634
+ // src/layers/scatter.ts
1635
+ var VERT8 = (
1636
+ /* glsl */
1637
+ `#version 300 es
1638
+ precision highp float;
1639
+ layout(location = 0) in vec2 aCorner; // unit quad [-1,1]^2
1640
+ layout(location = 1) in vec2 aPos; // point (offset data space)
1641
+ layout(location = 2) in vec4 aColor; // per-point color (used if uUseVertexColor>0.5)
1642
+ uniform vec2 uResolution;
1643
+ uniform float uSize; // radius in device px
1644
+ ${TRANSFORM_GLSL}
1645
+ out vec2 vLocal;
1646
+ out vec4 vColor;
1647
+ void main() {
1648
+ vec2 center = (dataToClip(aPos) * 0.5 + 0.5) * uResolution;
1649
+ vec2 pos = center + aCorner * uSize;
1650
+ vLocal = aCorner;
1651
+ vColor = aColor;
1652
+ gl_Position = vec4((pos / uResolution) * 2.0 - 1.0, 0.0, 1.0);
1653
+ }`
1654
+ );
1655
+ var FRAG8 = (
1656
+ /* glsl */
1657
+ `#version 300 es
1658
+ precision highp float;
1659
+ in vec2 vLocal;
1660
+ in vec4 vColor;
1661
+ uniform vec4 uColor;
1662
+ uniform float uUseVertexColor;
1663
+ out vec4 outColor;
1664
+ void main() {
1665
+ float r = length(vLocal);
1666
+ if (r > 1.0) discard;
1667
+ float alpha = smoothstep(1.0, 1.0 - 0.15, r); // soft edge
1668
+ vec4 c = uUseVertexColor > 0.5 ? vColor : uColor;
1669
+ outColor = vec4(c.rgb * c.a * alpha, c.a * alpha);
1670
+ }`
1671
+ );
1672
+ var CORNERS3 = new Float32Array([-1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1]);
1673
+ var programCache8 = /* @__PURE__ */ new WeakMap();
1674
+ function getProgram8(gl) {
1675
+ let p = programCache8.get(gl);
1676
+ if (!p) {
1677
+ p = createProgram(gl, VERT8, FRAG8);
1678
+ programCache8.set(gl, p);
1679
+ }
1680
+ return p;
1681
+ }
1682
+ var counter8 = 0;
1683
+ var ScatterLayer = class {
1684
+ constructor(gl, opts) {
1685
+ this.buffers = [];
1686
+ this.xRef = 0;
1687
+ this.yRef = 0;
1688
+ this.xBounds = [0, 0];
1689
+ this.yBounds = [0, 0];
1690
+ this.id = `scatter-${counter8++}`;
1691
+ this.gl = gl;
1692
+ this.program = getProgram8(gl);
1693
+ this.size = opts.size ?? 5;
1694
+ const colorInput = opts.color ?? "#3b82f6";
1695
+ this.color = Array.isArray(colorInput) ? colorInput : parseColor(colorInput);
1696
+ this.colorCss = typeof colorInput === "string" ? colorInput : toColorCss(this.color);
1697
+ this.name = opts.name ?? this.id;
1698
+ this.yAxis = opts.yAxis ?? "y";
1699
+ this.useVertexColor = opts.colorBy != null;
1700
+ const n = Math.min(opts.x.length, opts.y.length);
1701
+ this.count = n;
1702
+ this.xs = new Float64Array(n);
1703
+ this.ys = new Float64Array(n);
1704
+ this.xRef = n > 0 ? opts.x[0] : 0;
1705
+ this.yRef = n > 0 ? opts.y[0] : 0;
1706
+ const pos = new Float32Array(n * 2);
1707
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
1708
+ for (let i = 0; i < n; i++) {
1709
+ const x = opts.x[i], y = opts.y[i];
1710
+ this.xs[i] = x;
1711
+ this.ys[i] = y;
1712
+ pos[i * 2] = x - this.xRef;
1713
+ pos[i * 2 + 1] = y - this.yRef;
1714
+ if (x < minX) minX = x;
1715
+ if (x > maxX) maxX = x;
1716
+ if (y < minY) minY = y;
1717
+ if (y > maxY) maxY = y;
1718
+ }
1719
+ this.xBounds = [minX, maxX];
1720
+ this.yBounds = [minY, maxY];
1721
+ const colors = new Float32Array(n * 4);
1722
+ if (opts.colorBy) {
1723
+ const vals = opts.colorBy.values;
1724
+ const cmap = colormap(opts.colorBy.colormap ?? "viridis");
1725
+ let lo = opts.colorBy.domain?.[0] ?? Infinity;
1726
+ let hi = opts.colorBy.domain?.[1] ?? -Infinity;
1727
+ if (!opts.colorBy.domain) {
1728
+ for (let i = 0; i < n; i++) {
1729
+ const v = vals[i];
1730
+ if (v < lo) lo = v;
1731
+ if (v > hi) hi = v;
1732
+ }
1733
+ }
1734
+ const span = hi - lo || 1;
1735
+ for (let i = 0; i < n; i++) {
1736
+ const [r, g, b] = cmap((vals[i] - lo) / span);
1737
+ colors[i * 4] = r;
1738
+ colors[i * 4 + 1] = g;
1739
+ colors[i * 4 + 2] = b;
1740
+ colors[i * 4 + 3] = 1;
1741
+ }
1742
+ }
1743
+ const vao = gl.createVertexArray();
1744
+ this.vao = vao;
1745
+ gl.bindVertexArray(vao);
1746
+ const cornerBuf = gl.createBuffer();
1747
+ gl.bindBuffer(gl.ARRAY_BUFFER, cornerBuf);
1748
+ gl.bufferData(gl.ARRAY_BUFFER, CORNERS3, gl.STATIC_DRAW);
1749
+ gl.enableVertexAttribArray(0);
1750
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
1751
+ const posBuf = gl.createBuffer();
1752
+ gl.bindBuffer(gl.ARRAY_BUFFER, posBuf);
1753
+ gl.bufferData(gl.ARRAY_BUFFER, pos, gl.STATIC_DRAW);
1754
+ gl.enableVertexAttribArray(1);
1755
+ gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 0, 0);
1756
+ gl.vertexAttribDivisor(1, 1);
1757
+ const colorBuf = gl.createBuffer();
1758
+ gl.bindBuffer(gl.ARRAY_BUFFER, colorBuf);
1759
+ gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
1760
+ gl.enableVertexAttribArray(2);
1761
+ gl.vertexAttribPointer(2, 4, gl.FLOAT, false, 0, 0);
1762
+ gl.vertexAttribDivisor(2, 1);
1763
+ gl.bindVertexArray(null);
1764
+ this.buffers = [cornerBuf, posBuf, colorBuf];
1765
+ this.uniforms = uniformLocations(gl, this.program, [
1766
+ ...TRANSFORM_UNIFORMS,
1767
+ "uColor",
1768
+ "uResolution",
1769
+ "uSize",
1770
+ "uUseVertexColor"
1771
+ ]);
1772
+ }
1773
+ bounds() {
1774
+ if (this.count === 0) return null;
1775
+ return { x: this.xBounds, y: this.yBounds };
1776
+ }
1777
+ nearestByX(x) {
1778
+ if (this.count === 0) return null;
1779
+ let best = 0, bestDist = Infinity;
1780
+ for (let i = 0; i < this.count; i++) {
1781
+ const d = Math.abs(this.xs[i] - x);
1782
+ if (d < bestDist) {
1783
+ bestDist = d;
1784
+ best = i;
1785
+ }
1786
+ }
1787
+ return { x: this.xs[best], y: this.ys[best], index: best };
1788
+ }
1789
+ /** Replace point positions and re-upload (for streaming). Keeps uniform color. */
1790
+ setData(x, y) {
1791
+ const n = Math.min(x.length, y.length);
1792
+ this.count = n;
1793
+ this.xs = new Float64Array(n);
1794
+ this.ys = new Float64Array(n);
1795
+ this.xRef = n > 0 ? x[0] : 0;
1796
+ this.yRef = n > 0 ? y[0] : 0;
1797
+ const pos = new Float32Array(n * 2);
1798
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
1799
+ for (let i = 0; i < n; i++) {
1800
+ const vx = x[i], vy = y[i];
1801
+ this.xs[i] = vx;
1802
+ this.ys[i] = vy;
1803
+ pos[i * 2] = vx - this.xRef;
1804
+ pos[i * 2 + 1] = vy - this.yRef;
1805
+ if (vx < minX) minX = vx;
1806
+ if (vx > maxX) maxX = vx;
1807
+ if (vy < minY) minY = vy;
1808
+ if (vy > maxY) maxY = vy;
1809
+ }
1810
+ this.xBounds = [minX, maxX];
1811
+ this.yBounds = [minY, maxY];
1812
+ this.useVertexColor = false;
1813
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffers[1]);
1814
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, pos, this.gl.DYNAMIC_DRAW);
1815
+ }
1816
+ draw(state) {
1817
+ if (this.count === 0) return;
1818
+ const gl = state.gl;
1819
+ gl.useProgram(this.program);
1820
+ setTransformUniforms(gl, this.uniforms, state.x, state.y, this.xRef, this.yRef);
1821
+ gl.uniform4f(this.uniforms.uColor, this.color[0], this.color[1], this.color[2], this.color[3]);
1822
+ gl.uniform2f(this.uniforms.uResolution, state.pixelWidth, state.pixelHeight);
1823
+ gl.uniform1f(this.uniforms.uSize, this.size / 2 * state.dpr);
1824
+ gl.uniform1f(this.uniforms.uUseVertexColor, this.useVertexColor ? 1 : 0);
1825
+ gl.bindVertexArray(this.vao);
1826
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this.count);
1827
+ gl.bindVertexArray(null);
1828
+ }
1829
+ dispose() {
1830
+ this.gl.deleteVertexArray(this.vao);
1831
+ for (const b of this.buffers) this.gl.deleteBuffer(b);
1832
+ }
1833
+ };
1834
+
1835
+ // src/render/overlay.ts
1836
+ function plotRegion(layout) {
1837
+ const { margin } = layout;
1838
+ return {
1839
+ left: margin.left,
1840
+ top: margin.top,
1841
+ width: Math.max(0, layout.cssWidth - margin.left - margin.right),
1842
+ height: Math.max(0, layout.cssHeight - margin.top - margin.bottom)
1843
+ };
1844
+ }
1845
+ var lightTheme = {
1846
+ axis: "#334155",
1847
+ grid: "rgba(100,116,139,0.18)",
1848
+ gridMinor: "rgba(100,116,139,0.08)",
1849
+ text: "#475569",
1850
+ font: "12px system-ui, -apple-system, sans-serif"
1851
+ };
1852
+ var darkTheme = {
1853
+ axis: "#cbd5e1",
1854
+ grid: "rgba(148,163,184,0.16)",
1855
+ gridMinor: "rgba(148,163,184,0.07)",
1856
+ text: "#94a3b8",
1857
+ font: "12px system-ui, -apple-system, sans-serif"
1858
+ };
1859
+ function pxX(region, t) {
1860
+ return region.left + t * region.width;
1861
+ }
1862
+ function pxY(region, t) {
1863
+ return region.top + (1 - t) * region.height;
1864
+ }
1865
+ function drawGrid(ctx, region, scaleX, scaleY, ticksX, ticksY, theme) {
1866
+ ctx.save();
1867
+ ctx.lineWidth = 1;
1868
+ for (const t of ticksX) {
1869
+ if (!t.grid) continue;
1870
+ const x = Math.round(pxX(region, scaleX.norm(t.value))) + 0.5;
1871
+ ctx.strokeStyle = t.minor ? theme.gridMinor : theme.grid;
1872
+ ctx.beginPath();
1873
+ ctx.moveTo(x, region.top);
1874
+ ctx.lineTo(x, region.top + region.height);
1875
+ ctx.stroke();
1876
+ }
1877
+ for (const t of ticksY) {
1878
+ if (!t.grid) continue;
1879
+ const y = Math.round(pxY(region, scaleY.norm(t.value))) + 0.5;
1880
+ ctx.strokeStyle = t.minor ? theme.gridMinor : theme.grid;
1881
+ ctx.beginPath();
1882
+ ctx.moveTo(region.left, y);
1883
+ ctx.lineTo(region.left + region.width, y);
1884
+ ctx.stroke();
1885
+ }
1886
+ ctx.restore();
1887
+ }
1888
+ function drawXAxis(ctx, region, scaleX, ticksX, theme, title) {
1889
+ ctx.save();
1890
+ ctx.strokeStyle = theme.axis;
1891
+ ctx.fillStyle = theme.text;
1892
+ ctx.font = theme.font;
1893
+ ctx.lineWidth = 1;
1894
+ const bottom = region.top + region.height;
1895
+ ctx.beginPath();
1896
+ ctx.moveTo(region.left, bottom + 0.5);
1897
+ ctx.lineTo(region.left + region.width, bottom + 0.5);
1898
+ ctx.stroke();
1899
+ ctx.textAlign = "center";
1900
+ ctx.textBaseline = "top";
1901
+ for (const t of ticksX) {
1902
+ const x = Math.round(pxX(region, scaleX.norm(t.value))) + 0.5;
1903
+ const len = t.minor ? 3 : 5;
1904
+ ctx.beginPath();
1905
+ ctx.moveTo(x, bottom);
1906
+ ctx.lineTo(x, bottom + len);
1907
+ ctx.stroke();
1908
+ if (t.label) ctx.fillText(t.label, x, bottom + len + 3);
1909
+ }
1910
+ if (title) {
1911
+ ctx.textBaseline = "bottom";
1912
+ ctx.fillText(title, region.left + region.width / 2, bottom + 34);
1913
+ }
1914
+ ctx.restore();
1915
+ }
1916
+ function drawYAxis(ctx, region, scaleY, ticksY, theme, opts) {
1917
+ ctx.save();
1918
+ const color = opts.color ?? theme.axis;
1919
+ ctx.strokeStyle = color;
1920
+ ctx.fillStyle = opts.color ?? theme.text;
1921
+ ctx.font = theme.font;
1922
+ ctx.lineWidth = 1;
1923
+ const ax = Math.round(opts.x) + 0.5;
1924
+ const dir = opts.side === "left" ? -1 : 1;
1925
+ ctx.beginPath();
1926
+ ctx.moveTo(ax, region.top);
1927
+ ctx.lineTo(ax, region.top + region.height);
1928
+ ctx.stroke();
1929
+ ctx.textAlign = opts.side === "left" ? "right" : "left";
1930
+ ctx.textBaseline = "middle";
1931
+ for (const t of ticksY) {
1932
+ const y = Math.round(pxY(region, scaleY.norm(t.value))) + 0.5;
1933
+ const len = t.minor ? 3 : 5;
1934
+ ctx.beginPath();
1935
+ ctx.moveTo(ax, y);
1936
+ ctx.lineTo(ax + dir * len, y);
1937
+ ctx.stroke();
1938
+ if (t.label) ctx.fillText(t.label, ax + dir * (len + 4), y);
1939
+ }
1940
+ if (opts.title) {
1941
+ ctx.save();
1942
+ const tx = opts.titleX ?? (opts.side === "left" ? 12 : region.left + region.width + 36);
1943
+ ctx.translate(tx, region.top + region.height / 2);
1944
+ ctx.rotate(-Math.PI / 2);
1945
+ ctx.textAlign = "center";
1946
+ ctx.textBaseline = opts.side === "left" ? "top" : "bottom";
1947
+ ctx.fillText(opts.title, 0, 0);
1948
+ ctx.restore();
1949
+ }
1950
+ ctx.restore();
1951
+ }
1952
+ function drawCrosshair(ctx, region, px, theme) {
1953
+ ctx.save();
1954
+ ctx.strokeStyle = theme.text;
1955
+ ctx.globalAlpha = 0.4;
1956
+ ctx.setLineDash([3, 3]);
1957
+ ctx.lineWidth = 1;
1958
+ const x = Math.round(px) + 0.5;
1959
+ ctx.beginPath();
1960
+ ctx.moveTo(x, region.top);
1961
+ ctx.lineTo(x, region.top + region.height);
1962
+ ctx.stroke();
1963
+ ctx.restore();
1964
+ }
1965
+ function drawMarker(ctx, px, py, color) {
1966
+ ctx.save();
1967
+ ctx.fillStyle = color;
1968
+ ctx.strokeStyle = "#ffffff";
1969
+ ctx.lineWidth = 1.5;
1970
+ ctx.beginPath();
1971
+ ctx.arc(px, py, 4, 0, Math.PI * 2);
1972
+ ctx.fill();
1973
+ ctx.stroke();
1974
+ ctx.restore();
1975
+ }
1976
+
1977
+ // src/scales/scale.ts
1978
+ var LinearScale = class {
1979
+ constructor(domain = [0, 1]) {
1980
+ this.type = "linear";
1981
+ this.log = false;
1982
+ this.domain = domain;
1983
+ }
1984
+ norm(value) {
1985
+ const [a, b] = this.domain;
1986
+ return b === a ? 0 : (value - a) / (b - a);
1987
+ }
1988
+ invert(t) {
1989
+ const [a, b] = this.domain;
1990
+ return a + t * (b - a);
1991
+ }
1992
+ ticks(target = 6) {
1993
+ return autoTicks(this.domain[0], this.domain[1], target);
1994
+ }
1995
+ formatTick(value) {
1996
+ return defaultFormat(value);
1997
+ }
1998
+ };
1999
+ var LogScale = class _LogScale {
2000
+ constructor(domain = [1, 1e3]) {
2001
+ this.type = "log";
2002
+ this.log = true;
2003
+ this.domain = _LogScale.sanitize(domain);
2004
+ }
2005
+ static sanitize(d) {
2006
+ const lo = d[0] > 0 ? d[0] : 1e-9;
2007
+ const hi = d[1] > lo ? d[1] : lo * 10;
2008
+ return [lo, hi];
2009
+ }
2010
+ get la() {
2011
+ return Math.log10(this.domain[0]);
2012
+ }
2013
+ get lb() {
2014
+ return Math.log10(this.domain[1]);
2015
+ }
2016
+ norm(value) {
2017
+ if (value <= 0) return 0;
2018
+ return (Math.log10(value) - this.la) / (this.lb - this.la);
2019
+ }
2020
+ invert(t) {
2021
+ return Math.pow(10, this.la + t * (this.lb - this.la));
2022
+ }
2023
+ ticks() {
2024
+ const [a, b] = this.domain;
2025
+ const lo = Math.floor(Math.log10(a));
2026
+ const hi = Math.ceil(Math.log10(b));
2027
+ const ticks = [];
2028
+ for (let e = lo; e <= hi; e++) {
2029
+ const base = Math.pow(10, e);
2030
+ ticks.push({ value: base });
2031
+ for (let m = 2; m <= 9; m++) ticks.push({ value: m * base, minor: true, grid: false });
2032
+ }
2033
+ return ticks;
2034
+ }
2035
+ formatTick(value) {
2036
+ const e = Math.round(Math.log10(value));
2037
+ if (e <= -4 || e >= 5) return `1e${e}`;
2038
+ return defaultFormat(value);
2039
+ }
2040
+ };
2041
+ var SECOND = 1e3;
2042
+ var MINUTE = 6e4;
2043
+ var HOUR = 36e5;
2044
+ var DAY = 864e5;
2045
+ var TIME_STEPS = [
2046
+ SECOND,
2047
+ 5 * SECOND,
2048
+ 15 * SECOND,
2049
+ 30 * SECOND,
2050
+ MINUTE,
2051
+ 5 * MINUTE,
2052
+ 15 * MINUTE,
2053
+ 30 * MINUTE,
2054
+ HOUR,
2055
+ 3 * HOUR,
2056
+ 6 * HOUR,
2057
+ 12 * HOUR,
2058
+ DAY,
2059
+ 2 * DAY,
2060
+ 7 * DAY,
2061
+ 30 * DAY,
2062
+ 90 * DAY,
2063
+ 365 * DAY
2064
+ ];
2065
+ function pad2(n) {
2066
+ return n < 10 ? `0${n}` : `${n}`;
2067
+ }
2068
+ var TimeScale = class {
2069
+ constructor(domain = [0, DAY]) {
2070
+ this.type = "time";
2071
+ this.log = false;
2072
+ this.domain = domain;
2073
+ }
2074
+ norm(value) {
2075
+ const [a, b] = this.domain;
2076
+ return b === a ? 0 : (value - a) / (b - a);
2077
+ }
2078
+ invert(t) {
2079
+ const [a, b] = this.domain;
2080
+ return a + t * (b - a);
2081
+ }
2082
+ chooseStep(target) {
2083
+ const ideal = (this.domain[1] - this.domain[0]) / target;
2084
+ for (const s of TIME_STEPS) if (s >= ideal) return s;
2085
+ return TIME_STEPS[TIME_STEPS.length - 1];
2086
+ }
2087
+ ticks(target = 6) {
2088
+ const [a, b] = this.domain;
2089
+ if (!isFinite(a) || !isFinite(b) || a === b) return [];
2090
+ const step = this.chooseStep(target);
2091
+ const start = Math.ceil(a / step) * step;
2092
+ const ticks = [];
2093
+ for (let i = 0; i < 1e3; i++) {
2094
+ const v = start + i * step;
2095
+ if (v > b) break;
2096
+ ticks.push({ value: v });
2097
+ }
2098
+ return ticks;
2099
+ }
2100
+ formatTick(value) {
2101
+ const span = this.domain[1] - this.domain[0];
2102
+ const d = new Date(value);
2103
+ if (span < DAY) return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
2104
+ if (span < 90 * DAY) return `${d.getMonth() + 1}/${d.getDate()}`;
2105
+ return `${d.getFullYear()}`;
2106
+ }
2107
+ };
2108
+ function makeScale(type, domain) {
2109
+ switch (type) {
2110
+ case "linear":
2111
+ return new LinearScale(domain);
2112
+ case "log":
2113
+ return new LogScale(domain);
2114
+ case "time":
2115
+ return new TimeScale(domain);
2116
+ default:
2117
+ throw new Error(`Unknown scale type: ${type}`);
2118
+ }
2119
+ }
2120
+
2121
+ // src/ui/toolbar.ts
2122
+ var lightToolbar = {
2123
+ bg: "rgba(255,255,255,0.9)",
2124
+ fg: "#475569",
2125
+ border: "rgba(100,116,139,0.25)",
2126
+ activeBg: "#3b82f6",
2127
+ activeFg: "#ffffff",
2128
+ hoverBg: "rgba(100,116,139,0.12)"
2129
+ };
2130
+ var darkToolbar = {
2131
+ bg: "rgba(15,23,42,0.85)",
2132
+ fg: "#cbd5e1",
2133
+ border: "rgba(148,163,184,0.25)",
2134
+ activeBg: "#3b82f6",
2135
+ activeFg: "#ffffff",
2136
+ hoverBg: "rgba(148,163,184,0.15)"
2137
+ };
2138
+ var ICONS = {
2139
+ home: `<path d="M2 7.5 8 2l6 5.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 7v6.5h3.2V10h1.6v3.5H12V7" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>`,
2140
+ pan: `<path d="M8 1.5v13M1.5 8h13M8 1.5 6 3.5M8 1.5l2 2M8 14.5l-2-2M8 14.5l2-2M1.5 8l2-2M1.5 8l2 2M14.5 8l-2-2M14.5 8l-2 2" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>`,
2141
+ box: `<rect x="2.5" y="2.5" width="11" height="11" rx="1" fill="none" stroke="currentColor" stroke-width="1.4" stroke-dasharray="2.4 2"/><circle cx="8" cy="8" r="1.4" fill="currentColor"/>`,
2142
+ boxX: `<path d="M1.5 8h13M3.5 5.5 1.5 8l2 2.5M12.5 5.5 14.5 8l-2 2.5" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/><rect x="4.5" y="3" width="7" height="10" rx="1" fill="none" stroke="currentColor" stroke-width="1.1" stroke-dasharray="2 1.8"/>`,
2143
+ boxY: `<path d="M8 1.5v13M5.5 3.5 8 1.5l2.5 2M5.5 12.5 8 14.5l2.5-2" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/><rect x="3" y="4.5" width="10" height="7" rx="1" fill="none" stroke="currentColor" stroke-width="1.1" stroke-dasharray="2 1.8"/>`
2144
+ };
2145
+ function createToolbar(container, host, dark) {
2146
+ const t = dark ? darkToolbar : lightToolbar;
2147
+ const bar = document.createElement("div");
2148
+ Object.assign(bar.style, {
2149
+ position: "absolute",
2150
+ top: "8px",
2151
+ right: "8px",
2152
+ zIndex: "3",
2153
+ display: "flex",
2154
+ gap: "2px",
2155
+ padding: "3px",
2156
+ borderRadius: "8px",
2157
+ background: t.bg,
2158
+ border: `1px solid ${t.border}`,
2159
+ backdropFilter: "blur(6px)",
2160
+ boxShadow: "0 2px 8px rgba(0,0,0,0.15)"
2161
+ });
2162
+ const buttons = [
2163
+ { key: "home", title: "Reset view (Home)", icon: ICONS.home, action: () => host.home() },
2164
+ { key: "pan", title: "Pan", icon: ICONS.pan, mode: "pan" },
2165
+ { key: "box", title: "Box zoom", icon: ICONS.box, mode: "box" },
2166
+ { key: "box-x", title: "Zoom X only", icon: ICONS.boxX, mode: "box-x" },
2167
+ { key: "box-y", title: "Zoom Y only", icon: ICONS.boxY, mode: "box-y" }
2168
+ ];
2169
+ const modeButtons = /* @__PURE__ */ new Map();
2170
+ for (const b of buttons) {
2171
+ const btn = document.createElement("button");
2172
+ btn.type = "button";
2173
+ btn.title = b.title;
2174
+ btn.setAttribute("aria-label", b.title);
2175
+ btn.innerHTML = `<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">${b.icon}</svg>`;
2176
+ Object.assign(btn.style, {
2177
+ display: "inline-flex",
2178
+ alignItems: "center",
2179
+ justifyContent: "center",
2180
+ width: "26px",
2181
+ height: "26px",
2182
+ padding: "0",
2183
+ border: "none",
2184
+ borderRadius: "6px",
2185
+ background: "transparent",
2186
+ color: t.fg,
2187
+ cursor: "pointer",
2188
+ transition: "background 0.12s, color 0.12s"
2189
+ });
2190
+ btn.addEventListener("mouseenter", () => {
2191
+ if (btn.dataset.active !== "true") btn.style.background = t.hoverBg;
2192
+ });
2193
+ btn.addEventListener("mouseleave", () => {
2194
+ if (btn.dataset.active !== "true") btn.style.background = "transparent";
2195
+ });
2196
+ btn.addEventListener("click", () => {
2197
+ if (b.action) b.action();
2198
+ if (b.mode) host.setMode(b.mode);
2199
+ });
2200
+ if (b.mode) modeButtons.set(b.mode, btn);
2201
+ bar.appendChild(btn);
2202
+ }
2203
+ const setActive = (mode) => {
2204
+ for (const [m, btn] of modeButtons) {
2205
+ const active = m === mode;
2206
+ btn.dataset.active = String(active);
2207
+ btn.style.background = active ? t.activeBg : "transparent";
2208
+ btn.style.color = active ? t.activeFg : t.fg;
2209
+ }
2210
+ };
2211
+ setActive(host.getMode());
2212
+ host.onModeChange(setActive);
2213
+ container.appendChild(bar);
2214
+ return {
2215
+ element: bar,
2216
+ destroy: () => bar.remove()
2217
+ };
2218
+ }
2219
+
2220
+ // src/plot.ts
2221
+ var DEFAULT_MARGIN = { top: 16, right: 16, bottom: 40, left: 56 };
2222
+ var Y_AXIS_GAP = 52;
2223
+ function isPickable(layer) {
2224
+ return typeof layer.nearestByX === "function";
2225
+ }
2226
+ function padDomain(min, max, log, frac) {
2227
+ if (log) {
2228
+ const lo = min > 0 ? min : max > 0 ? max / 1e3 : 1e-9;
2229
+ const hi = max > lo ? max : lo * 10;
2230
+ const f = Math.pow(hi / lo, frac);
2231
+ return [lo / f, hi * f];
2232
+ }
2233
+ const pad = (max - min) * frac || 1;
2234
+ return [min - pad, max + pad];
2235
+ }
2236
+ var Plot = class {
2237
+ constructor(container, options = {}) {
2238
+ /** Named y axes, insertion-ordered. `"y"` is always the primary. */
2239
+ this.yAxes = /* @__PURE__ */ new Map();
2240
+ this.layers = [];
2241
+ this.dpr = 1;
2242
+ this.frameRequested = false;
2243
+ this.modeChangeCbs = [];
2244
+ this.toolbarHandle = null;
2245
+ this.hoverPx = null;
2246
+ this.container = container;
2247
+ if (getComputedStyle(container).position === "static") {
2248
+ container.style.position = "relative";
2249
+ }
2250
+ this.gridCanvas = this.makeCanvas(0);
2251
+ this.dataCanvas = this.makeCanvas(1);
2252
+ this.axisCanvas = this.makeCanvas(2);
2253
+ this.gridCtx = this.gridCanvas.getContext("2d");
2254
+ this.dataCtx = this.dataCanvas.getContext("2d");
2255
+ this.axisCtx = this.axisCanvas.getContext("2d");
2256
+ const s = getSharedGL();
2257
+ this.gl = s.gl;
2258
+ this.sharedCanvas = s.canvas;
2259
+ const sx = options.scales?.x ?? {};
2260
+ const sy = options.scales?.y ?? {};
2261
+ this.scaleX = makeScale(sx.type ?? "linear", sx.domain ?? [0, 1]);
2262
+ this.autoX = sx.domain == null;
2263
+ this.initialX = sx.domain ?? null;
2264
+ this.axisX = new Axis(options.axes?.x);
2265
+ this.yAxes.set("y", {
2266
+ id: "y",
2267
+ scale: makeScale(sy.type ?? "linear", sy.domain ?? [0, 1]),
2268
+ axis: new Axis(options.axes?.y),
2269
+ side: "left",
2270
+ auto: sy.domain == null,
2271
+ initial: sy.domain ?? null
2272
+ });
2273
+ this.isDark = options.theme === "dark";
2274
+ this.theme = options.theme === "dark" ? darkTheme : options.theme === "light" || options.theme == null ? lightTheme : options.theme;
2275
+ this.baseMargin = { ...DEFAULT_MARGIN, ...options.margin };
2276
+ this.mode = options.mode ?? "box";
2277
+ this.hoverEnabled = options.hover !== false;
2278
+ this.selectionDiv = document.createElement("div");
2279
+ Object.assign(this.selectionDiv.style, {
2280
+ position: "absolute",
2281
+ display: "none",
2282
+ zIndex: "2",
2283
+ pointerEvents: "none",
2284
+ background: "rgba(59,130,246,0.15)",
2285
+ border: "1px solid rgba(59,130,246,0.9)",
2286
+ borderRadius: "1px"
2287
+ });
2288
+ container.appendChild(this.selectionDiv);
2289
+ this.tooltip = document.createElement("div");
2290
+ Object.assign(this.tooltip.style, {
2291
+ position: "absolute",
2292
+ display: "none",
2293
+ zIndex: "4",
2294
+ pointerEvents: "none",
2295
+ padding: "6px 8px",
2296
+ borderRadius: "6px",
2297
+ font: "12px system-ui, -apple-system, sans-serif",
2298
+ lineHeight: "1.4",
2299
+ whiteSpace: "nowrap",
2300
+ background: this.isDark ? "rgba(15,23,42,0.92)" : "rgba(255,255,255,0.96)",
2301
+ color: this.isDark ? "#e2e8f0" : "#1e293b",
2302
+ border: `1px solid ${this.isDark ? "rgba(148,163,184,0.25)" : "rgba(100,116,139,0.25)"}`,
2303
+ boxShadow: "0 4px 12px rgba(0,0,0,0.18)"
2304
+ });
2305
+ container.appendChild(this.tooltip);
2306
+ this.resizeObserver = new ResizeObserver(() => this.resize());
2307
+ this.resizeObserver.observe(container);
2308
+ this.resize();
2309
+ if (options.interactive !== false) this.attachInteraction();
2310
+ if (options.toolbar !== false) {
2311
+ this.toolbarHandle = createToolbar(
2312
+ container,
2313
+ {
2314
+ setMode: (m) => this.setMode(m),
2315
+ getMode: () => this.mode,
2316
+ home: () => this.home(),
2317
+ onModeChange: (cb) => this.modeChangeCbs.push(cb)
2318
+ },
2319
+ this.isDark
2320
+ );
2321
+ }
2322
+ this.updateCursor();
2323
+ }
2324
+ makeCanvas(z) {
2325
+ const c = document.createElement("canvas");
2326
+ Object.assign(c.style, {
2327
+ position: "absolute",
2328
+ inset: "0",
2329
+ width: "100%",
2330
+ height: "100%",
2331
+ zIndex: String(z)
2332
+ });
2333
+ if (z < 2) c.style.pointerEvents = "none";
2334
+ this.container.appendChild(c);
2335
+ return c;
2336
+ }
2337
+ /** Margins grow to make room for extra y axes on each side. */
2338
+ computeMargin() {
2339
+ let leftCount = 0;
2340
+ let rightCount = 0;
2341
+ for (const ya of this.yAxes.values()) {
2342
+ if (ya.side === "left") leftCount++;
2343
+ else rightCount++;
2344
+ }
2345
+ return {
2346
+ top: this.baseMargin.top,
2347
+ bottom: this.baseMargin.bottom,
2348
+ left: this.baseMargin.left + Math.max(0, leftCount - 1) * Y_AXIS_GAP,
2349
+ right: this.baseMargin.right + rightCount * Y_AXIS_GAP
2350
+ };
2351
+ }
2352
+ layout() {
2353
+ return {
2354
+ cssWidth: this.container.clientWidth,
2355
+ cssHeight: this.container.clientHeight,
2356
+ margin: this.computeMargin()
2357
+ };
2358
+ }
2359
+ /** Pixel x-position (and title x) for each y axis, by draw order per side. */
2360
+ yAxisPositions() {
2361
+ const region = plotRegion(this.layout());
2362
+ const out = /* @__PURE__ */ new Map();
2363
+ let li = 0;
2364
+ let ri = 0;
2365
+ for (const ya of this.yAxes.values()) {
2366
+ if (ya.side === "left") {
2367
+ const x = region.left - li * Y_AXIS_GAP;
2368
+ out.set(ya.id, { x, titleX: x - 42 });
2369
+ li++;
2370
+ } else {
2371
+ const x = region.left + region.width + ri * Y_AXIS_GAP;
2372
+ out.set(ya.id, { x, titleX: x + 42 });
2373
+ ri++;
2374
+ }
2375
+ }
2376
+ return out;
2377
+ }
2378
+ // ---- Public API -----------------------------------------------------------
2379
+ register(layer) {
2380
+ if (!this.yAxes.has(layer.yAxis)) {
2381
+ throw new Error(`Unknown y axis "${layer.yAxis}". Call addYAxis() first.`);
2382
+ }
2383
+ this.layers.push(layer);
2384
+ this.autoscale();
2385
+ this.requestRender();
2386
+ return layer;
2387
+ }
2388
+ addLine(opts) {
2389
+ return this.register(new LineLayer(this.gl, opts));
2390
+ }
2391
+ addScatter(opts) {
2392
+ return this.register(new ScatterLayer(this.gl, opts));
2393
+ }
2394
+ addBar(opts) {
2395
+ return this.register(new BarLayer(this.gl, opts));
2396
+ }
2397
+ addArea(opts) {
2398
+ return this.register(new AreaLayer(this.gl, opts));
2399
+ }
2400
+ addHeatmap(opts) {
2401
+ return this.register(new HeatmapLayer(this.gl, opts));
2402
+ }
2403
+ addBox(opts) {
2404
+ return this.register(new BoxLayer(this.gl, opts));
2405
+ }
2406
+ addHexbin(opts) {
2407
+ return this.register(new HexbinLayer(this.gl, opts));
2408
+ }
2409
+ addContour(opts) {
2410
+ return this.register(new ContourLayer(this.gl, opts));
2411
+ }
2412
+ /** Compute an STFT of `signal` and render it as a heatmap (time × frequency). */
2413
+ addHeatmapSpectrogram(signal, opts = {}) {
2414
+ const s = spectrogram(signal, opts);
2415
+ return this.register(
2416
+ new HeatmapLayer(this.gl, {
2417
+ values: s.values,
2418
+ cols: s.cols,
2419
+ rows: s.rows,
2420
+ extent: s.extent,
2421
+ colormap: opts.colormap ?? "plasma"
2422
+ })
2423
+ );
2424
+ }
2425
+ /** Bin raw `values` and render a histogram as bars. */
2426
+ addHistogram(values, opts = {}) {
2427
+ const h = histogram(values, { bins: opts.bins, range: opts.range });
2428
+ return this.register(
2429
+ new BarLayer(this.gl, {
2430
+ x: h.centers,
2431
+ y: h.counts,
2432
+ width: h.binWidth * 0.98,
2433
+ color: opts.color,
2434
+ name: opts.name,
2435
+ yAxis: opts.yAxis
2436
+ })
2437
+ );
2438
+ }
2439
+ /** Register an additional named y axis. Series opt in via `addLine({ yAxis })`. */
2440
+ addYAxis(id, opts = {}) {
2441
+ if (this.yAxes.has(id)) throw new Error(`Y axis "${id}" already exists`);
2442
+ const { type, domain, side, color, ...axisConfig } = opts;
2443
+ this.yAxes.set(id, {
2444
+ id,
2445
+ scale: makeScale(type ?? "linear", domain ?? [0, 1]),
2446
+ axis: new Axis(axisConfig),
2447
+ side: side ?? "right",
2448
+ auto: domain == null,
2449
+ initial: domain ?? null,
2450
+ color
2451
+ });
2452
+ this.autoscale();
2453
+ this.requestRender();
2454
+ }
2455
+ removeLayer(layer) {
2456
+ const i = this.layers.indexOf(layer);
2457
+ if (i >= 0) {
2458
+ this.layers.splice(i, 1);
2459
+ layer.dispose();
2460
+ this.autoscale();
2461
+ this.requestRender();
2462
+ }
2463
+ }
2464
+ /** Configure ticks/format/title for the x axis or a y axis (default primary "y"). */
2465
+ setAxis(dim, config) {
2466
+ if (dim === "x") {
2467
+ this.axisX.update(config);
2468
+ } else {
2469
+ const ya = this.yAxes.get(dim === "y" ? "y" : dim);
2470
+ if (!ya) throw new Error(`Unknown axis "${dim}"`);
2471
+ ya.axis.update(config);
2472
+ }
2473
+ this.requestRender();
2474
+ }
2475
+ /** Set (or lock) the visible domain. `y` targets the primary axis; use `yAxes` for others. */
2476
+ setView(view) {
2477
+ if (view.x) {
2478
+ this.scaleX.domain = view.x;
2479
+ this.autoX = false;
2480
+ }
2481
+ if (view.y) this.setYDomain("y", view.y);
2482
+ if (view.yAxes) {
2483
+ for (const [id, r] of Object.entries(view.yAxes)) this.setYDomain(id, r);
2484
+ }
2485
+ this.requestRender();
2486
+ }
2487
+ setYDomain(id, range) {
2488
+ const ya = this.yAxes.get(id);
2489
+ if (!ya) throw new Error(`Unknown y axis "${id}"`);
2490
+ ya.scale.domain = range;
2491
+ ya.auto = false;
2492
+ }
2493
+ // ---- Interaction control --------------------------------------------------
2494
+ setMode(mode) {
2495
+ if (mode === this.mode) return;
2496
+ this.mode = mode;
2497
+ this.updateCursor();
2498
+ for (const cb of this.modeChangeCbs) cb(mode);
2499
+ }
2500
+ getMode() {
2501
+ return this.mode;
2502
+ }
2503
+ onModeChange(cb) {
2504
+ this.modeChangeCbs.push(cb);
2505
+ }
2506
+ /** Reset to the home view: explicit domains restored, auto axes re-fit to data. */
2507
+ home() {
2508
+ if (this.initialX) {
2509
+ this.scaleX.domain = this.initialX;
2510
+ this.autoX = false;
2511
+ } else {
2512
+ this.autoX = true;
2513
+ }
2514
+ for (const ya of this.yAxes.values()) {
2515
+ if (ya.initial) {
2516
+ ya.scale.domain = ya.initial;
2517
+ ya.auto = false;
2518
+ } else {
2519
+ ya.auto = true;
2520
+ }
2521
+ }
2522
+ this.autoscale();
2523
+ this.requestRender();
2524
+ }
2525
+ /** Re-fit auto axes to the data: x over all series, each y axis over its own series. */
2526
+ autoscale() {
2527
+ if (this.autoX) {
2528
+ let minX = Infinity, maxX = -Infinity, any = false;
2529
+ for (const l of this.layers) {
2530
+ const b = l.bounds();
2531
+ if (!b) continue;
2532
+ any = true;
2533
+ minX = Math.min(minX, b.x[0]);
2534
+ maxX = Math.max(maxX, b.x[1]);
2535
+ }
2536
+ if (any) this.scaleX.domain = padDomain(minX, maxX, this.scaleX.log, 0.02);
2537
+ }
2538
+ for (const ya of this.yAxes.values()) {
2539
+ if (!ya.auto) continue;
2540
+ let minY = Infinity, maxY = -Infinity, any = false;
2541
+ for (const l of this.layers) {
2542
+ if (l.yAxis !== ya.id) continue;
2543
+ const b = l.bounds();
2544
+ if (!b) continue;
2545
+ any = true;
2546
+ minY = Math.min(minY, b.y[0]);
2547
+ maxY = Math.max(maxY, b.y[1]);
2548
+ }
2549
+ if (any) ya.scale.domain = padDomain(minY, maxY, ya.scale.log, 0.05);
2550
+ }
2551
+ }
2552
+ destroy() {
2553
+ this.resizeObserver.disconnect();
2554
+ this.toolbarHandle?.destroy();
2555
+ this.selectionDiv.remove();
2556
+ this.tooltip.remove();
2557
+ for (const l of this.layers) l.dispose();
2558
+ this.container.removeChild(this.gridCanvas);
2559
+ this.container.removeChild(this.dataCanvas);
2560
+ this.container.removeChild(this.axisCanvas);
2561
+ }
2562
+ // ---- Rendering ------------------------------------------------------------
2563
+ resize() {
2564
+ const dpr = window.devicePixelRatio || 1;
2565
+ this.dpr = dpr;
2566
+ const w = this.container.clientWidth;
2567
+ const h = this.container.clientHeight;
2568
+ for (const c of [this.gridCanvas, this.dataCanvas, this.axisCanvas]) {
2569
+ c.width = Math.max(1, Math.round(w * dpr));
2570
+ c.height = Math.max(1, Math.round(h * dpr));
2571
+ }
2572
+ this.gridCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
2573
+ this.dataCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
2574
+ this.axisCtx.setTransform(dpr, 0, 0, dpr, 0, 0);
2575
+ this.render();
2576
+ }
2577
+ requestRender() {
2578
+ if (this.frameRequested) return;
2579
+ this.frameRequested = true;
2580
+ requestAnimationFrame(() => {
2581
+ this.frameRequested = false;
2582
+ this.render();
2583
+ });
2584
+ }
2585
+ primaryY() {
2586
+ return this.yAxes.get("y");
2587
+ }
2588
+ render() {
2589
+ const layout = this.layout();
2590
+ const region = plotRegion(layout);
2591
+ const primary = this.primaryY();
2592
+ const ticksX = this.axisX.resolve(this.scaleX);
2593
+ const ticksYPrimary = primary.axis.resolve(primary.scale);
2594
+ this.gridCtx.clearRect(0, 0, layout.cssWidth, layout.cssHeight);
2595
+ drawGrid(this.gridCtx, region, this.scaleX, primary.scale, ticksX, ticksYPrimary, this.theme);
2596
+ const gl = this.gl;
2597
+ const devW = this.dataCanvas.width;
2598
+ const devH = this.dataCanvas.height;
2599
+ sizeShared(gl, devW, devH);
2600
+ begin2D(gl);
2601
+ gl.clearColor(0, 0, 0, 0);
2602
+ gl.disable(gl.SCISSOR_TEST);
2603
+ gl.viewport(0, 0, devW, devH);
2604
+ gl.clear(gl.COLOR_BUFFER_BIT);
2605
+ const dpr = this.dpr;
2606
+ const vx = Math.round(region.left * dpr);
2607
+ const vw = Math.round(region.width * dpr);
2608
+ const vh = Math.round(region.height * dpr);
2609
+ const vy = devH - Math.round((region.top + region.height) * dpr);
2610
+ gl.enable(gl.SCISSOR_TEST);
2611
+ gl.scissor(vx, vy, vw, vh);
2612
+ gl.viewport(vx, vy, vw, vh);
2613
+ const xFrame = {
2614
+ lo: this.scaleX.domain[0],
2615
+ hi: this.scaleX.domain[1],
2616
+ log: this.scaleX.log
2617
+ };
2618
+ for (const layer of this.layers) {
2619
+ const ya = this.yAxes.get(layer.yAxis);
2620
+ layer.draw({
2621
+ gl,
2622
+ x: xFrame,
2623
+ y: { lo: ya.scale.domain[0], hi: ya.scale.domain[1], log: ya.scale.log },
2624
+ pixelWidth: vw,
2625
+ pixelHeight: vh,
2626
+ dpr: this.dpr
2627
+ });
2628
+ }
2629
+ gl.disable(gl.SCISSOR_TEST);
2630
+ this.dataCtx.clearRect(0, 0, layout.cssWidth, layout.cssHeight);
2631
+ this.dataCtx.drawImage(this.sharedCanvas, 0, 0, layout.cssWidth, layout.cssHeight);
2632
+ this.axisCtx.clearRect(0, 0, layout.cssWidth, layout.cssHeight);
2633
+ drawXAxis(this.axisCtx, region, this.scaleX, ticksX, this.theme, this.axisX.config.title);
2634
+ const positions = this.yAxisPositions();
2635
+ for (const ya of this.yAxes.values()) {
2636
+ const pos = positions.get(ya.id);
2637
+ const ticks = ya.axis.resolve(ya.scale);
2638
+ drawYAxis(this.axisCtx, region, ya.scale, ticks, this.theme, {
2639
+ x: pos.x,
2640
+ side: ya.side,
2641
+ title: ya.axis.config.title,
2642
+ color: ya.color,
2643
+ titleX: pos.titleX
2644
+ });
2645
+ }
2646
+ if (this.hoverEnabled && this.hoverPx) this.renderHover(region);
2647
+ else this.tooltip.style.display = "none";
2648
+ }
2649
+ renderHover(region) {
2650
+ const cursor = this.hoverPx;
2651
+ if (cursor.x < region.left || cursor.x > region.left + region.width || cursor.y < region.top || cursor.y > region.top + region.height) {
2652
+ this.tooltip.style.display = "none";
2653
+ return;
2654
+ }
2655
+ const nx = (cursor.x - region.left) / region.width;
2656
+ const dataX = this.scaleX.invert(nx);
2657
+ drawCrosshair(this.axisCtx, region, cursor.x, this.theme);
2658
+ const rows = [];
2659
+ for (const layer of this.layers) {
2660
+ if (!isPickable(layer)) continue;
2661
+ const p = layer.nearestByX(dataX);
2662
+ if (!p) continue;
2663
+ const ya = this.yAxes.get(layer.yAxis);
2664
+ const px = pxX(region, this.scaleX.norm(p.x));
2665
+ const py = pxY(region, ya.scale.norm(p.y));
2666
+ drawMarker(this.axisCtx, px, py, layer.colorCss);
2667
+ rows.push({ layer, x: p.x, y: p.y });
2668
+ }
2669
+ if (rows.length === 0) {
2670
+ this.tooltip.style.display = "none";
2671
+ return;
2672
+ }
2673
+ this.updateTooltip(rows, cursor, dataX);
2674
+ }
2675
+ updateTooltip(rows, cursor, dataX) {
2676
+ const tip = this.tooltip;
2677
+ tip.replaceChildren();
2678
+ const xfmt = this.axisX.config.format ?? defaultFormat;
2679
+ const header = document.createElement("div");
2680
+ header.style.opacity = "0.7";
2681
+ header.style.marginBottom = "3px";
2682
+ header.textContent = `x = ${xfmt(dataX)}`;
2683
+ tip.appendChild(header);
2684
+ for (const r of rows) {
2685
+ const yfmt = this.yAxes.get(r.layer.yAxis).axis.config.format ?? defaultFormat;
2686
+ const row = document.createElement("div");
2687
+ row.style.display = "flex";
2688
+ row.style.alignItems = "center";
2689
+ row.style.gap = "6px";
2690
+ const dot = document.createElement("span");
2691
+ Object.assign(dot.style, {
2692
+ width: "8px",
2693
+ height: "8px",
2694
+ borderRadius: "50%",
2695
+ background: r.layer.colorCss,
2696
+ flex: "0 0 auto"
2697
+ });
2698
+ const label = document.createElement("span");
2699
+ label.textContent = `${r.layer.name}: ${yfmt(r.y)}`;
2700
+ row.appendChild(dot);
2701
+ row.appendChild(label);
2702
+ tip.appendChild(row);
2703
+ }
2704
+ tip.style.display = "block";
2705
+ const cw = this.container.clientWidth;
2706
+ const tw = tip.offsetWidth;
2707
+ const th = tip.offsetHeight;
2708
+ let left = cursor.x + 14;
2709
+ if (left + tw > cw) left = cursor.x - tw - 14;
2710
+ let top = cursor.y + 14;
2711
+ if (top + th > this.container.clientHeight) top = cursor.y - th - 14;
2712
+ tip.style.left = `${Math.max(0, left)}px`;
2713
+ tip.style.top = `${Math.max(0, top)}px`;
2714
+ }
2715
+ // ---- Interaction ----------------------------------------------------------
2716
+ updateCursor() {
2717
+ this.axisCanvas.style.cursor = this.mode === "pan" ? "grab" : "crosshair";
2718
+ }
2719
+ axisLock() {
2720
+ if (this.mode === "box-x") return { x: true, y: false };
2721
+ if (this.mode === "box-y") return { x: false, y: true };
2722
+ return { x: true, y: true };
2723
+ }
2724
+ /**
2725
+ * Which region the pointer is over: the plot body, the x-axis strip (below),
2726
+ * or a specific y-axis strip (in a side margin). Dragging an axis strip pans
2727
+ * just that axis.
2728
+ */
2729
+ zoneAt(px, py) {
2730
+ const region = plotRegion(this.layout());
2731
+ const inX = px >= region.left && px <= region.left + region.width;
2732
+ const inY = py >= region.top && py <= region.top + region.height;
2733
+ if (inX && py > region.top + region.height) return { type: "x" };
2734
+ if (inY && !inX) {
2735
+ const positions = this.yAxisPositions();
2736
+ let bestId = null;
2737
+ let bestD = Infinity;
2738
+ for (const [id, pos] of positions) {
2739
+ const d = Math.abs(pos.x - px);
2740
+ if (d < bestD) {
2741
+ bestD = d;
2742
+ bestId = id;
2743
+ }
2744
+ }
2745
+ if (bestId && bestD <= Y_AXIS_GAP) return { type: "y", id: bestId };
2746
+ }
2747
+ return { type: "plot" };
2748
+ }
2749
+ panX(dxPx, region) {
2750
+ const dxData = dxPx / region.width * (this.scaleX.domain[1] - this.scaleX.domain[0]);
2751
+ this.scaleX.domain = [this.scaleX.domain[0] - dxData, this.scaleX.domain[1] - dxData];
2752
+ this.autoX = false;
2753
+ }
2754
+ panY(id, dyPx, region) {
2755
+ const dyFrac = dyPx / region.height;
2756
+ for (const ya of this.yAxes.values()) {
2757
+ if (id && ya.id !== id) continue;
2758
+ const span = ya.scale.domain[1] - ya.scale.domain[0];
2759
+ const d = dyFrac * span;
2760
+ ya.scale.domain = [ya.scale.domain[0] + d, ya.scale.domain[1] + d];
2761
+ ya.auto = false;
2762
+ }
2763
+ }
2764
+ attachInteraction() {
2765
+ const el = this.axisCanvas;
2766
+ el.style.touchAction = "none";
2767
+ el.addEventListener(
2768
+ "wheel",
2769
+ (e) => {
2770
+ e.preventDefault();
2771
+ const region = plotRegion(this.layout());
2772
+ const rect = el.getBoundingClientRect();
2773
+ const nx = (e.clientX - rect.left - region.left) / region.width;
2774
+ const ny = 1 - (e.clientY - rect.top - region.top) / region.height;
2775
+ const factor = Math.exp(e.deltaY * 1e-3);
2776
+ this.zoomAround(nx, ny, factor);
2777
+ },
2778
+ { passive: false }
2779
+ );
2780
+ let panning = false;
2781
+ let selecting = false;
2782
+ let axisDrag = null;
2783
+ let lastX = 0;
2784
+ let lastY = 0;
2785
+ let startX = 0;
2786
+ let startY = 0;
2787
+ el.addEventListener("pointerdown", (e) => {
2788
+ el.setPointerCapture(e.pointerId);
2789
+ this.hoverPx = null;
2790
+ const rect = el.getBoundingClientRect();
2791
+ const px = e.clientX - rect.left;
2792
+ const py = e.clientY - rect.top;
2793
+ const zone = this.zoneAt(px, py);
2794
+ if (zone.type === "x") {
2795
+ axisDrag = "x";
2796
+ lastX = e.clientX;
2797
+ } else if (zone.type === "y") {
2798
+ axisDrag = { y: zone.id };
2799
+ lastY = e.clientY;
2800
+ } else if (this.mode === "pan") {
2801
+ panning = true;
2802
+ lastX = e.clientX;
2803
+ lastY = e.clientY;
2804
+ el.style.cursor = "grabbing";
2805
+ } else {
2806
+ selecting = true;
2807
+ startX = px;
2808
+ startY = py;
2809
+ }
2810
+ });
2811
+ el.addEventListener("pointermove", (e) => {
2812
+ const rect = el.getBoundingClientRect();
2813
+ const region = plotRegion(this.layout());
2814
+ if (axisDrag === "x") {
2815
+ this.panX(e.clientX - lastX, region);
2816
+ lastX = e.clientX;
2817
+ this.requestRender();
2818
+ } else if (axisDrag && typeof axisDrag === "object") {
2819
+ this.panY(axisDrag.y, e.clientY - lastY, region);
2820
+ lastY = e.clientY;
2821
+ this.requestRender();
2822
+ } else if (panning) {
2823
+ this.panX(e.clientX - lastX, region);
2824
+ this.panY(null, e.clientY - lastY, region);
2825
+ lastX = e.clientX;
2826
+ lastY = e.clientY;
2827
+ this.requestRender();
2828
+ } else if (selecting) {
2829
+ this.drawSelection(startX, startY, e.clientX - rect.left, e.clientY - rect.top);
2830
+ } else {
2831
+ const px = e.clientX - rect.left;
2832
+ const py = e.clientY - rect.top;
2833
+ const zone = this.zoneAt(px, py);
2834
+ if (zone.type === "x") {
2835
+ el.style.cursor = "ew-resize";
2836
+ this.setHover(null);
2837
+ } else if (zone.type === "y") {
2838
+ el.style.cursor = "ns-resize";
2839
+ this.setHover(null);
2840
+ } else {
2841
+ this.updateCursor();
2842
+ if (this.hoverEnabled) this.setHover({ x: px, y: py });
2843
+ }
2844
+ }
2845
+ });
2846
+ el.addEventListener("pointerleave", () => this.setHover(null));
2847
+ const end = (e) => {
2848
+ if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);
2849
+ if (axisDrag) {
2850
+ axisDrag = null;
2851
+ } else if (panning) {
2852
+ panning = false;
2853
+ this.updateCursor();
2854
+ } else if (selecting) {
2855
+ selecting = false;
2856
+ const rect = el.getBoundingClientRect();
2857
+ this.applySelection(startX, startY, e.clientX - rect.left, e.clientY - rect.top);
2858
+ this.selectionDiv.style.display = "none";
2859
+ }
2860
+ };
2861
+ el.addEventListener("pointerup", end);
2862
+ el.addEventListener("pointercancel", end);
2863
+ }
2864
+ setHover(px) {
2865
+ const had = this.hoverPx !== null;
2866
+ if (!px && !had) return;
2867
+ this.hoverPx = px;
2868
+ this.requestRender();
2869
+ }
2870
+ drawSelection(x0, y0, x1, y1) {
2871
+ const region = plotRegion(this.layout());
2872
+ const lock = this.axisLock();
2873
+ const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
2874
+ let left, width, top, height;
2875
+ if (lock.x) {
2876
+ const a = clamp(x0, region.left, region.left + region.width);
2877
+ const b = clamp(x1, region.left, region.left + region.width);
2878
+ left = Math.min(a, b);
2879
+ width = Math.abs(a - b);
2880
+ } else {
2881
+ left = region.left;
2882
+ width = region.width;
2883
+ }
2884
+ if (lock.y) {
2885
+ const a = clamp(y0, region.top, region.top + region.height);
2886
+ const b = clamp(y1, region.top, region.top + region.height);
2887
+ top = Math.min(a, b);
2888
+ height = Math.abs(a - b);
2889
+ } else {
2890
+ top = region.top;
2891
+ height = region.height;
2892
+ }
2893
+ Object.assign(this.selectionDiv.style, {
2894
+ display: "block",
2895
+ left: `${left}px`,
2896
+ top: `${top}px`,
2897
+ width: `${width}px`,
2898
+ height: `${height}px`
2899
+ });
2900
+ }
2901
+ applySelection(x0, y0, x1, y1) {
2902
+ const region = plotRegion(this.layout());
2903
+ const lock = this.axisLock();
2904
+ const dxPx = Math.abs(x1 - x0);
2905
+ const dyPx = Math.abs(y1 - y0);
2906
+ if (lock.x && lock.y) {
2907
+ if (dxPx < 5 && dyPx < 5) return;
2908
+ } else if (lock.x && dxPx < 5) return;
2909
+ else if (lock.y && dyPx < 5) return;
2910
+ const clamp01 = (v) => Math.max(0, Math.min(1, v));
2911
+ if (lock.x) {
2912
+ const nA = clamp01((Math.min(x0, x1) - region.left) / region.width);
2913
+ const nB = clamp01((Math.max(x0, x1) - region.left) / region.width);
2914
+ const a = this.scaleX.invert(nA);
2915
+ const b = this.scaleX.invert(nB);
2916
+ this.scaleX.domain = [Math.min(a, b), Math.max(a, b)];
2917
+ this.autoX = false;
2918
+ }
2919
+ if (lock.y) {
2920
+ const nTop = clamp01(1 - (Math.min(y0, y1) - region.top) / region.height);
2921
+ const nBot = clamp01(1 - (Math.max(y0, y1) - region.top) / region.height);
2922
+ for (const ya of this.yAxes.values()) {
2923
+ const a = ya.scale.invert(nTop);
2924
+ const b = ya.scale.invert(nBot);
2925
+ ya.scale.domain = [Math.min(a, b), Math.max(a, b)];
2926
+ ya.auto = false;
2927
+ }
2928
+ }
2929
+ this.requestRender();
2930
+ }
2931
+ zoomAround(nx, ny, factor) {
2932
+ const lock = this.axisLock();
2933
+ if (lock.x) {
2934
+ const [x0, x1] = this.scaleX.domain;
2935
+ const cx = x0 + nx * (x1 - x0);
2936
+ this.scaleX.domain = [cx + (x0 - cx) * factor, cx + (x1 - cx) * factor];
2937
+ this.autoX = false;
2938
+ }
2939
+ if (lock.y) {
2940
+ for (const ya of this.yAxes.values()) {
2941
+ const [y0, y1] = ya.scale.domain;
2942
+ const cy = y0 + ny * (y1 - y0);
2943
+ ya.scale.domain = [cy + (y0 - cy) * factor, cy + (y1 - cy) * factor];
2944
+ ya.auto = false;
2945
+ }
2946
+ }
2947
+ this.requestRender();
2948
+ }
2949
+ };
2950
+
2951
+ // src/polar/polar.ts
2952
+ var PolarPlot = class {
2953
+ constructor(container, options = {}) {
2954
+ this.entries = [];
2955
+ this.R = 1;
2956
+ this.dpr = 1;
2957
+ this.frameRequested = false;
2958
+ this.container = container;
2959
+ if (getComputedStyle(container).position === "static") container.style.position = "relative";
2960
+ this.gridCanvas = this.makeCanvas(0);
2961
+ this.dataCanvas = this.makeCanvas(1);
2962
+ this.gridCtx = this.gridCanvas.getContext("2d");
2963
+ this.dataCtx = this.dataCanvas.getContext("2d");
2964
+ const s = getSharedGL();
2965
+ this.gl = s.gl;
2966
+ this.sharedCanvas = s.canvas;
2967
+ this.theme = options.theme === "dark" ? darkTheme : options.theme === "light" || options.theme == null ? lightTheme : options.theme;
2968
+ this.toRad = options.angleUnit === "deg" ? Math.PI / 180 : 1;
2969
+ this.fixedR = options.maxRadius;
2970
+ this.margin = options.margin ?? 28;
2971
+ this.resizeObserver = new ResizeObserver(() => this.resize());
2972
+ this.resizeObserver.observe(container);
2973
+ this.resize();
2974
+ }
2975
+ makeCanvas(z) {
2976
+ const c = document.createElement("canvas");
2977
+ Object.assign(c.style, { position: "absolute", inset: "0", width: "100%", height: "100%", zIndex: String(z) });
2978
+ c.style.pointerEvents = "none";
2979
+ this.container.appendChild(c);
2980
+ return c;
2981
+ }
2982
+ toXY(theta, r, closed) {
2983
+ const n = Math.min(theta.length, r.length);
2984
+ const m = closed && n > 0 ? n + 1 : n;
2985
+ const x = new Float64Array(m), y = new Float64Array(m);
2986
+ let maxR = 0;
2987
+ for (let i = 0; i < n; i++) {
2988
+ const a = theta[i] * this.toRad, rr = r[i];
2989
+ x[i] = rr * Math.cos(a);
2990
+ y[i] = rr * Math.sin(a);
2991
+ if (Math.abs(rr) > maxR) maxR = Math.abs(rr);
2992
+ }
2993
+ if (closed && n > 0) {
2994
+ x[n] = x[0];
2995
+ y[n] = y[0];
2996
+ }
2997
+ return { x, y, maxR };
2998
+ }
2999
+ addLine(opts) {
3000
+ const closed = opts.closed ?? false;
3001
+ const { x, y, maxR } = this.toXY(opts.theta, opts.r, closed);
3002
+ const layer = new LineLayer(this.gl, { x, y, color: opts.color, width: opts.width ?? 2, decimate: false });
3003
+ const entry = { layer, closed, maxR };
3004
+ this.entries.push(entry);
3005
+ this.refit();
3006
+ return this.handle(entry);
3007
+ }
3008
+ addScatter(opts) {
3009
+ const { x, y, maxR } = this.toXY(opts.theta, opts.r, false);
3010
+ const layer = new ScatterLayer(this.gl, { x, y, color: opts.color, size: opts.size ?? 5 });
3011
+ const entry = { layer, closed: false, maxR };
3012
+ this.entries.push(entry);
3013
+ this.refit();
3014
+ return this.handle(entry);
3015
+ }
3016
+ handle(entry) {
3017
+ return {
3018
+ setData: (theta, r) => {
3019
+ const { x, y, maxR } = this.toXY(theta, r, entry.closed);
3020
+ entry.layer.setData(x, y);
3021
+ entry.maxR = maxR;
3022
+ this.refit();
3023
+ this.requestRender();
3024
+ }
3025
+ };
3026
+ }
3027
+ refit() {
3028
+ if (this.fixedR != null) {
3029
+ this.R = this.fixedR;
3030
+ return;
3031
+ }
3032
+ let m = 0;
3033
+ for (const e of this.entries) m = Math.max(m, e.maxR);
3034
+ this.R = m || 1;
3035
+ }
3036
+ destroy() {
3037
+ this.resizeObserver.disconnect();
3038
+ for (const e of this.entries) e.layer.dispose();
3039
+ this.container.removeChild(this.gridCanvas);
3040
+ this.container.removeChild(this.dataCanvas);
3041
+ }
3042
+ resize() {
3043
+ this.dpr = window.devicePixelRatio || 1;
3044
+ const w = this.container.clientWidth, h = this.container.clientHeight;
3045
+ for (const c of [this.gridCanvas, this.dataCanvas]) {
3046
+ c.width = Math.max(1, Math.round(w * this.dpr));
3047
+ c.height = Math.max(1, Math.round(h * this.dpr));
3048
+ }
3049
+ this.gridCtx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
3050
+ this.dataCtx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
3051
+ this.render();
3052
+ }
3053
+ requestRender() {
3054
+ if (this.frameRequested) return;
3055
+ this.frameRequested = true;
3056
+ requestAnimationFrame(() => {
3057
+ this.frameRequested = false;
3058
+ this.render();
3059
+ });
3060
+ }
3061
+ /** The square drawing region (CSS px). */
3062
+ square() {
3063
+ const w = this.container.clientWidth, h = this.container.clientHeight;
3064
+ const side = Math.max(1, Math.min(w, h) - this.margin * 2);
3065
+ return { cx: w / 2, cy: h / 2, side, left: (w - side) / 2, top: (h - side) / 2 };
3066
+ }
3067
+ render() {
3068
+ const sq = this.square();
3069
+ const w = this.container.clientWidth, h = this.container.clientHeight;
3070
+ const gl = this.gl;
3071
+ const devW = this.dataCanvas.width, devH = this.dataCanvas.height;
3072
+ sizeShared(gl, devW, devH);
3073
+ begin2D(gl);
3074
+ gl.clearColor(0, 0, 0, 0);
3075
+ gl.disable(gl.SCISSOR_TEST);
3076
+ gl.viewport(0, 0, devW, devH);
3077
+ gl.clear(gl.COLOR_BUFFER_BIT);
3078
+ const dpr = this.dpr;
3079
+ const vx = Math.round(sq.left * dpr), vw = Math.round(sq.side * dpr), vh = Math.round(sq.side * dpr);
3080
+ const vy = devH - Math.round((sq.top + sq.side) * dpr);
3081
+ gl.enable(gl.SCISSOR_TEST);
3082
+ gl.scissor(vx, vy, vw, vh);
3083
+ gl.viewport(vx, vy, vw, vh);
3084
+ const frame = { lo: -this.R, hi: this.R, log: false };
3085
+ for (const e of this.entries) {
3086
+ e.layer.draw({ gl, x: frame, y: frame, pixelWidth: vw, pixelHeight: vh, dpr });
3087
+ }
3088
+ gl.disable(gl.SCISSOR_TEST);
3089
+ this.dataCtx.clearRect(0, 0, w, h);
3090
+ this.dataCtx.drawImage(this.sharedCanvas, 0, 0, w, h);
3091
+ this.drawGrid(sq);
3092
+ }
3093
+ drawGrid(sq) {
3094
+ const ctx = this.gridCtx;
3095
+ const w = this.container.clientWidth, h = this.container.clientHeight;
3096
+ ctx.clearRect(0, 0, w, h);
3097
+ const pr = sq.side / 2;
3098
+ const { cx, cy } = sq;
3099
+ ctx.save();
3100
+ ctx.font = this.theme.font;
3101
+ ctx.fillStyle = this.theme.text;
3102
+ ctx.strokeStyle = this.theme.grid;
3103
+ ctx.lineWidth = 1;
3104
+ for (let deg = 0; deg < 360; deg += 30) {
3105
+ const a = deg * Math.PI / 180;
3106
+ const ex = cx + Math.cos(a) * pr, ey = cy - Math.sin(a) * pr;
3107
+ ctx.beginPath();
3108
+ ctx.moveTo(cx, cy);
3109
+ ctx.lineTo(ex, ey);
3110
+ ctx.stroke();
3111
+ const lx = cx + Math.cos(a) * (pr + 12), ly = cy - Math.sin(a) * (pr + 12);
3112
+ ctx.textAlign = "center";
3113
+ ctx.textBaseline = "middle";
3114
+ ctx.fillText(`${deg}\xB0`, lx, ly);
3115
+ }
3116
+ const ticks = autoTicks(0, this.R, 4).map((t) => t.value).filter((v) => v > 0);
3117
+ ctx.strokeStyle = this.theme.grid;
3118
+ for (const rv of ticks) {
3119
+ const rad = rv / this.R * pr;
3120
+ ctx.beginPath();
3121
+ ctx.arc(cx, cy, rad, 0, Math.PI * 2);
3122
+ ctx.stroke();
3123
+ }
3124
+ ctx.restore();
3125
+ }
3126
+ };
3127
+
3128
+ // src/plot3d/mat4.ts
3129
+ function identity() {
3130
+ const m = new Float32Array(16);
3131
+ m[0] = m[5] = m[10] = m[15] = 1;
3132
+ return m;
3133
+ }
3134
+ function multiply(a, b) {
3135
+ const o = new Float32Array(16);
3136
+ for (let c = 0; c < 4; c++) {
3137
+ for (let r = 0; r < 4; r++) {
3138
+ let s = 0;
3139
+ for (let k = 0; k < 4; k++) s += a[k * 4 + r] * b[c * 4 + k];
3140
+ o[c * 4 + r] = s;
3141
+ }
3142
+ }
3143
+ return o;
3144
+ }
3145
+ function perspective(fovy, aspect, near, far) {
3146
+ const f = 1 / Math.tan(fovy / 2);
3147
+ const nf = 1 / (near - far);
3148
+ const m = new Float32Array(16);
3149
+ m[0] = f / aspect;
3150
+ m[5] = f;
3151
+ m[10] = (far + near) * nf;
3152
+ m[11] = -1;
3153
+ m[14] = 2 * far * near * nf;
3154
+ return m;
3155
+ }
3156
+ var sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
3157
+ var cross = (a, b) => [
3158
+ a[1] * b[2] - a[2] * b[1],
3159
+ a[2] * b[0] - a[0] * b[2],
3160
+ a[0] * b[1] - a[1] * b[0]
3161
+ ];
3162
+ function normalize2(v) {
3163
+ const l = Math.hypot(v[0], v[1], v[2]) || 1;
3164
+ return [v[0] / l, v[1] / l, v[2] / l];
3165
+ }
3166
+ function lookAt(eye, center, up) {
3167
+ const z = normalize2(sub(eye, center));
3168
+ const x = normalize2(cross(up, z));
3169
+ const y = cross(z, x);
3170
+ const m = identity();
3171
+ m[0] = x[0];
3172
+ m[4] = x[1];
3173
+ m[8] = x[2];
3174
+ m[1] = y[0];
3175
+ m[5] = y[1];
3176
+ m[9] = y[2];
3177
+ m[2] = z[0];
3178
+ m[6] = z[1];
3179
+ m[10] = z[2];
3180
+ m[12] = -(x[0] * eye[0] + x[1] * eye[1] + x[2] * eye[2]);
3181
+ m[13] = -(y[0] * eye[0] + y[1] * eye[1] + y[2] * eye[2]);
3182
+ m[14] = -(z[0] * eye[0] + z[1] * eye[1] + z[2] * eye[2]);
3183
+ return m;
3184
+ }
3185
+ function transformPoint(m, x, y, z) {
3186
+ return [
3187
+ m[0] * x + m[4] * y + m[8] * z + m[12],
3188
+ m[1] * x + m[5] * y + m[9] * z + m[13],
3189
+ m[2] * x + m[6] * y + m[10] * z + m[14],
3190
+ m[3] * x + m[7] * y + m[11] * z + m[15]
3191
+ ];
3192
+ }
3193
+ function scaleTranslate(s, t) {
3194
+ const m = identity();
3195
+ m[0] = s[0];
3196
+ m[5] = s[1];
3197
+ m[10] = s[2];
3198
+ m[12] = t[0];
3199
+ m[13] = t[1];
3200
+ m[14] = t[2];
3201
+ return m;
3202
+ }
3203
+
3204
+ // src/plot3d/pointcloud.ts
3205
+ var VERT9 = (
3206
+ /* glsl */
3207
+ `#version 300 es
3208
+ precision highp float;
3209
+ layout(location = 0) in vec3 aPos;
3210
+ layout(location = 1) in vec3 aColor;
3211
+ uniform mat4 uMVP;
3212
+ uniform float uSize;
3213
+ out vec3 vColor;
3214
+ void main() {
3215
+ vColor = aColor;
3216
+ gl_Position = uMVP * vec4(aPos, 1.0);
3217
+ gl_PointSize = uSize;
3218
+ }`
3219
+ );
3220
+ var FRAG9 = (
3221
+ /* glsl */
3222
+ `#version 300 es
3223
+ precision highp float;
3224
+ in vec3 vColor;
3225
+ out vec4 outColor;
3226
+ void main() {
3227
+ vec2 d = gl_PointCoord - 0.5;
3228
+ if (length(d) > 0.5) discard;
3229
+ outColor = vec4(vColor, 1.0);
3230
+ }`
3231
+ );
3232
+ var programCache9 = /* @__PURE__ */ new WeakMap();
3233
+ function getProgram9(gl) {
3234
+ let p = programCache9.get(gl);
3235
+ if (!p) {
3236
+ p = createProgram(gl, VERT9, FRAG9);
3237
+ programCache9.set(gl, p);
3238
+ }
3239
+ return p;
3240
+ }
3241
+ var counter9 = 0;
3242
+ var PointCloudLayer = class {
3243
+ constructor(gl, opts) {
3244
+ this.id = `points3d-${counter9++}`;
3245
+ this.gl = gl;
3246
+ this.program = getProgram9(gl);
3247
+ this.size = opts.size ?? 4;
3248
+ const n = Math.min(opts.x.length, opts.y.length, opts.z.length);
3249
+ this.count = n;
3250
+ const base = opts.color != null ? Array.isArray(opts.color) ? opts.color : parseColor(opts.color) : [0.4, 0.7, 1, 1];
3251
+ const cmap = opts.colorBy ? colormap(opts.colorBy.colormap ?? "viridis") : null;
3252
+ let lo = opts.colorBy?.domain?.[0] ?? Infinity;
3253
+ let hi = opts.colorBy?.domain?.[1] ?? -Infinity;
3254
+ if (opts.colorBy && !opts.colorBy.domain) {
3255
+ const v = opts.colorBy.values;
3256
+ for (let i = 0; i < n; i++) {
3257
+ const t = v[i];
3258
+ if (t < lo) lo = t;
3259
+ if (t > hi) hi = t;
3260
+ }
3261
+ }
3262
+ const span = hi - lo || 1;
3263
+ const data = new Float32Array(n * 6);
3264
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity, minZ = Infinity, maxZ = -Infinity;
3265
+ for (let i = 0; i < n; i++) {
3266
+ const x = opts.x[i], y = opts.y[i], z = opts.z[i];
3267
+ data[i * 6] = x;
3268
+ data[i * 6 + 1] = y;
3269
+ data[i * 6 + 2] = z;
3270
+ let c;
3271
+ if (cmap && opts.colorBy) c = cmap((opts.colorBy.values[i] - lo) / span);
3272
+ else c = [base[0], base[1], base[2]];
3273
+ data[i * 6 + 3] = c[0];
3274
+ data[i * 6 + 4] = c[1];
3275
+ data[i * 6 + 5] = c[2];
3276
+ if (x < minX) minX = x;
3277
+ if (x > maxX) maxX = x;
3278
+ if (y < minY) minY = y;
3279
+ if (y > maxY) maxY = y;
3280
+ if (z < minZ) minZ = z;
3281
+ if (z > maxZ) maxZ = z;
3282
+ }
3283
+ this.b3 = { x: [minX, maxX], y: [minY, maxY], z: [minZ, maxZ] };
3284
+ const vao = gl.createVertexArray();
3285
+ const buffer = gl.createBuffer();
3286
+ this.vao = vao;
3287
+ this.buffer = buffer;
3288
+ gl.bindVertexArray(vao);
3289
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
3290
+ gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
3291
+ gl.enableVertexAttribArray(0);
3292
+ gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 24, 0);
3293
+ gl.enableVertexAttribArray(1);
3294
+ gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 12);
3295
+ gl.bindVertexArray(null);
3296
+ this.uniforms = uniformLocations(gl, this.program, ["uMVP", "uSize"]);
3297
+ }
3298
+ bounds3() {
3299
+ return this.count ? this.b3 : null;
3300
+ }
3301
+ draw(gl, mvp) {
3302
+ if (this.count === 0) return;
3303
+ gl.useProgram(this.program);
3304
+ gl.uniformMatrix4fv(this.uniforms.uMVP, false, mvp);
3305
+ gl.uniform1f(this.uniforms.uSize, this.size);
3306
+ gl.bindVertexArray(this.vao);
3307
+ gl.drawArrays(gl.POINTS, 0, this.count);
3308
+ gl.bindVertexArray(null);
3309
+ }
3310
+ dispose() {
3311
+ this.gl.deleteVertexArray(this.vao);
3312
+ this.gl.deleteBuffer(this.buffer);
3313
+ }
3314
+ };
3315
+
3316
+ // src/plot3d/surface.ts
3317
+ var VERT10 = (
3318
+ /* glsl */
3319
+ `#version 300 es
3320
+ precision highp float;
3321
+ layout(location = 0) in vec3 aPos;
3322
+ layout(location = 1) in vec3 aNormal;
3323
+ layout(location = 2) in vec3 aColor;
3324
+ uniform mat4 uMVP;
3325
+ out vec3 vColor;
3326
+ out vec3 vN;
3327
+ void main() { vColor = aColor; vN = aNormal; gl_Position = uMVP * vec4(aPos, 1.0); }`
3328
+ );
3329
+ var FRAG10 = (
3330
+ /* glsl */
3331
+ `#version 300 es
3332
+ precision highp float;
3333
+ in vec3 vColor;
3334
+ in vec3 vN;
3335
+ uniform vec3 uLightDir;
3336
+ uniform float uAmbient;
3337
+ out vec4 outColor;
3338
+ void main() {
3339
+ float d = max(dot(normalize(vN), normalize(uLightDir)), 0.0);
3340
+ float shade = uAmbient + (1.0 - uAmbient) * d;
3341
+ outColor = vec4(vColor * shade, 1.0);
3342
+ }`
3343
+ );
3344
+ var programCache10 = /* @__PURE__ */ new WeakMap();
3345
+ function getProgram10(gl) {
3346
+ let p = programCache10.get(gl);
3347
+ if (!p) {
3348
+ p = createProgram(gl, VERT10, FRAG10);
3349
+ programCache10.set(gl, p);
3350
+ }
3351
+ return p;
3352
+ }
3353
+ var counter10 = 0;
3354
+ var SurfaceLayer = class {
3355
+ constructor(gl, opts) {
3356
+ this.lightDir = [0.5, 1, 0.35];
3357
+ this.ambient = 0.35;
3358
+ this.id = `surface-${counter10++}`;
3359
+ this.gl = gl;
3360
+ this.program = getProgram10(gl);
3361
+ const { cols, rows, values } = opts;
3362
+ const [x0, x1] = opts.extentX ?? [0, cols - 1];
3363
+ const [z0, z1] = opts.extentZ ?? [0, rows - 1];
3364
+ const dxWorld = (x1 - x0) / Math.max(1, cols - 1);
3365
+ const dzWorld = (z1 - z0) / Math.max(1, rows - 1);
3366
+ let vmin = Infinity, vmax = -Infinity;
3367
+ for (let i = 0; i < values.length; i++) {
3368
+ const v = values[i];
3369
+ if (v < vmin) vmin = v;
3370
+ if (v > vmax) vmax = v;
3371
+ }
3372
+ const span = vmax - vmin || 1;
3373
+ const cmap = colormap(opts.colormap ?? "viridis");
3374
+ const wx = (c) => x0 + c / (cols - 1) * (x1 - x0);
3375
+ const wz = (r) => z0 + r / (rows - 1) * (z1 - z0);
3376
+ const at = (c, r) => values[r * cols + c];
3377
+ const normalAt = (c, r) => {
3378
+ const cl = Math.max(0, Math.min(cols - 1, c)), rl = Math.max(0, Math.min(rows - 1, r));
3379
+ const dzdx = (at(Math.min(cols - 1, cl + 1), rl) - at(Math.max(0, cl - 1), rl)) / (2 * dxWorld);
3380
+ const dzdz = (at(cl, Math.min(rows - 1, rl + 1)) - at(cl, Math.max(0, rl - 1))) / (2 * dzWorld);
3381
+ const nl = Math.hypot(-dzdx, 1, -dzdz) || 1;
3382
+ return [-dzdx / nl, 1 / nl, -dzdz / nl];
3383
+ };
3384
+ const data = [];
3385
+ const vert = (c, r) => {
3386
+ const [nx, ny, nz] = normalAt(c, r);
3387
+ const [cr, cg, cb] = cmap((at(c, r) - vmin) / span);
3388
+ data.push(wx(c), at(c, r), wz(r), nx, ny, nz, cr, cg, cb);
3389
+ };
3390
+ for (let r = 0; r < rows - 1; r++) {
3391
+ for (let c = 0; c < cols - 1; c++) {
3392
+ vert(c, r);
3393
+ vert(c + 1, r);
3394
+ vert(c + 1, r + 1);
3395
+ vert(c, r);
3396
+ vert(c + 1, r + 1);
3397
+ vert(c, r + 1);
3398
+ }
3399
+ }
3400
+ this.vertexCount = data.length / 9;
3401
+ this.b3 = { x: [x0, x1], y: [vmin, vmax], z: [z0, z1] };
3402
+ const vao = gl.createVertexArray();
3403
+ const buffer = gl.createBuffer();
3404
+ this.vao = vao;
3405
+ this.buffer = buffer;
3406
+ gl.bindVertexArray(vao);
3407
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
3408
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
3409
+ gl.enableVertexAttribArray(0);
3410
+ gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 36, 0);
3411
+ gl.enableVertexAttribArray(1);
3412
+ gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 36, 12);
3413
+ gl.enableVertexAttribArray(2);
3414
+ gl.vertexAttribPointer(2, 3, gl.FLOAT, false, 36, 24);
3415
+ gl.bindVertexArray(null);
3416
+ this.uniforms = uniformLocations(gl, this.program, ["uMVP", "uLightDir", "uAmbient"]);
3417
+ }
3418
+ bounds3() {
3419
+ return this.b3;
3420
+ }
3421
+ /** Set the light direction (world space) and ambient term (0..1). */
3422
+ setLight(dir, ambient) {
3423
+ this.lightDir = dir;
3424
+ this.ambient = ambient;
3425
+ }
3426
+ draw(gl, mvp) {
3427
+ gl.useProgram(this.program);
3428
+ gl.uniformMatrix4fv(this.uniforms.uMVP, false, mvp);
3429
+ gl.uniform3f(this.uniforms.uLightDir, this.lightDir[0], this.lightDir[1], this.lightDir[2]);
3430
+ gl.uniform1f(this.uniforms.uAmbient, this.ambient);
3431
+ gl.bindVertexArray(this.vao);
3432
+ gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
3433
+ gl.bindVertexArray(null);
3434
+ }
3435
+ dispose() {
3436
+ this.gl.deleteVertexArray(this.vao);
3437
+ this.gl.deleteBuffer(this.buffer);
3438
+ }
3439
+ };
3440
+
3441
+ // src/plot3d/plot3d.ts
3442
+ var LINE_VERT = (
3443
+ /* glsl */
3444
+ `#version 300 es
3445
+ precision highp float;
3446
+ layout(location = 0) in vec3 aPos;
3447
+ uniform mat4 uVP;
3448
+ void main() { gl_Position = uVP * vec4(aPos, 1.0); }`
3449
+ );
3450
+ var LINE_FRAG = (
3451
+ /* glsl */
3452
+ `#version 300 es
3453
+ precision highp float;
3454
+ uniform vec4 uColor;
3455
+ out vec4 outColor;
3456
+ void main() { outColor = uColor; }`
3457
+ );
3458
+ var BOX_EDGES = new Float32Array([
3459
+ -1,
3460
+ -1,
3461
+ -1,
3462
+ 1,
3463
+ -1,
3464
+ -1,
3465
+ 1,
3466
+ -1,
3467
+ -1,
3468
+ 1,
3469
+ 1,
3470
+ -1,
3471
+ 1,
3472
+ 1,
3473
+ -1,
3474
+ -1,
3475
+ 1,
3476
+ -1,
3477
+ -1,
3478
+ 1,
3479
+ -1,
3480
+ -1,
3481
+ -1,
3482
+ -1,
3483
+ -1,
3484
+ -1,
3485
+ 1,
3486
+ 1,
3487
+ -1,
3488
+ 1,
3489
+ 1,
3490
+ -1,
3491
+ 1,
3492
+ 1,
3493
+ 1,
3494
+ 1,
3495
+ 1,
3496
+ 1,
3497
+ 1,
3498
+ -1,
3499
+ 1,
3500
+ 1,
3501
+ -1,
3502
+ 1,
3503
+ 1,
3504
+ -1,
3505
+ -1,
3506
+ 1,
3507
+ -1,
3508
+ -1,
3509
+ -1,
3510
+ -1,
3511
+ -1,
3512
+ 1,
3513
+ 1,
3514
+ -1,
3515
+ -1,
3516
+ 1,
3517
+ -1,
3518
+ 1,
3519
+ 1,
3520
+ 1,
3521
+ -1,
3522
+ 1,
3523
+ 1,
3524
+ 1,
3525
+ -1,
3526
+ 1,
3527
+ -1,
3528
+ -1,
3529
+ 1,
3530
+ 1
3531
+ ]);
3532
+ var Plot3D = class {
3533
+ constructor(container, options = {}) {
3534
+ this.layers = [];
3535
+ this.normalize = scaleTranslate([1, 1, 1], [0, 0, 0]);
3536
+ this.dataBounds = null;
3537
+ this.dpr = 1;
3538
+ this.frameRequested = false;
3539
+ this.tickCount = 0;
3540
+ this.labels = [];
3541
+ // Lighting.
3542
+ this.lightAz = 0.9;
3543
+ this.lightEl = 0.9;
3544
+ this.ambient = 0.35;
3545
+ this.controlsEl = null;
3546
+ this.container = container;
3547
+ if (getComputedStyle(container).position === "static") container.style.position = "relative";
3548
+ this.canvas = document.createElement("canvas");
3549
+ Object.assign(this.canvas.style, { position: "absolute", inset: "0", width: "100%", height: "100%" });
3550
+ container.appendChild(this.canvas);
3551
+ this.displayCtx = this.canvas.getContext("2d");
3552
+ const s = getSharedGL();
3553
+ this.gl = s.gl;
3554
+ this.sharedCanvas = s.canvas;
3555
+ this.bg = options.background ?? [0.04, 0.06, 0.13, 1];
3556
+ this.azimuth = options.azimuth ?? 0.7;
3557
+ this.elevation = options.elevation ?? 0.5;
3558
+ this.distance = options.distance ?? 3.6;
3559
+ this.axisLabels = options.axisLabels ?? {};
3560
+ this.lineProgram = createProgram(this.gl, LINE_VERT, LINE_FRAG);
3561
+ this.lineUniforms = uniformLocations(this.gl, this.lineProgram, ["uVP", "uColor"]);
3562
+ this.boxVao = this.gl.createVertexArray();
3563
+ this.boxBuf = this.gl.createBuffer();
3564
+ this.bindLineVao(this.boxVao, this.boxBuf, BOX_EDGES);
3565
+ this.tickVao = this.gl.createVertexArray();
3566
+ this.tickBuf = this.gl.createBuffer();
3567
+ this.bindLineVao(this.tickVao, this.tickBuf, new Float32Array(0));
3568
+ if (options.lightControls) this.buildControls();
3569
+ this.resizeObserver = new ResizeObserver(() => this.resize());
3570
+ this.resizeObserver.observe(container);
3571
+ this.attachControls();
3572
+ this.resize();
3573
+ }
3574
+ bindLineVao(vao, buf, data) {
3575
+ const gl = this.gl;
3576
+ gl.bindVertexArray(vao);
3577
+ gl.bindBuffer(gl.ARRAY_BUFFER, buf);
3578
+ gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
3579
+ gl.enableVertexAttribArray(0);
3580
+ gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
3581
+ gl.bindVertexArray(null);
3582
+ }
3583
+ addSurface(opts) {
3584
+ const l = new SurfaceLayer(this.gl, opts);
3585
+ l.setLight(this.lightDir(), this.ambient);
3586
+ this.layers.push(l);
3587
+ this.recompute();
3588
+ this.requestRender();
3589
+ return l;
3590
+ }
3591
+ addPointCloud(opts) {
3592
+ const l = new PointCloudLayer(this.gl, opts);
3593
+ this.layers.push(l);
3594
+ this.recompute();
3595
+ this.requestRender();
3596
+ return l;
3597
+ }
3598
+ /** Update the light direction (azimuth/elevation, radians) and ambient (0..1). */
3599
+ setLight(params) {
3600
+ if (params.azimuth != null) this.lightAz = params.azimuth;
3601
+ if (params.elevation != null) this.lightEl = params.elevation;
3602
+ if (params.ambient != null) this.ambient = params.ambient;
3603
+ const dir = this.lightDir();
3604
+ for (const l of this.layers) {
3605
+ if ("setLight" in l) l.setLight(dir, this.ambient);
3606
+ }
3607
+ this.requestRender();
3608
+ }
3609
+ lightDir() {
3610
+ const el = this.lightEl, az = this.lightAz;
3611
+ return [Math.cos(el) * Math.cos(az), Math.sin(el), Math.cos(el) * Math.sin(az)];
3612
+ }
3613
+ destroy() {
3614
+ this.resizeObserver.disconnect();
3615
+ for (const l of this.layers) l.dispose();
3616
+ this.gl.deleteProgram(this.lineProgram);
3617
+ this.gl.deleteVertexArray(this.boxVao);
3618
+ this.gl.deleteVertexArray(this.tickVao);
3619
+ this.gl.deleteBuffer(this.boxBuf);
3620
+ this.gl.deleteBuffer(this.tickBuf);
3621
+ this.controlsEl?.remove();
3622
+ this.container.removeChild(this.canvas);
3623
+ }
3624
+ recompute() {
3625
+ let b = null;
3626
+ for (const l of this.layers) {
3627
+ const lb = l.bounds3();
3628
+ if (!lb) continue;
3629
+ b = b ? {
3630
+ x: [Math.min(b.x[0], lb.x[0]), Math.max(b.x[1], lb.x[1])],
3631
+ y: [Math.min(b.y[0], lb.y[0]), Math.max(b.y[1], lb.y[1])],
3632
+ z: [Math.min(b.z[0], lb.z[0]), Math.max(b.z[1], lb.z[1])]
3633
+ } : lb;
3634
+ }
3635
+ if (!b) return;
3636
+ this.dataBounds = b;
3637
+ const axis = (r) => {
3638
+ const span = r[1] - r[0];
3639
+ return span === 0 ? [1, -r[0]] : [2 / span, -(r[1] + r[0]) / span];
3640
+ };
3641
+ const [sx, tx] = axis(b.x), [sy, ty] = axis(b.y), [sz, tz] = axis(b.z);
3642
+ this.normalize = scaleTranslate([sx, sy, sz], [tx, ty, tz]);
3643
+ this.buildAxes();
3644
+ }
3645
+ /** Build tick-mark line geometry + tick/title labels from the data bounds. */
3646
+ buildAxes() {
3647
+ const b = this.dataBounds;
3648
+ if (!b) return;
3649
+ const cube = (r, v) => 2 * (v - r[0]) / (r[1] - r[0] || 1) - 1;
3650
+ const seg = [];
3651
+ const labels = [];
3652
+ const mark = (x0, y0, z0, x1, y1, z1) => seg.push(x0, y0, z0, x1, y1, z1);
3653
+ for (const t of autoTicks(b.x[0], b.x[1], 5)) {
3654
+ const cx = cube(b.x, t.value);
3655
+ mark(cx, -1, -1, cx, -1, -1.06);
3656
+ labels.push({ p: [cx, -1, -1.16], text: defaultFormat(t.value) });
3657
+ }
3658
+ for (const t of autoTicks(b.y[0], b.y[1], 5)) {
3659
+ const cy = cube(b.y, t.value);
3660
+ mark(-1, cy, -1, -1.06, cy, -1);
3661
+ labels.push({ p: [-1.16, cy, -1], text: defaultFormat(t.value) });
3662
+ }
3663
+ for (const t of autoTicks(b.z[0], b.z[1], 5)) {
3664
+ const cz = cube(b.z, t.value);
3665
+ mark(-1, -1, cz, -1.06, -1, cz);
3666
+ labels.push({ p: [-1.16, -1, cz], text: defaultFormat(t.value) });
3667
+ }
3668
+ if (this.axisLabels.x) labels.push({ p: [0, -1.25, -1.3], text: this.axisLabels.x, title: true });
3669
+ if (this.axisLabels.y) labels.push({ p: [-1.35, 0, -1], text: this.axisLabels.y, title: true });
3670
+ if (this.axisLabels.z) labels.push({ p: [-1.3, -1.25, 0], text: this.axisLabels.z, title: true });
3671
+ this.labels = labels;
3672
+ this.tickCount = seg.length / 3;
3673
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.tickBuf);
3674
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(seg), this.gl.STATIC_DRAW);
3675
+ }
3676
+ resize() {
3677
+ this.dpr = window.devicePixelRatio || 1;
3678
+ const w = this.container.clientWidth, h = this.container.clientHeight;
3679
+ this.canvas.width = Math.max(1, Math.round(w * this.dpr));
3680
+ this.canvas.height = Math.max(1, Math.round(h * this.dpr));
3681
+ this.render();
3682
+ }
3683
+ requestRender() {
3684
+ if (this.frameRequested) return;
3685
+ this.frameRequested = true;
3686
+ requestAnimationFrame(() => {
3687
+ this.frameRequested = false;
3688
+ this.render();
3689
+ });
3690
+ }
3691
+ viewProj() {
3692
+ const aspect = this.canvas.width / Math.max(1, this.canvas.height);
3693
+ const proj = perspective(50 * Math.PI / 180, aspect, 0.01, 100);
3694
+ const el = Math.max(-1.5, Math.min(1.5, this.elevation));
3695
+ const eye = [
3696
+ this.distance * Math.cos(el) * Math.sin(this.azimuth),
3697
+ this.distance * Math.sin(el),
3698
+ this.distance * Math.cos(el) * Math.cos(this.azimuth)
3699
+ ];
3700
+ const view = lookAt(eye, [0, 0, 0], [0, 1, 0]);
3701
+ const vp = multiply(proj, view);
3702
+ return { vp, mvp: multiply(vp, this.normalize) };
3703
+ }
3704
+ render() {
3705
+ const gl = this.gl;
3706
+ const w = this.canvas.width, h = this.canvas.height;
3707
+ sizeShared(gl, w, h);
3708
+ begin3D(gl);
3709
+ gl.viewport(0, 0, w, h);
3710
+ gl.clearColor(this.bg[0], this.bg[1], this.bg[2], this.bg[3]);
3711
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
3712
+ const { vp, mvp } = this.viewProj();
3713
+ gl.useProgram(this.lineProgram);
3714
+ gl.uniformMatrix4fv(this.lineUniforms.uVP, false, vp);
3715
+ gl.uniform4f(this.lineUniforms.uColor, 0.6, 0.65, 0.75, 0.4);
3716
+ gl.bindVertexArray(this.boxVao);
3717
+ gl.drawArrays(gl.LINES, 0, BOX_EDGES.length / 3);
3718
+ if (this.tickCount > 0) {
3719
+ gl.uniform4f(this.lineUniforms.uColor, 0.75, 0.8, 0.9, 0.8);
3720
+ gl.bindVertexArray(this.tickVao);
3721
+ gl.drawArrays(gl.LINES, 0, this.tickCount);
3722
+ }
3723
+ gl.bindVertexArray(null);
3724
+ for (const l of this.layers) l.draw(gl, mvp);
3725
+ this.displayCtx.clearRect(0, 0, w, h);
3726
+ this.displayCtx.drawImage(this.sharedCanvas, 0, 0);
3727
+ this.drawLabels(vp, w, h);
3728
+ }
3729
+ drawLabels(vp, w, h) {
3730
+ if (this.labels.length === 0) return;
3731
+ const ctx = this.displayCtx;
3732
+ ctx.save();
3733
+ ctx.textAlign = "center";
3734
+ ctx.textBaseline = "middle";
3735
+ ctx.fillStyle = "#cbd5e1";
3736
+ for (const lab of this.labels) {
3737
+ const c = transformPoint(vp, lab.p[0], lab.p[1], lab.p[2]);
3738
+ if (c[3] <= 0) continue;
3739
+ const sx = (c[0] / c[3] * 0.5 + 0.5) * w;
3740
+ const sy = (1 - (c[1] / c[3] * 0.5 + 0.5)) * h;
3741
+ ctx.font = `${(lab.title ? 13 : 11) * this.dpr}px system-ui, sans-serif`;
3742
+ ctx.fillStyle = lab.title ? "#e2e8f0" : "#94a3b8";
3743
+ ctx.fillText(lab.text, sx, sy);
3744
+ }
3745
+ ctx.restore();
3746
+ }
3747
+ attachControls() {
3748
+ const el = this.canvas;
3749
+ el.style.touchAction = "none";
3750
+ el.style.cursor = "grab";
3751
+ let dragging = false, lastX = 0, lastY = 0;
3752
+ el.addEventListener("pointerdown", (e) => {
3753
+ dragging = true;
3754
+ lastX = e.clientX;
3755
+ lastY = e.clientY;
3756
+ el.setPointerCapture(e.pointerId);
3757
+ el.style.cursor = "grabbing";
3758
+ });
3759
+ el.addEventListener("pointermove", (e) => {
3760
+ if (!dragging) return;
3761
+ this.azimuth -= (e.clientX - lastX) * 0.01;
3762
+ this.elevation += (e.clientY - lastY) * 0.01;
3763
+ lastX = e.clientX;
3764
+ lastY = e.clientY;
3765
+ this.requestRender();
3766
+ });
3767
+ const end = (e) => {
3768
+ dragging = false;
3769
+ if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);
3770
+ el.style.cursor = "grab";
3771
+ };
3772
+ el.addEventListener("pointerup", end);
3773
+ el.addEventListener("pointercancel", end);
3774
+ el.addEventListener("wheel", (e) => {
3775
+ e.preventDefault();
3776
+ this.distance = Math.max(0.5, Math.min(20, this.distance * Math.exp(e.deltaY * 1e-3)));
3777
+ this.requestRender();
3778
+ }, { passive: false });
3779
+ }
3780
+ buildControls() {
3781
+ const panel = document.createElement("div");
3782
+ Object.assign(panel.style, {
3783
+ position: "absolute",
3784
+ top: "8px",
3785
+ left: "8px",
3786
+ zIndex: "5",
3787
+ display: "flex",
3788
+ flexDirection: "column",
3789
+ gap: "4px",
3790
+ padding: "8px 10px",
3791
+ borderRadius: "8px",
3792
+ background: "rgba(15,23,42,0.8)",
3793
+ border: "1px solid rgba(148,163,184,0.25)",
3794
+ font: "11px system-ui, sans-serif",
3795
+ color: "#cbd5e1",
3796
+ backdropFilter: "blur(6px)"
3797
+ });
3798
+ const row = (label, min, max, val, on) => {
3799
+ const wrap = document.createElement("label");
3800
+ wrap.style.display = "flex";
3801
+ wrap.style.alignItems = "center";
3802
+ wrap.style.gap = "6px";
3803
+ const span = document.createElement("span");
3804
+ span.textContent = label;
3805
+ span.style.width = "48px";
3806
+ const input = document.createElement("input");
3807
+ input.type = "range";
3808
+ input.min = String(min);
3809
+ input.max = String(max);
3810
+ input.value = String(val);
3811
+ input.style.width = "90px";
3812
+ input.addEventListener("input", () => on(parseFloat(input.value)));
3813
+ wrap.appendChild(span);
3814
+ wrap.appendChild(input);
3815
+ panel.appendChild(wrap);
3816
+ };
3817
+ row("light", 0, 360, this.lightAz * 180 / Math.PI, (v) => this.setLight({ azimuth: v * Math.PI / 180 }));
3818
+ row("ambient", 0, 100, this.ambient * 100, (v) => this.setLight({ ambient: v / 100 }));
3819
+ this.container.appendChild(panel);
3820
+ this.controlsEl = panel;
3821
+ }
3822
+ };
3823
+ export {
3824
+ AreaLayer,
3825
+ Axis,
3826
+ BarLayer,
3827
+ BoxLayer,
3828
+ ContourLayer,
3829
+ HeatmapLayer,
3830
+ HexbinLayer,
3831
+ LineLayer,
3832
+ LinearScale,
3833
+ LogScale,
3834
+ Plot,
3835
+ Plot3D,
3836
+ PointCloudLayer,
3837
+ PolarPlot,
3838
+ ScatterLayer,
3839
+ SurfaceLayer,
3840
+ TimeScale,
3841
+ autoTicks,
3842
+ boxStats,
3843
+ colormap,
3844
+ createToolbar,
3845
+ darkTheme,
3846
+ defaultFormat,
3847
+ fft,
3848
+ histogram,
3849
+ kde,
3850
+ lightTheme,
3851
+ makeScale,
3852
+ parseColor,
3853
+ quantileSorted,
3854
+ resolveTicks,
3855
+ spectrogram,
3856
+ toColorCss,
3857
+ withMinorTicks
3858
+ };
3859
+ //# sourceMappingURL=index.js.map