@photonviz/core 0.2.1 → 0.3.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 CHANGED
@@ -1,11 +1,11 @@
1
1
  // src/axes/ticks.ts
2
2
  function niceStep(rawStep) {
3
3
  const mag = Math.pow(10, Math.floor(Math.log10(rawStep)));
4
- const norm = rawStep / mag;
4
+ const norm2 = rawStep / mag;
5
5
  let nice;
6
- if (norm < 1.5) nice = 1;
7
- else if (norm < 3) nice = 2;
8
- else if (norm < 7) nice = 5;
6
+ if (norm2 < 1.5) nice = 1;
7
+ else if (norm2 < 3) nice = 2;
8
+ else if (norm2 < 7) nice = 5;
9
9
  else nice = 10;
10
10
  return nice * mag;
11
11
  }
@@ -408,6 +408,7 @@ var BarLayer = class {
408
408
  this.count = n;
409
409
  this.barWidth = opts.width ?? medianSpacing(opts.x, n) * 0.8;
410
410
  this.offset = opts.offset ?? 0;
411
+ this.orientation = opts.orientation ?? "v";
411
412
  const rects = this.buildRects(opts.x, opts.y, opts.base, n);
412
413
  const vao = gl.createVertexArray();
413
414
  this.vao = vao;
@@ -429,23 +430,30 @@ var BarLayer = class {
429
430
  }
430
431
  buildRects(x, y, base, n) {
431
432
  const width = this.barWidth, off = this.offset;
433
+ const horizontal = this.orientation === "h";
432
434
  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 posRef = n > 0 ? x[0] : 0;
436
+ const valRef = n > 0 ? y[0] : 0;
437
+ this.xRef = horizontal ? valRef : posRef;
438
+ this.yRef = horizontal ? posRef : valRef;
435
439
  const rects = new Float32Array(n * 4);
436
440
  let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
437
441
  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);
442
+ const cpos = x[i] + off;
443
+ const p0 = cpos - width / 2, p1 = cpos + width / 2;
444
+ const b = baseAt(i), val = y[i];
445
+ const ax0 = horizontal ? b : p0;
446
+ const ax1 = horizontal ? val : p1;
447
+ const ay0 = horizontal ? p0 : b;
448
+ const ay1 = horizontal ? p1 : val;
449
+ rects[i * 4] = ax0 - this.xRef;
450
+ rects[i * 4 + 1] = ay0 - this.yRef;
451
+ rects[i * 4 + 2] = ax1 - this.xRef;
452
+ rects[i * 4 + 3] = ay1 - this.yRef;
453
+ minX = Math.min(minX, ax0, ax1);
454
+ maxX = Math.max(maxX, ax0, ax1);
455
+ minY = Math.min(minY, ay0, ay1);
456
+ maxY = Math.max(maxY, ay0, ay1);
449
457
  }
450
458
  this.xBounds = [minX, maxX];
451
459
  this.yBounds = [minY, maxY];
@@ -638,7 +646,7 @@ function kde(values, lo, hi, points = 64, bandwidth) {
638
646
  const h = bandwidth ?? 1.06 * sd * Math.pow(Math.max(1, n), -0.2);
639
647
  const xs = new Float64Array(points);
640
648
  const ys = new Float64Array(points);
641
- const norm = 1 / (n * h * Math.sqrt(2 * Math.PI));
649
+ const norm2 = 1 / (n * h * Math.sqrt(2 * Math.PI));
642
650
  for (let p = 0; p < points; p++) {
643
651
  const x = lo + (hi - lo) * p / (points - 1);
644
652
  let sum = 0;
@@ -647,7 +655,7 @@ function kde(values, lo, hi, points = 64, bandwidth) {
647
655
  sum += Math.exp(-0.5 * u * u);
648
656
  }
649
657
  xs[p] = x;
650
- ys[p] = sum * norm;
658
+ ys[p] = sum * norm2;
651
659
  }
652
660
  return { xs, ys };
653
661
  }
@@ -1082,17 +1090,38 @@ var ANCHORS = {
1082
1090
  [0.95, 0.95, 0.95]
1083
1091
  ]
1084
1092
  };
1085
- function colormap(name = "viridis") {
1093
+ var LUT_SIZE = 256;
1094
+ var lutCache = /* @__PURE__ */ new Map();
1095
+ function buildLUT(name) {
1086
1096
  const anchors = ANCHORS[name];
1087
1097
  const last = anchors.length - 1;
1088
- return (t) => {
1089
- const clamped = t <= 0 ? 0 : t >= 1 ? 1 : t;
1090
- const pos = clamped * last;
1098
+ const lut = new Float32Array(LUT_SIZE * 3);
1099
+ for (let s = 0; s < LUT_SIZE; s++) {
1100
+ const pos = s / (LUT_SIZE - 1) * last;
1091
1101
  const i = Math.min(last - 1, Math.floor(pos));
1092
1102
  const f = pos - i;
1093
1103
  const a = anchors[i];
1094
1104
  const b = anchors[i + 1];
1095
- return [a[0] + (b[0] - a[0]) * f, a[1] + (b[1] - a[1]) * f, a[2] + (b[2] - a[2]) * f];
1105
+ lut[s * 3] = a[0] + (b[0] - a[0]) * f;
1106
+ lut[s * 3 + 1] = a[1] + (b[1] - a[1]) * f;
1107
+ lut[s * 3 + 2] = a[2] + (b[2] - a[2]) * f;
1108
+ }
1109
+ return lut;
1110
+ }
1111
+ function colormapLUT(name = "viridis") {
1112
+ let lut = lutCache.get(name);
1113
+ if (!lut) {
1114
+ lut = buildLUT(name);
1115
+ lutCache.set(name, lut);
1116
+ }
1117
+ return lut;
1118
+ }
1119
+ function colormap(name = "viridis") {
1120
+ const lut = colormapLUT(name);
1121
+ return (t) => {
1122
+ const c = t <= 0 ? 0 : t >= 1 ? 1 : t;
1123
+ const j = (c * (LUT_SIZE - 1) | 0) * 3;
1124
+ return [lut[j], lut[j + 1], lut[j + 2]];
1096
1125
  };
1097
1126
  }
1098
1127
 
@@ -1450,6 +1479,148 @@ var ErrorBarLayer = class {
1450
1479
  }
1451
1480
  };
1452
1481
 
1482
+ // src/layers/graph.ts
1483
+ var EDGE_VERT = (
1484
+ /* glsl */
1485
+ `#version 300 es
1486
+ precision highp float;
1487
+ layout(location = 0) in vec2 aPos;
1488
+ ${TRANSFORM_GLSL}
1489
+ void main() { gl_Position = vec4(dataToClip(aPos), 0.0, 1.0); }`
1490
+ );
1491
+ var SOLID_FRAG2 = (
1492
+ /* glsl */
1493
+ `#version 300 es
1494
+ precision highp float;
1495
+ uniform vec4 uColor;
1496
+ out vec4 outColor;
1497
+ void main() { outColor = vec4(uColor.rgb * uColor.a, uColor.a); }`
1498
+ );
1499
+ var NODE_VERT = (
1500
+ /* glsl */
1501
+ `#version 300 es
1502
+ precision highp float;
1503
+ layout(location = 0) in vec2 aPos;
1504
+ uniform float uSize;
1505
+ ${TRANSFORM_GLSL}
1506
+ void main() {
1507
+ gl_Position = vec4(dataToClip(aPos), 0.0, 1.0);
1508
+ gl_PointSize = uSize;
1509
+ }`
1510
+ );
1511
+ var NODE_FRAG = (
1512
+ /* glsl */
1513
+ `#version 300 es
1514
+ precision highp float;
1515
+ uniform vec4 uColor;
1516
+ out vec4 outColor;
1517
+ void main() {
1518
+ vec2 p = gl_PointCoord * 2.0 - 1.0;
1519
+ float r = length(p);
1520
+ if (r > 1.0) discard;
1521
+ float a = smoothstep(1.0, 0.82, r);
1522
+ outColor = vec4(uColor.rgb * uColor.a * a, uColor.a * a);
1523
+ }`
1524
+ );
1525
+ var programCache5 = /* @__PURE__ */ new WeakMap();
1526
+ function getPrograms(gl) {
1527
+ let p = programCache5.get(gl);
1528
+ if (!p) {
1529
+ p = { edge: createProgram(gl, EDGE_VERT, SOLID_FRAG2), node: createProgram(gl, NODE_VERT, NODE_FRAG) };
1530
+ programCache5.set(gl, p);
1531
+ }
1532
+ return p;
1533
+ }
1534
+ var counter7 = 0;
1535
+ var GraphLayer = class {
1536
+ constructor(gl, opts) {
1537
+ this.buffers = [];
1538
+ this.xRef = 0;
1539
+ this.yRef = 0;
1540
+ this.xBounds = [0, 0];
1541
+ this.yBounds = [0, 0];
1542
+ this.id = `graph-${counter7++}`;
1543
+ this.gl = gl;
1544
+ this.progs = getPrograms(gl);
1545
+ this.name = opts.name ?? this.id;
1546
+ this.yAxis = opts.yAxis ?? "y";
1547
+ const nc = opts.nodeColor ?? "#60a5fa";
1548
+ this.nodeColor = Array.isArray(nc) ? nc : parseColor(nc);
1549
+ this.colorCss = typeof nc === "string" ? nc : toColorCss(this.nodeColor);
1550
+ const ec = opts.edgeColor ?? "rgba(148,163,184,0.5)";
1551
+ this.edgeColor = Array.isArray(ec) ? ec : parseColor(ec);
1552
+ this.nodeSize = opts.nodeSize ?? 10;
1553
+ const n = Math.min(opts.x.length, opts.y.length);
1554
+ this.nodeCount = n;
1555
+ this.xRef = n > 0 ? opts.x[0] : 0;
1556
+ this.yRef = n > 0 ? opts.y[0] : 0;
1557
+ const nodePos = new Float32Array(n * 2);
1558
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
1559
+ for (let i = 0; i < n; i++) {
1560
+ const px = opts.x[i], py = opts.y[i];
1561
+ nodePos[i * 2] = px - this.xRef;
1562
+ nodePos[i * 2 + 1] = py - this.yRef;
1563
+ if (px < minX) minX = px;
1564
+ if (px > maxX) maxX = px;
1565
+ if (py < minY) minY = py;
1566
+ if (py > maxY) maxY = py;
1567
+ }
1568
+ this.xBounds = [minX, maxX];
1569
+ this.yBounds = [minY, maxY];
1570
+ const edgePos = [];
1571
+ for (const [a, b] of opts.edges) {
1572
+ if (a < 0 || b < 0 || a >= n || b >= n) continue;
1573
+ edgePos.push(opts.x[a] - this.xRef, opts.y[a] - this.yRef, opts.x[b] - this.xRef, opts.y[b] - this.yRef);
1574
+ }
1575
+ this.edgeVerts = edgePos.length / 2;
1576
+ const nodeBuf = gl.createBuffer();
1577
+ const edgeBuf = gl.createBuffer();
1578
+ this.buffers = [nodeBuf, edgeBuf];
1579
+ this.nodeVao = gl.createVertexArray();
1580
+ gl.bindVertexArray(this.nodeVao);
1581
+ gl.bindBuffer(gl.ARRAY_BUFFER, nodeBuf);
1582
+ gl.bufferData(gl.ARRAY_BUFFER, nodePos, gl.STATIC_DRAW);
1583
+ gl.enableVertexAttribArray(0);
1584
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
1585
+ this.edgeVao = gl.createVertexArray();
1586
+ gl.bindVertexArray(this.edgeVao);
1587
+ gl.bindBuffer(gl.ARRAY_BUFFER, edgeBuf);
1588
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(edgePos), gl.STATIC_DRAW);
1589
+ gl.enableVertexAttribArray(0);
1590
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
1591
+ gl.bindVertexArray(null);
1592
+ this.edgeUniforms = uniformLocations(gl, this.progs.edge, [...TRANSFORM_UNIFORMS, "uColor"]);
1593
+ this.nodeUniforms = uniformLocations(gl, this.progs.node, [...TRANSFORM_UNIFORMS, "uColor", "uSize"]);
1594
+ }
1595
+ bounds() {
1596
+ if (this.nodeCount === 0) return null;
1597
+ return { x: this.xBounds, y: this.yBounds };
1598
+ }
1599
+ draw(state) {
1600
+ if (this.nodeCount === 0) return;
1601
+ const gl = state.gl;
1602
+ if (this.edgeVerts > 0) {
1603
+ gl.useProgram(this.progs.edge);
1604
+ setTransformUniforms(gl, this.edgeUniforms, state.x, state.y, this.xRef, this.yRef);
1605
+ gl.uniform4f(this.edgeUniforms.uColor, this.edgeColor[0], this.edgeColor[1], this.edgeColor[2], this.edgeColor[3]);
1606
+ gl.bindVertexArray(this.edgeVao);
1607
+ gl.drawArrays(gl.LINES, 0, this.edgeVerts);
1608
+ }
1609
+ gl.useProgram(this.progs.node);
1610
+ setTransformUniforms(gl, this.nodeUniforms, state.x, state.y, this.xRef, this.yRef);
1611
+ gl.uniform4f(this.nodeUniforms.uColor, this.nodeColor[0], this.nodeColor[1], this.nodeColor[2], this.nodeColor[3]);
1612
+ gl.uniform1f(this.nodeUniforms.uSize, this.nodeSize * state.dpr);
1613
+ gl.bindVertexArray(this.nodeVao);
1614
+ gl.drawArrays(gl.POINTS, 0, this.nodeCount);
1615
+ gl.bindVertexArray(null);
1616
+ }
1617
+ dispose() {
1618
+ this.gl.deleteVertexArray(this.nodeVao);
1619
+ this.gl.deleteVertexArray(this.edgeVao);
1620
+ for (const b of this.buffers) this.gl.deleteBuffer(b);
1621
+ }
1622
+ };
1623
+
1453
1624
  // src/layers/heatmap.ts
1454
1625
  var VERT5 = (
1455
1626
  /* glsl */
@@ -1476,19 +1647,19 @@ void main() {
1476
1647
  outColor = vec4(c.rgb * c.a, c.a);
1477
1648
  }`
1478
1649
  );
1479
- var programCache5 = /* @__PURE__ */ new WeakMap();
1650
+ var programCache6 = /* @__PURE__ */ new WeakMap();
1480
1651
  function getProgram5(gl) {
1481
- let p = programCache5.get(gl);
1652
+ let p = programCache6.get(gl);
1482
1653
  if (!p) {
1483
1654
  p = createProgram(gl, VERT5, FRAG6);
1484
- programCache5.set(gl, p);
1655
+ programCache6.set(gl, p);
1485
1656
  }
1486
1657
  return p;
1487
1658
  }
1488
- var counter7 = 0;
1659
+ var counter8 = 0;
1489
1660
  var HeatmapLayer = class {
1490
1661
  constructor(gl, opts) {
1491
- this.id = `heatmap-${counter7++}`;
1662
+ this.id = `heatmap-${counter8++}`;
1492
1663
  this.gl = gl;
1493
1664
  this.program = getProgram5(gl);
1494
1665
  this.yAxis = opts.yAxis ?? "y";
@@ -1498,7 +1669,7 @@ var HeatmapLayer = class {
1498
1669
  this.xRef = x0;
1499
1670
  this.yRef = y0;
1500
1671
  const { cols, rows, values } = opts;
1501
- const cmap = colormap(opts.colormap ?? "viridis");
1672
+ const lut = colormapLUT(opts.colormap ?? "viridis");
1502
1673
  let lo = opts.domain?.[0] ?? Infinity;
1503
1674
  let hi = opts.domain?.[1] ?? -Infinity;
1504
1675
  if (!opts.domain) {
@@ -1511,10 +1682,12 @@ var HeatmapLayer = class {
1511
1682
  const span = hi - lo || 1;
1512
1683
  const pixels = new Uint8Array(cols * rows * 4);
1513
1684
  for (let i = 0; i < cols * rows; i++) {
1514
- const [r, g, b] = cmap((values[i] - lo) / span);
1515
- pixels[i * 4] = Math.round(r * 255);
1516
- pixels[i * 4 + 1] = Math.round(g * 255);
1517
- pixels[i * 4 + 2] = Math.round(b * 255);
1685
+ let t = (values[i] - lo) / span;
1686
+ t = t <= 0 ? 0 : t >= 1 ? 1 : t;
1687
+ const j = (t * 255 | 0) * 3;
1688
+ pixels[i * 4] = lut[j] * 255 + 0.5 | 0;
1689
+ pixels[i * 4 + 1] = lut[j + 1] * 255 + 0.5 | 0;
1690
+ pixels[i * 4 + 2] = lut[j + 2] * 255 + 0.5 | 0;
1518
1691
  pixels[i * 4 + 3] = 255;
1519
1692
  }
1520
1693
  const texture = gl.createTexture();
@@ -1624,16 +1797,16 @@ var HEX = (() => {
1624
1797
  }
1625
1798
  return new Float32Array(verts);
1626
1799
  })();
1627
- var programCache6 = /* @__PURE__ */ new WeakMap();
1800
+ var programCache7 = /* @__PURE__ */ new WeakMap();
1628
1801
  function getProgram6(gl) {
1629
- let p = programCache6.get(gl);
1802
+ let p = programCache7.get(gl);
1630
1803
  if (!p) {
1631
1804
  p = createProgram(gl, VERT6, FRAG7);
1632
- programCache6.set(gl, p);
1805
+ programCache7.set(gl, p);
1633
1806
  }
1634
1807
  return p;
1635
1808
  }
1636
- var counter8 = 0;
1809
+ var counter9 = 0;
1637
1810
  var HexbinLayer = class {
1638
1811
  constructor(gl, opts) {
1639
1812
  this.buffers = [];
@@ -1641,7 +1814,7 @@ var HexbinLayer = class {
1641
1814
  this.yRef = 0;
1642
1815
  this.xBounds = [0, 0];
1643
1816
  this.yBounds = [0, 0];
1644
- this.id = `hexbin-${counter8++}`;
1817
+ this.id = `hexbin-${counter9++}`;
1645
1818
  this.gl = gl;
1646
1819
  this.program = getProgram6(gl);
1647
1820
  this.yAxis = opts.yAxis ?? "y";
@@ -1739,6 +1912,155 @@ var HexbinLayer = class {
1739
1912
  }
1740
1913
  };
1741
1914
 
1915
+ // src/layers/image.ts
1916
+ var VERT7 = (
1917
+ /* glsl */
1918
+ `#version 300 es
1919
+ precision highp float;
1920
+ layout(location = 0) in vec2 aPos; // offset data space
1921
+ layout(location = 1) in vec2 aUV;
1922
+ ${TRANSFORM_GLSL}
1923
+ out vec2 vUV;
1924
+ void main() {
1925
+ vUV = aUV;
1926
+ gl_Position = vec4(dataToClip(aPos), 0.0, 1.0);
1927
+ }`
1928
+ );
1929
+ var FRAG8 = (
1930
+ /* glsl */
1931
+ `#version 300 es
1932
+ precision highp float;
1933
+ in vec2 vUV;
1934
+ uniform sampler2D uTex;
1935
+ uniform float uOpacity;
1936
+ out vec4 outColor;
1937
+ void main() {
1938
+ vec4 c = texture(uTex, vUV);
1939
+ float a = c.a * uOpacity;
1940
+ outColor = vec4(c.rgb * a, a);
1941
+ }`
1942
+ );
1943
+ var programCache8 = /* @__PURE__ */ new WeakMap();
1944
+ function getProgram7(gl) {
1945
+ let p = programCache8.get(gl);
1946
+ if (!p) {
1947
+ p = createProgram(gl, VERT7, FRAG8);
1948
+ programCache8.set(gl, p);
1949
+ }
1950
+ return p;
1951
+ }
1952
+ var counter10 = 0;
1953
+ var ImageLayer = class {
1954
+ constructor(gl, opts) {
1955
+ this.colorCss = "#94a3b8";
1956
+ this.ready = false;
1957
+ this.img = null;
1958
+ this.id = `image-${counter10++}`;
1959
+ this.gl = gl;
1960
+ this.program = getProgram7(gl);
1961
+ this.name = opts.name ?? this.id;
1962
+ this.yAxis = opts.yAxis ?? "y";
1963
+ this.ext = opts.extent;
1964
+ this.opacity = opts.opacity ?? 1;
1965
+ this.smooth = opts.smooth !== false;
1966
+ const [x0, x1] = opts.extent.x;
1967
+ const [y0, y1] = opts.extent.y;
1968
+ this.xRef = x0;
1969
+ this.yRef = y0;
1970
+ this.texture = gl.createTexture();
1971
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
1972
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 0]));
1973
+ gl.bindTexture(gl.TEXTURE_2D, null);
1974
+ if (typeof opts.source === "string") {
1975
+ const img = new Image();
1976
+ this.img = img;
1977
+ img.crossOrigin = "anonymous";
1978
+ img.onload = () => {
1979
+ this.upload(img);
1980
+ opts.onLoad?.();
1981
+ };
1982
+ img.src = opts.source;
1983
+ } else {
1984
+ this.upload(opts.source);
1985
+ }
1986
+ const data = new Float32Array([
1987
+ x0 - this.xRef,
1988
+ y0 - this.yRef,
1989
+ 0,
1990
+ 0,
1991
+ x1 - this.xRef,
1992
+ y0 - this.yRef,
1993
+ 1,
1994
+ 0,
1995
+ x1 - this.xRef,
1996
+ y1 - this.yRef,
1997
+ 1,
1998
+ 1,
1999
+ x0 - this.xRef,
2000
+ y0 - this.yRef,
2001
+ 0,
2002
+ 0,
2003
+ x1 - this.xRef,
2004
+ y1 - this.yRef,
2005
+ 1,
2006
+ 1,
2007
+ x0 - this.xRef,
2008
+ y1 - this.yRef,
2009
+ 0,
2010
+ 1
2011
+ ]);
2012
+ const vao = gl.createVertexArray();
2013
+ const buffer = gl.createBuffer();
2014
+ this.vao = vao;
2015
+ this.buffer = buffer;
2016
+ gl.bindVertexArray(vao);
2017
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
2018
+ gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
2019
+ gl.enableVertexAttribArray(0);
2020
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 16, 0);
2021
+ gl.enableVertexAttribArray(1);
2022
+ gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 16, 8);
2023
+ gl.bindVertexArray(null);
2024
+ this.uniforms = uniformLocations(gl, this.program, [...TRANSFORM_UNIFORMS, "uTex", "uOpacity"]);
2025
+ }
2026
+ upload(src) {
2027
+ const gl = this.gl;
2028
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
2029
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
2030
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, src);
2031
+ gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
2032
+ const filter = this.smooth ? gl.LINEAR : gl.NEAREST;
2033
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
2034
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
2035
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
2036
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
2037
+ gl.bindTexture(gl.TEXTURE_2D, null);
2038
+ this.ready = true;
2039
+ }
2040
+ bounds() {
2041
+ return { x: this.ext.x, y: this.ext.y };
2042
+ }
2043
+ draw(state) {
2044
+ if (!this.ready) return;
2045
+ const gl = state.gl;
2046
+ gl.useProgram(this.program);
2047
+ setTransformUniforms(gl, this.uniforms, state.x, state.y, this.xRef, this.yRef);
2048
+ gl.activeTexture(gl.TEXTURE0);
2049
+ gl.bindTexture(gl.TEXTURE_2D, this.texture);
2050
+ gl.uniform1i(this.uniforms.uTex, 0);
2051
+ gl.uniform1f(this.uniforms.uOpacity, this.opacity);
2052
+ gl.bindVertexArray(this.vao);
2053
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
2054
+ gl.bindVertexArray(null);
2055
+ }
2056
+ dispose() {
2057
+ if (this.img) this.img.onload = null;
2058
+ this.gl.deleteVertexArray(this.vao);
2059
+ this.gl.deleteBuffer(this.buffer);
2060
+ this.gl.deleteTexture(this.texture);
2061
+ }
2062
+ };
2063
+
1742
2064
  // src/layers/line-util.ts
1743
2065
  function decimateIndices(ys, i0, i1, cols) {
1744
2066
  const out = [i0];
@@ -1832,9 +2154,9 @@ ${log}`);
1832
2154
  }
1833
2155
  return sh;
1834
2156
  }
1835
- var programCache7 = /* @__PURE__ */ new WeakMap();
2157
+ var programCache9 = /* @__PURE__ */ new WeakMap();
1836
2158
  function getDecProgram(gl) {
1837
- if (programCache7.has(gl)) return programCache7.get(gl);
2159
+ if (programCache9.has(gl)) return programCache9.get(gl);
1838
2160
  let result = null;
1839
2161
  try {
1840
2162
  const vs = compile(gl, gl.VERTEX_SHADER, DEC_VERT);
@@ -1858,7 +2180,7 @@ function getDecProgram(gl) {
1858
2180
  } catch {
1859
2181
  result = null;
1860
2182
  }
1861
- programCache7.set(gl, result);
2183
+ programCache9.set(gl, result);
1862
2184
  return result;
1863
2185
  }
1864
2186
  var GpuDecimator = class {
@@ -1975,7 +2297,7 @@ function pickNearest(xs, ys, count, mode, cursorPx, cursorPy, project, gatePx =
1975
2297
 
1976
2298
  // src/layers/line.ts
1977
2299
  var GPU_DECIMATE_MIN = 2e5;
1978
- var VERT7 = (
2300
+ var VERT8 = (
1979
2301
  /* glsl */
1980
2302
  `#version 300 es
1981
2303
  precision highp float;
@@ -2005,7 +2327,7 @@ void main() {
2005
2327
  gl_Position = vec4((pos / uResolution) * 2.0 - 1.0, 0.0, 1.0);
2006
2328
  }`
2007
2329
  );
2008
- var FRAG8 = (
2330
+ var FRAG9 = (
2009
2331
  /* glsl */
2010
2332
  `#version 300 es
2011
2333
  precision highp float;
@@ -2125,12 +2447,12 @@ void main() {
2125
2447
  }`
2126
2448
  );
2127
2449
  var CORNERS2 = new Float32Array([0, -1, 1, -1, 1, 1, 0, -1, 1, 1, 0, 1]);
2128
- var programCache8 = /* @__PURE__ */ new WeakMap();
2129
- function getProgram7(gl) {
2130
- let p = programCache8.get(gl);
2450
+ var programCache10 = /* @__PURE__ */ new WeakMap();
2451
+ function getProgram8(gl) {
2452
+ let p = programCache10.get(gl);
2131
2453
  if (!p) {
2132
- p = createProgram(gl, VERT7, FRAG8);
2133
- programCache8.set(gl, p);
2454
+ p = createProgram(gl, VERT8, FRAG9);
2455
+ programCache10.set(gl, p);
2134
2456
  }
2135
2457
  return p;
2136
2458
  }
@@ -2172,7 +2494,7 @@ function upperBound(a, v) {
2172
2494
  }
2173
2495
  return lo;
2174
2496
  }
2175
- var counter9 = 0;
2497
+ var counter11 = 0;
2176
2498
  var LineLayer = class {
2177
2499
  constructor(gl, opts) {
2178
2500
  this.gpuDec = null;
@@ -2182,9 +2504,9 @@ var LineLayer = class {
2182
2504
  this.yBounds = [0, 0];
2183
2505
  this.decKey = "";
2184
2506
  this.decSegments = 0;
2185
- this.id = `line-${counter9++}`;
2507
+ this.id = `line-${counter11++}`;
2186
2508
  this.gl = gl;
2187
- this.program = getProgram7(gl);
2509
+ this.program = getProgram8(gl);
2188
2510
  this.joinProgram = getJoinProgram(gl);
2189
2511
  this.width = opts.width ?? 1.5;
2190
2512
  this.joinStyle = opts.join ?? "round";
@@ -2433,14 +2755,757 @@ var LineLayer = class {
2433
2755
  }
2434
2756
  };
2435
2757
 
2436
- // src/layers/quiver.ts
2437
- var SHAFT_VERT = (
2438
- /* glsl */
2439
- `#version 300 es
2440
- precision highp float;
2441
- layout(location = 0) in vec2 aCorner; // (along 0..1, side -1..1)
2442
- layout(location = 1) in vec4 aArrow; // (bx,by,tx,ty) offset data space
2443
- layout(location = 2) in vec4 aColor;
2758
+ // src/graph/force.ts
2759
+ function forceLayout(nodeCount, edges, opts = {}) {
2760
+ const n = nodeCount;
2761
+ const x = new Float64Array(n);
2762
+ const y = new Float64Array(n);
2763
+ for (let i = 0; i < n; i++) {
2764
+ const a = i / n * Math.PI * 2;
2765
+ x[i] = Math.cos(a);
2766
+ y[i] = Math.sin(a);
2767
+ }
2768
+ if (n < 2) return { x, y };
2769
+ const iterations = opts.iterations ?? 300;
2770
+ const area2 = opts.area ?? 1;
2771
+ const k = Math.sqrt(area2 / n);
2772
+ const gravity = opts.gravity ?? 0.05;
2773
+ const dispX = new Float64Array(n);
2774
+ const dispY = new Float64Array(n);
2775
+ let temp = 0.1;
2776
+ for (let it = 0; it < iterations; it++) {
2777
+ dispX.fill(0);
2778
+ dispY.fill(0);
2779
+ for (let i = 0; i < n; i++) {
2780
+ for (let j = i + 1; j < n; j++) {
2781
+ const dx = x[i] - x[j];
2782
+ const dy = y[i] - y[j];
2783
+ const d = Math.hypot(dx, dy) || 1e-6;
2784
+ const f = k * k / d;
2785
+ const ux = dx / d, uy = dy / d;
2786
+ dispX[i] += ux * f;
2787
+ dispY[i] += uy * f;
2788
+ dispX[j] -= ux * f;
2789
+ dispY[j] -= uy * f;
2790
+ }
2791
+ }
2792
+ for (const [a, b] of edges) {
2793
+ const dx = x[a] - x[b];
2794
+ const dy = y[a] - y[b];
2795
+ const d = Math.hypot(dx, dy) || 1e-6;
2796
+ const f = d * d / k;
2797
+ const ux = dx / d, uy = dy / d;
2798
+ dispX[a] -= ux * f;
2799
+ dispY[a] -= uy * f;
2800
+ dispX[b] += ux * f;
2801
+ dispY[b] += uy * f;
2802
+ }
2803
+ for (let i = 0; i < n; i++) {
2804
+ dispX[i] -= x[i] * gravity;
2805
+ dispY[i] -= y[i] * gravity;
2806
+ const d = Math.hypot(dispX[i], dispY[i]) || 1e-6;
2807
+ const lim = Math.min(d, temp);
2808
+ x[i] += dispX[i] / d * lim;
2809
+ y[i] += dispY[i] / d * lim;
2810
+ }
2811
+ temp *= 0.99;
2812
+ }
2813
+ return { x, y };
2814
+ }
2815
+
2816
+ // src/geo/earcut.ts
2817
+ var Z_ORDER_THRESHOLD = 80;
2818
+ var Node = class {
2819
+ constructor(i, x, y) {
2820
+ this.i = i;
2821
+ this.x = x;
2822
+ this.y = y;
2823
+ this.prev = this;
2824
+ this.next = this;
2825
+ this.steiner = false;
2826
+ /** Morton (z-order) code; 0 until {@link indexCurve} assigns it. */
2827
+ this.z = 0;
2828
+ /** Neighbours in the z-order-sorted list (null outside the hashed path). */
2829
+ this.prevZ = null;
2830
+ this.nextZ = null;
2831
+ }
2832
+ };
2833
+ function earcut(data, holeIndices, dim = 2) {
2834
+ const hasHoles = holeIndices != null && holeIndices.length > 0;
2835
+ const outerLen = hasHoles ? holeIndices[0] * dim : data.length;
2836
+ let outerNode = linkedList(data, 0, outerLen, dim, true);
2837
+ const triangles = [];
2838
+ if (!outerNode || outerNode.next === outerNode.prev) return triangles;
2839
+ if (hasHoles) outerNode = eliminateHoles(data, holeIndices, outerNode, dim);
2840
+ let minX = 0;
2841
+ let minY = 0;
2842
+ let invSize = 0;
2843
+ if (data.length > Z_ORDER_THRESHOLD * dim) {
2844
+ minX = Infinity;
2845
+ minY = Infinity;
2846
+ let maxX = -Infinity;
2847
+ let maxY = -Infinity;
2848
+ for (let i = 0; i < outerLen; i += dim) {
2849
+ const x = data[i];
2850
+ const y = data[i + 1];
2851
+ if (x < minX) minX = x;
2852
+ if (y < minY) minY = y;
2853
+ if (x > maxX) maxX = x;
2854
+ if (y > maxY) maxY = y;
2855
+ }
2856
+ const size = Math.max(maxX - minX, maxY - minY);
2857
+ invSize = size !== 0 ? 32767 / size : 0;
2858
+ }
2859
+ earcutLinked(outerNode, triangles, dim, minX, minY, invSize, 0);
2860
+ return triangles;
2861
+ }
2862
+ function linkedList(data, start, end, dim, clockwise) {
2863
+ let last = null;
2864
+ if (clockwise === ringArea(data, start, end, dim) > 0) {
2865
+ for (let i = start; i < end; i += dim) last = insertNode(i, data[i], data[i + 1], last);
2866
+ } else {
2867
+ for (let i = end - dim; i >= start; i -= dim) last = insertNode(i, data[i], data[i + 1], last);
2868
+ }
2869
+ if (last && equals(last, last.next)) {
2870
+ removeNode(last);
2871
+ last = last.next;
2872
+ }
2873
+ return last;
2874
+ }
2875
+ function filterPoints(start, end) {
2876
+ if (!start) return start;
2877
+ let e = end ?? start;
2878
+ let p = start;
2879
+ let again;
2880
+ do {
2881
+ again = false;
2882
+ if (!p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) === 0)) {
2883
+ removeNode(p);
2884
+ p = e = p.prev;
2885
+ if (p === p.next) break;
2886
+ again = true;
2887
+ } else {
2888
+ p = p.next;
2889
+ }
2890
+ } while (again || p !== e);
2891
+ return e;
2892
+ }
2893
+ function earcutLinked(earStart, triangles, dim, minX, minY, invSize, pass) {
2894
+ if (!earStart) return;
2895
+ let ear = earStart;
2896
+ if (pass === 0 && invSize) indexCurve(ear, minX, minY, invSize);
2897
+ let stop = earStart;
2898
+ while (ear.prev !== ear.next) {
2899
+ const prev = ear.prev;
2900
+ const next = ear.next;
2901
+ const found = invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear);
2902
+ if (found) {
2903
+ triangles.push(prev.i / dim | 0, ear.i / dim | 0, next.i / dim | 0);
2904
+ removeNode(ear);
2905
+ ear = next.next;
2906
+ stop = next.next;
2907
+ continue;
2908
+ }
2909
+ ear = next;
2910
+ if (ear === stop) {
2911
+ if (pass === 0) earcutLinked(filterPoints(ear), triangles, dim, minX, minY, invSize, 1);
2912
+ else if (pass === 1) {
2913
+ const cured = cureLocalIntersections(filterPoints(ear), triangles, dim);
2914
+ earcutLinked(cured, triangles, dim, minX, minY, invSize, 2);
2915
+ } else if (pass === 2) splitEarcut(ear, triangles, dim, minX, minY, invSize);
2916
+ break;
2917
+ }
2918
+ }
2919
+ }
2920
+ function isEar(ear) {
2921
+ const a = ear.prev;
2922
+ const b = ear;
2923
+ const c = ear.next;
2924
+ if (area(a, b, c) >= 0) return false;
2925
+ let p = ear.next.next;
2926
+ while (p !== ear.prev) {
2927
+ if (pointInTriangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y) && area(p.prev, p, p.next) >= 0) {
2928
+ return false;
2929
+ }
2930
+ p = p.next;
2931
+ }
2932
+ return true;
2933
+ }
2934
+ function isEarHashed(ear, minX, minY, invSize) {
2935
+ const a = ear.prev;
2936
+ const b = ear;
2937
+ const c = ear.next;
2938
+ if (area(a, b, c) >= 0) return false;
2939
+ const ax = a.x;
2940
+ const bx = b.x;
2941
+ const cx = c.x;
2942
+ const ay = a.y;
2943
+ const by = b.y;
2944
+ const cy = c.y;
2945
+ const x0 = Math.min(ax, bx, cx);
2946
+ const y0 = Math.min(ay, by, cy);
2947
+ const x1 = Math.max(ax, bx, cx);
2948
+ const y1 = Math.max(ay, by, cy);
2949
+ const minZ = zOrder(x0, y0, minX, minY, invSize);
2950
+ const maxZ = zOrder(x1, y1, minX, minY, invSize);
2951
+ let p = ear.prevZ;
2952
+ let n = ear.nextZ;
2953
+ const blocks = (q) => q.x >= x0 && q.x <= x1 && q.y >= y0 && q.y <= y1 && q !== a && q !== c && pointInTriangle(ax, ay, bx, by, cx, cy, q.x, q.y) && area(q.prev, q, q.next) >= 0;
2954
+ while (p && p.z >= minZ && n && n.z <= maxZ) {
2955
+ if (blocks(p)) return false;
2956
+ p = p.prevZ;
2957
+ if (blocks(n)) return false;
2958
+ n = n.nextZ;
2959
+ }
2960
+ while (p && p.z >= minZ) {
2961
+ if (blocks(p)) return false;
2962
+ p = p.prevZ;
2963
+ }
2964
+ while (n && n.z <= maxZ) {
2965
+ if (blocks(n)) return false;
2966
+ n = n.nextZ;
2967
+ }
2968
+ return true;
2969
+ }
2970
+ function eliminateHoles(data, holeIndices, outerNode, dim) {
2971
+ const queue = [];
2972
+ for (let i = 0, len = holeIndices.length; i < len; i++) {
2973
+ const start = holeIndices[i] * dim;
2974
+ const end = i < len - 1 ? holeIndices[i + 1] * dim : data.length;
2975
+ const list = linkedList(data, start, end, dim, false);
2976
+ if (list) {
2977
+ if (list === list.next) list.steiner = true;
2978
+ queue.push(getLeftmost(list));
2979
+ }
2980
+ }
2981
+ queue.sort((a, b) => a.x - b.x);
2982
+ let node = outerNode;
2983
+ for (const hole of queue) node = eliminateHole(hole, node);
2984
+ return node;
2985
+ }
2986
+ function eliminateHole(hole, outerNode) {
2987
+ const bridge = findHoleBridge(hole, outerNode);
2988
+ if (!bridge) return outerNode;
2989
+ const bridgeReverse = splitPolygon(bridge, hole);
2990
+ filterPoints(bridgeReverse, bridgeReverse.next);
2991
+ return filterPoints(bridge, bridge.next);
2992
+ }
2993
+ function findHoleBridge(hole, outerNode) {
2994
+ let p = outerNode;
2995
+ const hx = hole.x;
2996
+ const hy = hole.y;
2997
+ let qx = -Infinity;
2998
+ let m = null;
2999
+ do {
3000
+ if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
3001
+ const x = p.x + (hy - p.y) / (p.next.y - p.y) * (p.next.x - p.x);
3002
+ if (x <= hx && x > qx) {
3003
+ qx = x;
3004
+ m = p.x < p.next.x ? p : p.next;
3005
+ if (x === hx) return m;
3006
+ }
3007
+ }
3008
+ p = p.next;
3009
+ } while (p !== outerNode);
3010
+ if (!m) return null;
3011
+ const stop = m;
3012
+ const mx = m.x;
3013
+ const my = m.y;
3014
+ let tanMin = Infinity;
3015
+ p = m;
3016
+ do {
3017
+ if (hx >= p.x && p.x >= mx && hx !== p.x && pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p.x, p.y)) {
3018
+ const tan = Math.abs(hy - p.y) / (hx - p.x);
3019
+ if (locallyInside(p, hole) && (tan < tanMin || tan === tanMin && (p.x > m.x || p.x === m.x && sectorContainsSector(m, p)))) {
3020
+ m = p;
3021
+ tanMin = tan;
3022
+ }
3023
+ }
3024
+ p = p.next;
3025
+ } while (p !== stop);
3026
+ return m;
3027
+ }
3028
+ function cureLocalIntersections(start, triangles, dim) {
3029
+ let p = start;
3030
+ do {
3031
+ const a = p.prev;
3032
+ const b = p.next.next;
3033
+ if (!equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) {
3034
+ triangles.push(a.i / dim | 0, p.i / dim | 0, b.i / dim | 0);
3035
+ removeNode(p);
3036
+ removeNode(p.next);
3037
+ p = start = b;
3038
+ }
3039
+ p = p.next;
3040
+ } while (p !== start);
3041
+ return filterPoints(p);
3042
+ }
3043
+ function splitEarcut(start, triangles, dim, minX, minY, invSize) {
3044
+ let a = start;
3045
+ do {
3046
+ let b = a.next.next;
3047
+ while (b !== a.prev) {
3048
+ if (a.i !== b.i && isValidDiagonal(a, b)) {
3049
+ let c = splitPolygon(a, b);
3050
+ const a2 = filterPoints(a, a.next);
3051
+ c = filterPoints(c, c.next);
3052
+ earcutLinked(a2, triangles, dim, minX, minY, invSize, 0);
3053
+ earcutLinked(c, triangles, dim, minX, minY, invSize, 0);
3054
+ return;
3055
+ }
3056
+ b = b.next;
3057
+ }
3058
+ a = a.next;
3059
+ } while (a !== start);
3060
+ }
3061
+ function area(p, q, r) {
3062
+ return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
3063
+ }
3064
+ function equals(a, b) {
3065
+ return a.x === b.x && a.y === b.y;
3066
+ }
3067
+ function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) {
3068
+ return (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 && (ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 && (bx - px) * (cy - py) - (cx - px) * (by - py) >= 0;
3069
+ }
3070
+ function isValidDiagonal(a, b) {
3071
+ return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) && (locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && (area(a.prev, a, b.prev) !== 0 || area(a, b.prev, b) !== 0) || equals(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0);
3072
+ }
3073
+ function onSegment(p, q, r) {
3074
+ return q.x <= Math.max(p.x, r.x) && q.x >= Math.min(p.x, r.x) && q.y <= Math.max(p.y, r.y) && q.y >= Math.min(p.y, r.y);
3075
+ }
3076
+ function sign(n) {
3077
+ return n > 0 ? 1 : n < 0 ? -1 : 0;
3078
+ }
3079
+ function intersects(p1, q1, p2, q2) {
3080
+ const o1 = sign(area(p1, q1, p2));
3081
+ const o2 = sign(area(p1, q1, q2));
3082
+ const o3 = sign(area(p2, q2, p1));
3083
+ const o4 = sign(area(p2, q2, q1));
3084
+ if (o1 !== o2 && o3 !== o4) return true;
3085
+ if (o1 === 0 && onSegment(p1, p2, q1)) return true;
3086
+ if (o2 === 0 && onSegment(p1, q2, q1)) return true;
3087
+ if (o3 === 0 && onSegment(p2, p1, q2)) return true;
3088
+ if (o4 === 0 && onSegment(p2, q1, q2)) return true;
3089
+ return false;
3090
+ }
3091
+ function intersectsPolygon(a, b) {
3092
+ let p = a;
3093
+ do {
3094
+ if (p.i !== a.i && p.next.i !== a.i && p.i !== b.i && p.next.i !== b.i && intersects(p, p.next, a, b)) {
3095
+ return true;
3096
+ }
3097
+ p = p.next;
3098
+ } while (p !== a);
3099
+ return false;
3100
+ }
3101
+ function locallyInside(a, b) {
3102
+ return area(a.prev, a, a.next) < 0 ? area(a, b, a.next) >= 0 && area(a, a.prev, b) >= 0 : area(a, b, a.prev) < 0 || area(a, a.next, b) < 0;
3103
+ }
3104
+ function middleInside(a, b) {
3105
+ let p = a;
3106
+ let inside = false;
3107
+ const px = (a.x + b.x) / 2;
3108
+ const py = (a.y + b.y) / 2;
3109
+ do {
3110
+ if (p.y > py !== p.next.y > py && p.next.y !== p.y && px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x) {
3111
+ inside = !inside;
3112
+ }
3113
+ p = p.next;
3114
+ } while (p !== a);
3115
+ return inside;
3116
+ }
3117
+ function sectorContainsSector(m, p) {
3118
+ return area(m.prev, m, p.prev) < 0 && area(p.next, m, m.next) < 0;
3119
+ }
3120
+ function splitPolygon(a, b) {
3121
+ const a2 = new Node(a.i, a.x, a.y);
3122
+ const b2 = new Node(b.i, b.x, b.y);
3123
+ const an = a.next;
3124
+ const bp = b.prev;
3125
+ a.next = b;
3126
+ b.prev = a;
3127
+ a2.next = an;
3128
+ an.prev = a2;
3129
+ b2.next = a2;
3130
+ a2.prev = b2;
3131
+ bp.next = b2;
3132
+ b2.prev = bp;
3133
+ return b2;
3134
+ }
3135
+ function insertNode(i, x, y, last) {
3136
+ const p = new Node(i, x, y);
3137
+ if (!last) {
3138
+ p.prev = p;
3139
+ p.next = p;
3140
+ } else {
3141
+ p.next = last.next;
3142
+ p.prev = last;
3143
+ last.next.prev = p;
3144
+ last.next = p;
3145
+ }
3146
+ return p;
3147
+ }
3148
+ function removeNode(p) {
3149
+ p.next.prev = p.prev;
3150
+ p.prev.next = p.next;
3151
+ if (p.prevZ) p.prevZ.nextZ = p.nextZ;
3152
+ if (p.nextZ) p.nextZ.prevZ = p.prevZ;
3153
+ }
3154
+ function getLeftmost(start) {
3155
+ let p = start;
3156
+ let leftmost = start;
3157
+ do {
3158
+ if (p.x < leftmost.x || p.x === leftmost.x && p.y < leftmost.y) leftmost = p;
3159
+ p = p.next;
3160
+ } while (p !== start);
3161
+ return leftmost;
3162
+ }
3163
+ function ringArea(data, start, end, dim) {
3164
+ let sum = 0;
3165
+ for (let i = start, j = end - dim; i < end; i += dim) {
3166
+ sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]);
3167
+ j = i;
3168
+ }
3169
+ return sum;
3170
+ }
3171
+ function indexCurve(start, minX, minY, invSize) {
3172
+ let p = start;
3173
+ do {
3174
+ if (p.z === 0) p.z = zOrder(p.x, p.y, minX, minY, invSize);
3175
+ p.prevZ = p.prev;
3176
+ p.nextZ = p.next;
3177
+ p = p.next;
3178
+ } while (p !== start);
3179
+ p.prevZ.nextZ = null;
3180
+ p.prevZ = null;
3181
+ sortLinked(p);
3182
+ }
3183
+ function sortLinked(list) {
3184
+ let inSize = 1;
3185
+ let numMerges;
3186
+ do {
3187
+ let p = list;
3188
+ list = null;
3189
+ let tail = null;
3190
+ numMerges = 0;
3191
+ while (p) {
3192
+ numMerges++;
3193
+ let q = p;
3194
+ let pSize = 0;
3195
+ for (let i = 0; i < inSize; i++) {
3196
+ pSize++;
3197
+ q = q.nextZ;
3198
+ if (!q) break;
3199
+ }
3200
+ let qSize = inSize;
3201
+ while (pSize > 0 || qSize > 0 && q) {
3202
+ let e;
3203
+ if (pSize !== 0 && (qSize === 0 || !q || p.z <= q.z)) {
3204
+ e = p;
3205
+ p = p.nextZ;
3206
+ pSize--;
3207
+ } else {
3208
+ e = q;
3209
+ q = q.nextZ;
3210
+ qSize--;
3211
+ }
3212
+ if (tail) tail.nextZ = e;
3213
+ else list = e;
3214
+ e.prevZ = tail;
3215
+ tail = e;
3216
+ }
3217
+ p = q;
3218
+ }
3219
+ tail.nextZ = null;
3220
+ inSize *= 2;
3221
+ } while (numMerges > 1);
3222
+ }
3223
+ function zOrder(x, y, minX, minY, invSize) {
3224
+ let ix = (x - minX) * invSize | 0;
3225
+ let iy = (y - minY) * invSize | 0;
3226
+ ix = (ix | ix << 8) & 16711935;
3227
+ ix = (ix | ix << 4) & 252645135;
3228
+ ix = (ix | ix << 2) & 858993459;
3229
+ ix = (ix | ix << 1) & 1431655765;
3230
+ iy = (iy | iy << 8) & 16711935;
3231
+ iy = (iy | iy << 4) & 252645135;
3232
+ iy = (iy | iy << 2) & 858993459;
3233
+ iy = (iy | iy << 1) & 1431655765;
3234
+ return ix | iy << 1;
3235
+ }
3236
+
3237
+ // src/layers/patches.ts
3238
+ var FILL_VERT = (
3239
+ /* glsl */
3240
+ `#version 300 es
3241
+ precision highp float;
3242
+ layout(location = 0) in vec2 aPos; // offset data-space position
3243
+ layout(location = 1) in vec4 aColor; // per-vertex color (premultiplied on output)
3244
+ ${TRANSFORM_GLSL}
3245
+ out vec4 vColor;
3246
+ void main() {
3247
+ vColor = aColor;
3248
+ gl_Position = vec4(dataToClip(aPos), 0.0, 1.0);
3249
+ }`
3250
+ );
3251
+ var FILL_FRAG = (
3252
+ /* glsl */
3253
+ `#version 300 es
3254
+ precision highp float;
3255
+ in vec4 vColor;
3256
+ out vec4 outColor;
3257
+ void main() { outColor = vec4(vColor.rgb * vColor.a, vColor.a); }`
3258
+ );
3259
+ var programCache11 = /* @__PURE__ */ new WeakMap();
3260
+ function getFillProgram(gl) {
3261
+ let p = programCache11.get(gl);
3262
+ if (!p) {
3263
+ p = createProgram(gl, FILL_VERT, FILL_FRAG);
3264
+ programCache11.set(gl, p);
3265
+ }
3266
+ return p;
3267
+ }
3268
+ var counter12 = 0;
3269
+ var PatchesLayer = class {
3270
+ constructor(gl, opts) {
3271
+ this.buffers = [];
3272
+ this.vertexCount = 0;
3273
+ this.xRef = 0;
3274
+ this.yRef = 0;
3275
+ this.xBounds = [0, 0];
3276
+ this.yBounds = [0, 0];
3277
+ this.id = `patches-${counter12++}`;
3278
+ this.gl = gl;
3279
+ this.program = getFillProgram(gl);
3280
+ this.name = opts.name ?? this.id;
3281
+ this.yAxis = opts.yAxis ?? "y";
3282
+ const defColor = opts.color ?? "#3b82f6";
3283
+ const defRgba = Array.isArray(defColor) ? defColor : parseColor(defColor);
3284
+ this.colorCss = typeof defColor === "string" ? defColor : "#3b82f6";
3285
+ const opacity = opts.opacity ?? 1;
3286
+ const cmap = opts.colormap ? colormap(opts.colormap) : null;
3287
+ let lo = opts.domain?.[0] ?? Infinity;
3288
+ let hi = opts.domain?.[1] ?? -Infinity;
3289
+ if (cmap && !opts.domain) {
3290
+ for (const patch of opts.patches) {
3291
+ const v = patch.value;
3292
+ if (v == null) continue;
3293
+ if (v < lo) lo = v;
3294
+ if (v > hi) hi = v;
3295
+ }
3296
+ }
3297
+ const span = hi - lo || 1;
3298
+ for (const patch of opts.patches) {
3299
+ if (patch.x.length > 0) {
3300
+ this.xRef = patch.x[0];
3301
+ this.yRef = patch.y[0];
3302
+ break;
3303
+ }
3304
+ }
3305
+ const positions = [];
3306
+ const colors = [];
3307
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
3308
+ for (const patch of opts.patches) {
3309
+ const n = Math.min(patch.x.length, patch.y.length);
3310
+ if (n < 3) continue;
3311
+ let rgba;
3312
+ if (patch.color != null) {
3313
+ rgba = Array.isArray(patch.color) ? patch.color : parseColor(patch.color);
3314
+ } else if (cmap && patch.value != null) {
3315
+ const [r, g, b] = cmap((patch.value - lo) / span);
3316
+ rgba = [r, g, b, 1];
3317
+ } else {
3318
+ rgba = defRgba;
3319
+ }
3320
+ const a = rgba[3] * opacity;
3321
+ const flat = new Array(n * 2);
3322
+ for (let i = 0; i < n; i++) {
3323
+ flat[i * 2] = patch.x[i];
3324
+ flat[i * 2 + 1] = patch.y[i];
3325
+ }
3326
+ const tris = earcut(flat, patch.holes && patch.holes.length ? patch.holes : void 0, 2);
3327
+ for (const idx of tris) {
3328
+ const px = flat[idx * 2];
3329
+ const py = flat[idx * 2 + 1];
3330
+ positions.push(px - this.xRef, py - this.yRef);
3331
+ colors.push(rgba[0], rgba[1], rgba[2], a);
3332
+ }
3333
+ for (let i = 0; i < n; i++) {
3334
+ const px = patch.x[i], py = patch.y[i];
3335
+ if (px < minX) minX = px;
3336
+ if (px > maxX) maxX = px;
3337
+ if (py < minY) minY = py;
3338
+ if (py > maxY) maxY = py;
3339
+ }
3340
+ }
3341
+ this.vertexCount = positions.length / 2;
3342
+ this.xBounds = minX <= maxX ? [minX, maxX] : [0, 0];
3343
+ this.yBounds = minY <= maxY ? [minY, maxY] : [0, 0];
3344
+ const vao = gl.createVertexArray();
3345
+ this.vao = vao;
3346
+ gl.bindVertexArray(vao);
3347
+ const posBuf = gl.createBuffer();
3348
+ gl.bindBuffer(gl.ARRAY_BUFFER, posBuf);
3349
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
3350
+ gl.enableVertexAttribArray(0);
3351
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
3352
+ const colBuf = gl.createBuffer();
3353
+ gl.bindBuffer(gl.ARRAY_BUFFER, colBuf);
3354
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
3355
+ gl.enableVertexAttribArray(1);
3356
+ gl.vertexAttribPointer(1, 4, gl.FLOAT, false, 0, 0);
3357
+ gl.bindVertexArray(null);
3358
+ this.buffers = [posBuf, colBuf];
3359
+ this.uniforms = uniformLocations(gl, this.program, [...TRANSFORM_UNIFORMS]);
3360
+ }
3361
+ bounds() {
3362
+ if (this.vertexCount === 0) return null;
3363
+ return { x: this.xBounds, y: this.yBounds };
3364
+ }
3365
+ draw(state) {
3366
+ if (this.vertexCount === 0) return;
3367
+ const gl = state.gl;
3368
+ gl.useProgram(this.program);
3369
+ setTransformUniforms(gl, this.uniforms, state.x, state.y, this.xRef, this.yRef);
3370
+ gl.bindVertexArray(this.vao);
3371
+ gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
3372
+ gl.bindVertexArray(null);
3373
+ }
3374
+ dispose() {
3375
+ this.gl.deleteVertexArray(this.vao);
3376
+ for (const b of this.buffers) this.gl.deleteBuffer(b);
3377
+ }
3378
+ };
3379
+
3380
+ // src/layers/pie.ts
3381
+ var DEFAULT_COLORS = [
3382
+ "#3b82f6",
3383
+ "#f472b6",
3384
+ "#22d3ee",
3385
+ "#a3e635",
3386
+ "#fbbf24",
3387
+ "#a78bfa",
3388
+ "#34d399",
3389
+ "#fb7185",
3390
+ "#60a5fa",
3391
+ "#f59e0b"
3392
+ ];
3393
+ var counter13 = 0;
3394
+ var PieLayer = class {
3395
+ constructor(gl, opts) {
3396
+ this.buffers = [];
3397
+ this.vertexCount = 0;
3398
+ this.id = `pie-${counter13++}`;
3399
+ this.gl = gl;
3400
+ this.program = getFillProgram(gl);
3401
+ this.name = opts.name ?? this.id;
3402
+ this.yAxis = opts.yAxis ?? "y";
3403
+ const [cx, cy] = opts.center ?? [0, 0];
3404
+ const R = opts.radius ?? 1;
3405
+ const rIn = opts.innerRadius ?? 0;
3406
+ const start = opts.startAngle ?? Math.PI / 2;
3407
+ this.xRef = cx;
3408
+ this.yRef = cy;
3409
+ this.xBounds = [cx - R, cx + R];
3410
+ this.yBounds = [cy - R, cy + R];
3411
+ this.colorCss = "#3b82f6";
3412
+ const n = opts.values.length;
3413
+ let total = 0;
3414
+ for (let i = 0; i < n; i++) total += Math.max(0, opts.values[i]);
3415
+ if (total <= 0) total = 1;
3416
+ const cmap = opts.colormap ? colormap(opts.colormap) : null;
3417
+ const colorAt = (i) => {
3418
+ if (opts.colors?.[i] != null) {
3419
+ const c = opts.colors[i];
3420
+ return Array.isArray(c) ? c : parseColor(c);
3421
+ }
3422
+ if (cmap) {
3423
+ const [r, g, b] = cmap(n > 1 ? i / (n - 1) : 0);
3424
+ return [r, g, b, 1];
3425
+ }
3426
+ return parseColor(DEFAULT_COLORS[i % DEFAULT_COLORS.length]);
3427
+ };
3428
+ const positions = [];
3429
+ const colors = [];
3430
+ const push = (x, y, c) => {
3431
+ positions.push(x - cx, y - cy);
3432
+ colors.push(c[0], c[1], c[2], c[3]);
3433
+ };
3434
+ let a0 = start;
3435
+ for (let i = 0; i < n; i++) {
3436
+ const frac = Math.max(0, opts.values[i]) / total;
3437
+ const span = frac * Math.PI * 2;
3438
+ if (span <= 0) continue;
3439
+ const a1 = a0 - span;
3440
+ const c = colorAt(i);
3441
+ const segs = Math.max(2, Math.ceil(span / (Math.PI / 64)));
3442
+ for (let s = 0; s < segs; s++) {
3443
+ const t0 = a0 + (a1 - a0) * s / segs;
3444
+ const t1 = a0 + (a1 - a0) * (s + 1) / segs;
3445
+ const ox0 = cx + R * Math.cos(t0), oy0 = cy + R * Math.sin(t0);
3446
+ const ox1 = cx + R * Math.cos(t1), oy1 = cy + R * Math.sin(t1);
3447
+ if (rIn <= 0) {
3448
+ push(cx, cy, c);
3449
+ push(ox0, oy0, c);
3450
+ push(ox1, oy1, c);
3451
+ } else {
3452
+ const ix0 = cx + rIn * Math.cos(t0), iy0 = cy + rIn * Math.sin(t0);
3453
+ const ix1 = cx + rIn * Math.cos(t1), iy1 = cy + rIn * Math.sin(t1);
3454
+ push(ix0, iy0, c);
3455
+ push(ox0, oy0, c);
3456
+ push(ox1, oy1, c);
3457
+ push(ix0, iy0, c);
3458
+ push(ox1, oy1, c);
3459
+ push(ix1, iy1, c);
3460
+ }
3461
+ }
3462
+ a0 = a1;
3463
+ }
3464
+ this.vertexCount = positions.length / 2;
3465
+ const vao = gl.createVertexArray();
3466
+ this.vao = vao;
3467
+ gl.bindVertexArray(vao);
3468
+ const posBuf = gl.createBuffer();
3469
+ gl.bindBuffer(gl.ARRAY_BUFFER, posBuf);
3470
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
3471
+ gl.enableVertexAttribArray(0);
3472
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
3473
+ const colBuf = gl.createBuffer();
3474
+ gl.bindBuffer(gl.ARRAY_BUFFER, colBuf);
3475
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
3476
+ gl.enableVertexAttribArray(1);
3477
+ gl.vertexAttribPointer(1, 4, gl.FLOAT, false, 0, 0);
3478
+ gl.bindVertexArray(null);
3479
+ this.buffers = [posBuf, colBuf];
3480
+ this.uniforms = uniformLocations(gl, this.program, [...TRANSFORM_UNIFORMS]);
3481
+ }
3482
+ bounds() {
3483
+ if (this.vertexCount === 0) return null;
3484
+ return { x: this.xBounds, y: this.yBounds };
3485
+ }
3486
+ draw(state) {
3487
+ if (this.vertexCount === 0) return;
3488
+ const gl = state.gl;
3489
+ gl.useProgram(this.program);
3490
+ setTransformUniforms(gl, this.uniforms, state.x, state.y, this.xRef, this.yRef);
3491
+ gl.bindVertexArray(this.vao);
3492
+ gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
3493
+ gl.bindVertexArray(null);
3494
+ }
3495
+ dispose() {
3496
+ this.gl.deleteVertexArray(this.vao);
3497
+ for (const b of this.buffers) this.gl.deleteBuffer(b);
3498
+ }
3499
+ };
3500
+
3501
+ // src/layers/quiver.ts
3502
+ var SHAFT_VERT = (
3503
+ /* glsl */
3504
+ `#version 300 es
3505
+ precision highp float;
3506
+ layout(location = 0) in vec2 aCorner; // (along 0..1, side -1..1)
3507
+ layout(location = 1) in vec4 aArrow; // (bx,by,tx,ty) offset data space
3508
+ layout(location = 2) in vec4 aColor;
2444
3509
  uniform vec2 uResolution;
2445
3510
  uniform float uWidth;
2446
3511
  ${TRANSFORM_GLSL}
@@ -2483,7 +3548,7 @@ void main() {
2483
3548
  gl_Position = vec4((pos / uResolution) * 2.0 - 1.0, 0.0, 1.0);
2484
3549
  }`
2485
3550
  );
2486
- var FRAG9 = (
3551
+ var FRAG10 = (
2487
3552
  /* glsl */
2488
3553
  `#version 300 es
2489
3554
  precision highp float;
@@ -2501,12 +3566,12 @@ var cache3 = /* @__PURE__ */ new WeakMap();
2501
3566
  function programs3(gl) {
2502
3567
  let p = cache3.get(gl);
2503
3568
  if (!p) {
2504
- p = { shaft: createProgram(gl, SHAFT_VERT, FRAG9), head: createProgram(gl, HEAD_VERT, FRAG9) };
3569
+ p = { shaft: createProgram(gl, SHAFT_VERT, FRAG10), head: createProgram(gl, HEAD_VERT, FRAG10) };
2505
3570
  cache3.set(gl, p);
2506
3571
  }
2507
3572
  return p;
2508
3573
  }
2509
- var counter10 = 0;
3574
+ var counter14 = 0;
2510
3575
  var QuiverLayer = class {
2511
3576
  constructor(gl, opts) {
2512
3577
  this.buffers = [];
@@ -2514,7 +3579,7 @@ var QuiverLayer = class {
2514
3579
  this.yRef = 0;
2515
3580
  this.xBounds = [0, 0];
2516
3581
  this.yBounds = [0, 0];
2517
- this.id = `quiver-${counter10++}`;
3582
+ this.id = `quiver-${counter14++}`;
2518
3583
  this.gl = gl;
2519
3584
  this.progs = programs3(gl);
2520
3585
  const colorInput = opts.color ?? "#3b82f6";
@@ -2664,7 +3729,15 @@ var QuiverLayer = class {
2664
3729
  };
2665
3730
 
2666
3731
  // src/layers/scatter.ts
2667
- var VERT8 = (
3732
+ var MARKERS = {
3733
+ circle: 0,
3734
+ square: 1,
3735
+ triangle: 2,
3736
+ diamond: 3,
3737
+ cross: 4,
3738
+ plus: 5
3739
+ };
3740
+ var VERT9 = (
2668
3741
  /* glsl */
2669
3742
  `#version 300 es
2670
3743
  precision highp float;
@@ -2684,7 +3757,7 @@ void main() {
2684
3757
  gl_Position = vec4((pos / uResolution) * 2.0 - 1.0, 0.0, 1.0);
2685
3758
  }`
2686
3759
  );
2687
- var FRAG10 = (
3760
+ var FRAG11 = (
2688
3761
  /* glsl */
2689
3762
  `#version 300 es
2690
3763
  precision highp float;
@@ -2692,26 +3765,62 @@ in vec2 vLocal;
2692
3765
  in vec4 vColor;
2693
3766
  uniform vec4 uColor;
2694
3767
  uniform float uUseVertexColor;
3768
+ uniform int uMarker;
2695
3769
  out vec4 outColor;
3770
+
3771
+ float sdBox(vec2 p, vec2 b) {
3772
+ vec2 d = abs(p) - b;
3773
+ return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
3774
+ }
3775
+ // iq's equilateral-triangle SDF (apex toward +y).
3776
+ float sdTri(vec2 p) {
3777
+ const float k = sqrt(3.0);
3778
+ p.x = abs(p.x) - 1.0;
3779
+ p.y = p.y + 1.0 / k;
3780
+ if (p.x + k * p.y > 0.0) p = vec2(p.x - k * p.y, -k * p.x - p.y) / 2.0;
3781
+ p.x -= clamp(p.x, -2.0, 0.0);
3782
+ return -length(p) * sign(p.y);
3783
+ }
3784
+
2696
3785
  void main() {
2697
- float r = length(vLocal);
2698
- if (r > 1.0) discard;
2699
- float alpha = smoothstep(1.0, 1.0 - 0.15, r); // soft edge
3786
+ vec2 p = vLocal;
3787
+ if (uMarker == 0) {
3788
+ // Circle \u2014 original soft-edged path, unchanged.
3789
+ float r = length(p);
3790
+ if (r > 1.0) discard;
3791
+ float alpha = smoothstep(1.0, 1.0 - 0.15, r);
3792
+ vec4 c0 = uUseVertexColor > 0.5 ? vColor : uColor;
3793
+ outColor = vec4(c0.rgb * c0.a * alpha, c0.a * alpha);
3794
+ return;
3795
+ }
3796
+ float d;
3797
+ if (uMarker == 1) d = sdBox(p, vec2(0.88)); // square
3798
+ else if (uMarker == 2) d = sdTri(p); // triangle (point up)
3799
+ else if (uMarker == 3) d = abs(p.x) + abs(p.y) - 1.0; // diamond
3800
+ else if (uMarker == 4) { // cross (\xD7)
3801
+ vec2 q = vec2(p.x + p.y, p.x - p.y) * 0.70710678;
3802
+ d = min(sdBox(q, vec2(1.0, 0.30)), sdBox(q, vec2(0.30, 1.0)));
3803
+ } else { // plus (+)
3804
+ d = min(sdBox(p, vec2(1.0, 0.30)), sdBox(p, vec2(0.30, 1.0)));
3805
+ }
3806
+ float aa = fwidth(d) + 1e-4;
3807
+ float alpha = 1.0 - smoothstep(-aa, aa, d);
3808
+ if (alpha <= 0.0) discard;
2700
3809
  vec4 c = uUseVertexColor > 0.5 ? vColor : uColor;
2701
3810
  outColor = vec4(c.rgb * c.a * alpha, c.a * alpha);
2702
3811
  }`
2703
3812
  );
2704
3813
  var CORNERS3 = new Float32Array([-1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1]);
2705
- var programCache9 = /* @__PURE__ */ new WeakMap();
2706
- function getProgram8(gl) {
2707
- let p = programCache9.get(gl);
3814
+ var programCache12 = /* @__PURE__ */ new WeakMap();
3815
+ function getProgram9(gl) {
3816
+ let p = programCache12.get(gl);
2708
3817
  if (!p) {
2709
- p = createProgram(gl, VERT8, FRAG10);
2710
- programCache9.set(gl, p);
3818
+ p = createProgram(gl, VERT9, FRAG11);
3819
+ programCache12.set(gl, p);
2711
3820
  }
2712
3821
  return p;
2713
3822
  }
2714
- var counter11 = 0;
3823
+ var counter15 = 0;
2715
3824
  var ScatterLayer = class {
2716
3825
  constructor(gl, opts) {
2717
3826
  this.buffers = [];
@@ -2719,10 +3828,11 @@ var ScatterLayer = class {
2719
3828
  this.yRef = 0;
2720
3829
  this.xBounds = [0, 0];
2721
3830
  this.yBounds = [0, 0];
2722
- this.id = `scatter-${counter11++}`;
3831
+ this.id = `scatter-${counter15++}`;
2723
3832
  this.gl = gl;
2724
- this.program = getProgram8(gl);
3833
+ this.program = getProgram9(gl);
2725
3834
  this.size = opts.size ?? 5;
3835
+ this.marker = MARKERS[opts.marker ?? "circle"];
2726
3836
  const colorInput = opts.color ?? "#3b82f6";
2727
3837
  this.color = Array.isArray(colorInput) ? colorInput : parseColor(colorInput);
2728
3838
  this.colorCss = typeof colorInput === "string" ? colorInput : toColorCss(this.color);
@@ -2754,7 +3864,7 @@ var ScatterLayer = class {
2754
3864
  const colors = new Float32Array(n * 4);
2755
3865
  if (opts.colorBy) {
2756
3866
  const vals = opts.colorBy.values;
2757
- const cmap = colormap(opts.colorBy.colormap ?? "viridis");
3867
+ const lut = colormapLUT(opts.colorBy.colormap ?? "viridis");
2758
3868
  let lo = opts.colorBy.domain?.[0] ?? Infinity;
2759
3869
  let hi = opts.colorBy.domain?.[1] ?? -Infinity;
2760
3870
  if (!opts.colorBy.domain) {
@@ -2766,10 +3876,12 @@ var ScatterLayer = class {
2766
3876
  }
2767
3877
  const span = hi - lo || 1;
2768
3878
  for (let i = 0; i < n; i++) {
2769
- const [r, g, b] = cmap((vals[i] - lo) / span);
2770
- colors[i * 4] = r;
2771
- colors[i * 4 + 1] = g;
2772
- colors[i * 4 + 2] = b;
3879
+ let t = (vals[i] - lo) / span;
3880
+ t = t <= 0 ? 0 : t >= 1 ? 1 : t;
3881
+ const j = (t * 255 | 0) * 3;
3882
+ colors[i * 4] = lut[j];
3883
+ colors[i * 4 + 1] = lut[j + 1];
3884
+ colors[i * 4 + 2] = lut[j + 2];
2773
3885
  colors[i * 4 + 3] = 1;
2774
3886
  }
2775
3887
  }
@@ -2800,7 +3912,8 @@ var ScatterLayer = class {
2800
3912
  "uColor",
2801
3913
  "uResolution",
2802
3914
  "uSize",
2803
- "uUseVertexColor"
3915
+ "uUseVertexColor",
3916
+ "uMarker"
2804
3917
  ]);
2805
3918
  }
2806
3919
  bounds() {
@@ -2852,6 +3965,7 @@ var ScatterLayer = class {
2852
3965
  gl.uniform2f(this.uniforms.uResolution, state.pixelWidth, state.pixelHeight);
2853
3966
  gl.uniform1f(this.uniforms.uSize, this.size / 2 * state.dpr);
2854
3967
  gl.uniform1f(this.uniforms.uUseVertexColor, this.useVertexColor ? 1 : 0);
3968
+ gl.uniform1i(this.uniforms.uMarker, this.marker);
2855
3969
  gl.bindVertexArray(this.vao);
2856
3970
  gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this.count);
2857
3971
  gl.bindVertexArray(null);
@@ -2900,7 +4014,7 @@ void main() {
2900
4014
  gl_Position = vec4((pos / uResolution) * 2.0 - 1.0, 0.0, 1.0);
2901
4015
  }`
2902
4016
  );
2903
- var SOLID_FRAG2 = (
4017
+ var SOLID_FRAG3 = (
2904
4018
  /* glsl */
2905
4019
  `#version 300 es
2906
4020
  precision highp float;
@@ -2928,12 +4042,12 @@ var cache4 = /* @__PURE__ */ new WeakMap();
2928
4042
  function programs4(gl) {
2929
4043
  let p = cache4.get(gl);
2930
4044
  if (!p) {
2931
- p = { stem: createProgram(gl, STEM_VERT, SOLID_FRAG2), marker: createProgram(gl, MARKER_VERT, DISC_FRAG) };
4045
+ p = { stem: createProgram(gl, STEM_VERT, SOLID_FRAG3), marker: createProgram(gl, MARKER_VERT, DISC_FRAG) };
2932
4046
  cache4.set(gl, p);
2933
4047
  }
2934
4048
  return p;
2935
4049
  }
2936
- var counter12 = 0;
4050
+ var counter16 = 0;
2937
4051
  var StemLayer = class {
2938
4052
  constructor(gl, opts) {
2939
4053
  this.buffers = [];
@@ -2941,7 +4055,7 @@ var StemLayer = class {
2941
4055
  this.yRef = 0;
2942
4056
  this.xBounds = [0, 0];
2943
4057
  this.yBounds = [0, 0];
2944
- this.id = `stem-${counter12++}`;
4058
+ this.id = `stem-${counter16++}`;
2945
4059
  this.gl = gl;
2946
4060
  this.progs = programs4(gl);
2947
4061
  const colorInput = opts.color ?? "#3b82f6";
@@ -3075,89 +4189,173 @@ var darkTheme = {
3075
4189
  text: "#94a3b8",
3076
4190
  font: "12px system-ui, -apple-system, sans-serif"
3077
4191
  };
3078
- function pxX(region, t) {
3079
- return region.left + t * region.width;
4192
+ function resolveAxisStyle(cfg, theme, colorOverride) {
4193
+ const line = colorOverride ?? theme.axis;
4194
+ const text = colorOverride ?? theme.text;
4195
+ return {
4196
+ showAxisLine: cfg.showAxisLine ?? true,
4197
+ axisLineColor: cfg.axisLineColor ?? line,
4198
+ axisLineWidth: cfg.axisLineWidth ?? 1,
4199
+ showTicks: cfg.showTicks ?? true,
4200
+ tickColor: cfg.tickColor ?? line,
4201
+ tickLength: cfg.tickLength ?? 5,
4202
+ tickMinorLength: 3,
4203
+ tickWidth: cfg.tickWidth ?? 1,
4204
+ labelColor: cfg.labelColor ?? text,
4205
+ labelFont: cfg.labelFont ?? theme.font,
4206
+ labelRotation: cfg.labelRotation ?? 0,
4207
+ labelStandoff: cfg.labelStandoff ?? 3,
4208
+ titleColor: cfg.titleColor ?? text,
4209
+ titleFont: cfg.titleFont ?? theme.font,
4210
+ showGrid: cfg.showGrid ?? true,
4211
+ gridColor: cfg.gridColor ?? theme.grid,
4212
+ gridMinorColor: cfg.gridMinorColor ?? theme.gridMinor,
4213
+ gridWidth: cfg.gridWidth ?? 1,
4214
+ gridDash: cfg.gridDash ?? []
4215
+ };
3080
4216
  }
3081
- function pxY(region, t) {
4217
+ function drawTitle(ctx, region, title, isDark) {
4218
+ ctx.save();
4219
+ ctx.font = title.font ?? "600 15px system-ui, -apple-system, sans-serif";
4220
+ ctx.fillStyle = title.color ?? (isDark ? "#e2e8f0" : "#1e293b");
4221
+ ctx.textBaseline = "middle";
4222
+ const align = title.align ?? "center";
4223
+ const y = region.top / 2;
4224
+ let x;
4225
+ if (align === "left") {
4226
+ ctx.textAlign = "left";
4227
+ x = region.left;
4228
+ } else if (align === "right") {
4229
+ ctx.textAlign = "right";
4230
+ x = region.left + region.width;
4231
+ } else {
4232
+ ctx.textAlign = "center";
4233
+ x = region.left + region.width / 2;
4234
+ }
4235
+ ctx.fillText(title.text, x, y);
4236
+ ctx.restore();
4237
+ }
4238
+ function pxX(region, t) {
4239
+ return region.left + t * region.width;
4240
+ }
4241
+ function pxY(region, t) {
3082
4242
  return region.top + (1 - t) * region.height;
3083
4243
  }
3084
- function drawGrid(ctx, region, scaleX, scaleY, ticksX, ticksY, theme) {
4244
+ function drawGrid(ctx, region, scaleX, scaleY, ticksX, ticksY, styleX, styleY) {
3085
4245
  ctx.save();
3086
- ctx.lineWidth = 1;
3087
- for (const t of ticksX) {
3088
- if (!t.grid) continue;
3089
- const x = Math.round(pxX(region, scaleX.norm(t.value))) + 0.5;
3090
- ctx.strokeStyle = t.minor ? theme.gridMinor : theme.grid;
3091
- ctx.beginPath();
3092
- ctx.moveTo(x, region.top);
3093
- ctx.lineTo(x, region.top + region.height);
3094
- ctx.stroke();
4246
+ if (styleX.showGrid) {
4247
+ ctx.lineWidth = styleX.gridWidth;
4248
+ ctx.setLineDash(styleX.gridDash);
4249
+ for (const t of ticksX) {
4250
+ if (!t.grid) continue;
4251
+ const x = Math.round(pxX(region, scaleX.norm(t.value))) + 0.5;
4252
+ ctx.strokeStyle = t.minor ? styleX.gridMinorColor : styleX.gridColor;
4253
+ ctx.beginPath();
4254
+ ctx.moveTo(x, region.top);
4255
+ ctx.lineTo(x, region.top + region.height);
4256
+ ctx.stroke();
4257
+ }
3095
4258
  }
3096
- for (const t of ticksY) {
3097
- if (!t.grid) continue;
3098
- const y = Math.round(pxY(region, scaleY.norm(t.value))) + 0.5;
3099
- ctx.strokeStyle = t.minor ? theme.gridMinor : theme.grid;
3100
- ctx.beginPath();
3101
- ctx.moveTo(region.left, y);
3102
- ctx.lineTo(region.left + region.width, y);
3103
- ctx.stroke();
4259
+ if (styleY.showGrid) {
4260
+ ctx.lineWidth = styleY.gridWidth;
4261
+ ctx.setLineDash(styleY.gridDash);
4262
+ for (const t of ticksY) {
4263
+ if (!t.grid) continue;
4264
+ const y = Math.round(pxY(region, scaleY.norm(t.value))) + 0.5;
4265
+ ctx.strokeStyle = t.minor ? styleY.gridMinorColor : styleY.gridColor;
4266
+ ctx.beginPath();
4267
+ ctx.moveTo(region.left, y);
4268
+ ctx.lineTo(region.left + region.width, y);
4269
+ ctx.stroke();
4270
+ }
3104
4271
  }
3105
4272
  ctx.restore();
3106
4273
  }
3107
- function drawXAxis(ctx, region, scaleX, ticksX, theme, title) {
4274
+ function drawXAxis(ctx, region, scaleX, ticksX, style, title) {
3108
4275
  ctx.save();
3109
- ctx.strokeStyle = theme.axis;
3110
- ctx.fillStyle = theme.text;
3111
- ctx.font = theme.font;
3112
- ctx.lineWidth = 1;
4276
+ ctx.lineWidth = style.axisLineWidth;
3113
4277
  const bottom = region.top + region.height;
3114
- ctx.beginPath();
3115
- ctx.moveTo(region.left, bottom + 0.5);
3116
- ctx.lineTo(region.left + region.width, bottom + 0.5);
3117
- ctx.stroke();
3118
- ctx.textAlign = "center";
3119
- ctx.textBaseline = "top";
3120
- for (const t of ticksX) {
3121
- const x = Math.round(pxX(region, scaleX.norm(t.value))) + 0.5;
3122
- const len = t.minor ? 3 : 5;
4278
+ if (style.showAxisLine) {
4279
+ ctx.strokeStyle = style.axisLineColor;
3123
4280
  ctx.beginPath();
3124
- ctx.moveTo(x, bottom);
3125
- ctx.lineTo(x, bottom + len);
4281
+ ctx.moveTo(region.left, bottom + 0.5);
4282
+ ctx.lineTo(region.left + region.width, bottom + 0.5);
3126
4283
  ctx.stroke();
3127
- if (t.label) ctx.fillText(t.label, x, bottom + len + 3);
4284
+ }
4285
+ ctx.fillStyle = style.labelColor;
4286
+ ctx.font = style.labelFont;
4287
+ const rot = style.labelRotation * Math.PI / 180;
4288
+ for (const t of ticksX) {
4289
+ const x = Math.round(pxX(region, scaleX.norm(t.value))) + 0.5;
4290
+ const len = t.minor ? style.tickMinorLength : style.tickLength;
4291
+ if (style.showTicks) {
4292
+ ctx.strokeStyle = style.tickColor;
4293
+ ctx.lineWidth = style.tickWidth;
4294
+ ctx.beginPath();
4295
+ ctx.moveTo(x, bottom);
4296
+ ctx.lineTo(x, bottom + len);
4297
+ ctx.stroke();
4298
+ }
4299
+ if (!t.label) continue;
4300
+ const ly = bottom + (style.showTicks ? len : 0) + style.labelStandoff;
4301
+ if (rot !== 0) {
4302
+ ctx.save();
4303
+ ctx.translate(x, ly);
4304
+ ctx.rotate(rot);
4305
+ ctx.textAlign = "right";
4306
+ ctx.textBaseline = "middle";
4307
+ ctx.fillText(t.label, 0, 0);
4308
+ ctx.restore();
4309
+ } else {
4310
+ ctx.textAlign = "center";
4311
+ ctx.textBaseline = "top";
4312
+ ctx.fillText(t.label, x, ly);
4313
+ }
3128
4314
  }
3129
4315
  if (title) {
4316
+ ctx.fillStyle = style.titleColor;
4317
+ ctx.font = style.titleFont;
4318
+ ctx.textAlign = "center";
3130
4319
  ctx.textBaseline = "bottom";
3131
4320
  ctx.fillText(title, region.left + region.width / 2, bottom + 34);
3132
4321
  }
3133
4322
  ctx.restore();
3134
4323
  }
3135
- function drawYAxis(ctx, region, scaleY, ticksY, theme, opts) {
4324
+ function drawYAxis(ctx, region, scaleY, ticksY, style, opts) {
3136
4325
  ctx.save();
3137
- const color = opts.color ?? theme.axis;
3138
- ctx.strokeStyle = color;
3139
- ctx.fillStyle = opts.color ?? theme.text;
3140
- ctx.font = theme.font;
3141
- ctx.lineWidth = 1;
4326
+ ctx.font = style.labelFont;
4327
+ ctx.lineWidth = style.axisLineWidth;
3142
4328
  const ax = Math.round(opts.x) + 0.5;
3143
4329
  const dir = opts.side === "left" ? -1 : 1;
3144
- ctx.beginPath();
3145
- ctx.moveTo(ax, region.top);
3146
- ctx.lineTo(ax, region.top + region.height);
3147
- ctx.stroke();
4330
+ if (style.showAxisLine) {
4331
+ ctx.strokeStyle = style.axisLineColor;
4332
+ ctx.beginPath();
4333
+ ctx.moveTo(ax, region.top);
4334
+ ctx.lineTo(ax, region.top + region.height);
4335
+ ctx.stroke();
4336
+ }
4337
+ ctx.fillStyle = style.labelColor;
3148
4338
  ctx.textAlign = opts.side === "left" ? "right" : "left";
3149
4339
  ctx.textBaseline = "middle";
3150
4340
  for (const t of ticksY) {
3151
4341
  const y = Math.round(pxY(region, scaleY.norm(t.value))) + 0.5;
3152
- const len = t.minor ? 3 : 5;
3153
- ctx.beginPath();
3154
- ctx.moveTo(ax, y);
3155
- ctx.lineTo(ax + dir * len, y);
3156
- ctx.stroke();
3157
- if (t.label) ctx.fillText(t.label, ax + dir * (len + 4), y);
4342
+ const len = t.minor ? style.tickMinorLength : style.tickLength;
4343
+ if (style.showTicks) {
4344
+ ctx.strokeStyle = style.tickColor;
4345
+ ctx.lineWidth = style.tickWidth;
4346
+ ctx.beginPath();
4347
+ ctx.moveTo(ax, y);
4348
+ ctx.lineTo(ax + dir * len, y);
4349
+ ctx.stroke();
4350
+ }
4351
+ if (t.label) {
4352
+ ctx.fillText(t.label, ax + dir * ((style.showTicks ? len : 0) + style.labelStandoff + 1), y);
4353
+ }
3158
4354
  }
3159
4355
  if (opts.title) {
3160
4356
  ctx.save();
4357
+ ctx.fillStyle = style.titleColor;
4358
+ ctx.font = style.titleFont;
3161
4359
  const tx = opts.titleX ?? (opts.side === "left" ? 12 : region.left + region.width + 36);
3162
4360
  ctx.translate(tx, region.top + region.height / 2);
3163
4361
  ctx.rotate(-Math.PI / 2);
@@ -3345,7 +4543,32 @@ var TimeScale = class {
3345
4543
  return `${d.getFullYear()}`;
3346
4544
  }
3347
4545
  };
3348
- function makeScale(type, domain) {
4546
+ var CategoricalScale = class {
4547
+ constructor(factors = []) {
4548
+ this.type = "categorical";
4549
+ this.log = false;
4550
+ this.factors = factors;
4551
+ const n = factors.length;
4552
+ this.domain = n > 0 ? [-0.5, n - 0.5] : [-0.5, 0.5];
4553
+ }
4554
+ norm(value) {
4555
+ const [a, b] = this.domain;
4556
+ return b === a ? 0 : (value - a) / (b - a);
4557
+ }
4558
+ // Continuous linear inverse (satisfies the Scale contract used by pan/box math).
4559
+ // The nearest factor index is `Math.round(scale.invert(t))`.
4560
+ invert(t) {
4561
+ const [a, b] = this.domain;
4562
+ return a + t * (b - a);
4563
+ }
4564
+ ticks() {
4565
+ return this.factors.map((f, i) => ({ value: i, label: f, grid: false }));
4566
+ }
4567
+ formatTick(value) {
4568
+ return this.factors[Math.round(value)] ?? "";
4569
+ }
4570
+ };
4571
+ function makeScale(type, domain, factors) {
3349
4572
  switch (type) {
3350
4573
  case "linear":
3351
4574
  return new LinearScale(domain);
@@ -3353,6 +4576,8 @@ function makeScale(type, domain) {
3353
4576
  return new LogScale(domain);
3354
4577
  case "time":
3355
4578
  return new TimeScale(domain);
4579
+ case "categorical":
4580
+ return new CategoricalScale(factors ?? []);
3356
4581
  default:
3357
4582
  throw new Error(`Unknown scale type: ${type}`);
3358
4583
  }
@@ -3460,6 +4685,7 @@ function createToolbar(container, host, dark) {
3460
4685
  // src/plot.ts
3461
4686
  var DEFAULT_MARGIN = { top: 16, right: 16, bottom: 40, left: 56 };
3462
4687
  var Y_AXIS_GAP = 52;
4688
+ var TITLE_RESERVE = 28;
3463
4689
  function isPickable(layer) {
3464
4690
  return typeof layer.pick === "function";
3465
4691
  }
@@ -3500,6 +4726,7 @@ var Plot = class {
3500
4726
  this.pressPx = null;
3501
4727
  /** A point clicked to pin its details, until another click clears it. */
3502
4728
  this.selected = null;
4729
+ this.annotations = [];
3503
4730
  this.container = container;
3504
4731
  if (getComputedStyle(container).position === "static") {
3505
4732
  container.style.position = "relative";
@@ -3515,21 +4742,28 @@ var Plot = class {
3515
4742
  this.sharedCanvas = s.canvas;
3516
4743
  const sx = options.scales?.x ?? {};
3517
4744
  const sy = options.scales?.y ?? {};
3518
- this.scaleX = makeScale(sx.type ?? "linear", sx.domain ?? [0, 1]);
3519
- this.autoX = sx.domain == null;
3520
- this.initialX = sx.domain ?? null;
4745
+ const xCat = sx.type === "categorical";
4746
+ this.scaleX = makeScale(sx.type ?? "linear", sx.domain ?? [0, 1], sx.factors);
4747
+ this.autoX = !xCat && sx.domain == null;
4748
+ this.initialX = xCat ? this.scaleX.domain : sx.domain ?? null;
3521
4749
  this.axisX = new Axis(options.axes?.x);
4750
+ const yCat = sy.type === "categorical";
4751
+ const yScale = makeScale(sy.type ?? "linear", sy.domain ?? [0, 1], sy.factors);
3522
4752
  this.yAxes.set("y", {
3523
4753
  id: "y",
3524
- scale: makeScale(sy.type ?? "linear", sy.domain ?? [0, 1]),
4754
+ scale: yScale,
3525
4755
  axis: new Axis(options.axes?.y),
3526
4756
  side: "left",
3527
- auto: sy.domain == null,
3528
- initial: sy.domain ?? null
4757
+ auto: !yCat && sy.domain == null,
4758
+ initial: yCat ? yScale.domain : sy.domain ?? null
3529
4759
  });
3530
4760
  this.isDark = options.theme === "dark";
3531
4761
  this.theme = options.theme === "dark" ? darkTheme : options.theme === "light" || options.theme == null ? lightTheme : options.theme;
3532
4762
  this.baseMargin = { ...DEFAULT_MARGIN, ...options.margin };
4763
+ this.bgFill = options.background;
4764
+ this.borderFill = options.border;
4765
+ this.title = typeof options.title === "string" ? { text: options.title } : options.title ?? null;
4766
+ this.legend = options.legend === true ? {} : options.legend || null;
3533
4767
  this.mode = options.mode ?? "pan";
3534
4768
  this.hoverEnabled = options.hover !== false;
3535
4769
  this.pickMode = options.pick ?? "x";
@@ -3583,6 +4817,21 @@ var Plot = class {
3583
4817
  boxShadow: "0 6px 18px rgba(0,0,0,0.25)"
3584
4818
  });
3585
4819
  container.appendChild(this.infoBox);
4820
+ this.legendDiv = document.createElement("div");
4821
+ Object.assign(this.legendDiv.style, {
4822
+ position: "absolute",
4823
+ display: "none",
4824
+ zIndex: "5",
4825
+ pointerEvents: "none",
4826
+ padding: "6px 8px",
4827
+ borderRadius: "6px",
4828
+ font: "12px system-ui, -apple-system, sans-serif",
4829
+ lineHeight: "1.5",
4830
+ background: this.isDark ? "rgba(15,23,42,0.85)" : "rgba(255,255,255,0.9)",
4831
+ color: this.isDark ? "#e2e8f0" : "#1e293b",
4832
+ border: `1px solid ${this.isDark ? "rgba(148,163,184,0.25)" : "rgba(100,116,139,0.2)"}`
4833
+ });
4834
+ container.appendChild(this.legendDiv);
3586
4835
  this.resizeObserver = new ResizeObserver(() => this.resize());
3587
4836
  this.resizeObserver.observe(container);
3588
4837
  this.resize();
@@ -3623,7 +4872,7 @@ var Plot = class {
3623
4872
  else rightCount++;
3624
4873
  }
3625
4874
  return {
3626
- top: this.baseMargin.top,
4875
+ top: this.baseMargin.top + (this.title ? TITLE_RESERVE : 0),
3627
4876
  bottom: this.baseMargin.bottom,
3628
4877
  left: this.baseMargin.left + Math.max(0, leftCount - 1) * Y_AXIS_GAP,
3629
4878
  right: this.baseMargin.right + rightCount * Y_AXIS_GAP
@@ -3702,6 +4951,67 @@ var Plot = class {
3702
4951
  addBar(opts) {
3703
4952
  return this.register(new BarLayer(this.gl, opts));
3704
4953
  }
4954
+ /**
4955
+ * Grouped (clustered) bars: one {@link BarLayer} per series, each shifted within
4956
+ * its category group so the bars sit side by side. Returns the layers in order.
4957
+ */
4958
+ addGroupedBars(opts) {
4959
+ const m = opts.series.length;
4960
+ if (m === 0) return [];
4961
+ const groupWidth = opts.groupWidth ?? 0.8;
4962
+ const slot = groupWidth / m;
4963
+ const barWidth = slot * (1 - (opts.gap ?? 0.1));
4964
+ const layers = [];
4965
+ for (let s = 0; s < m; s++) {
4966
+ const ser = opts.series[s];
4967
+ const off = (s - (m - 1) / 2) * slot;
4968
+ layers.push(
4969
+ new BarLayer(this.gl, {
4970
+ x: opts.x,
4971
+ y: ser.y,
4972
+ width: barWidth,
4973
+ offset: off,
4974
+ color: ser.color,
4975
+ name: ser.name,
4976
+ orientation: opts.orientation,
4977
+ yAxis: opts.yAxis
4978
+ })
4979
+ );
4980
+ }
4981
+ for (const l of layers) this.register(l);
4982
+ return layers;
4983
+ }
4984
+ /**
4985
+ * Stacked bars: each series is drawn from the running cumulative total of the
4986
+ * ones before it, so they stack. Returns the layers bottom-to-top.
4987
+ */
4988
+ addStackedBars(opts) {
4989
+ const n = opts.x.length;
4990
+ const cum = new Float64Array(n);
4991
+ const layers = [];
4992
+ for (const ser of opts.series) {
4993
+ const base = Float64Array.from(cum);
4994
+ const top = new Float64Array(n);
4995
+ for (let i = 0; i < n; i++) {
4996
+ top[i] = cum[i] + (ser.y[i] ?? 0);
4997
+ cum[i] = top[i];
4998
+ }
4999
+ layers.push(
5000
+ new BarLayer(this.gl, {
5001
+ x: opts.x,
5002
+ y: top,
5003
+ base,
5004
+ width: opts.width,
5005
+ color: ser.color,
5006
+ name: ser.name,
5007
+ orientation: opts.orientation,
5008
+ yAxis: opts.yAxis
5009
+ })
5010
+ );
5011
+ }
5012
+ for (const l of layers) this.register(l);
5013
+ return layers;
5014
+ }
3705
5015
  addArea(opts) {
3706
5016
  return this.register(new AreaLayer(this.gl, opts));
3707
5017
  }
@@ -3729,6 +5039,83 @@ var Plot = class {
3729
5039
  addCandlestick(opts) {
3730
5040
  return this.register(new CandlestickLayer(this.gl, opts));
3731
5041
  }
5042
+ /** Filled polygons (choropleth-capable). Rings are triangulated with earcut. */
5043
+ addPatches(opts) {
5044
+ return this.register(new PatchesLayer(this.gl, opts));
5045
+ }
5046
+ /** A pie / donut chart. Set `equalAspect: true` on the plot so it stays circular. */
5047
+ addPie(opts) {
5048
+ return this.register(new PieLayer(this.gl, opts));
5049
+ }
5050
+ /** An RGBA image / URL drawn over a data-space extent. URLs redraw on load. */
5051
+ addImage(opts) {
5052
+ return this.register(
5053
+ new ImageLayer(this.gl, {
5054
+ ...opts,
5055
+ onLoad: () => {
5056
+ this.requestRender();
5057
+ opts.onLoad?.();
5058
+ }
5059
+ })
5060
+ );
5061
+ }
5062
+ /**
5063
+ * A node-link graph. Provide node `x`/`y`, or omit them (with `nodes`, or let the
5064
+ * count be inferred from the edges) to auto-place with a force-directed layout.
5065
+ */
5066
+ addGraph(opts) {
5067
+ let { x, y } = opts;
5068
+ if (!x || !y) {
5069
+ const n = opts.nodes ?? opts.edges.reduce((m, [a, b]) => Math.max(m, a, b), -1) + 1;
5070
+ const layout = forceLayout(n, opts.edges);
5071
+ x = layout.x;
5072
+ y = layout.y;
5073
+ }
5074
+ return this.register(new GraphLayer(this.gl, { ...opts, x, y }));
5075
+ }
5076
+ /**
5077
+ * Stacked area: each series is filled from the running cumulative total of the
5078
+ * ones before it. Returns the layers bottom-to-top.
5079
+ */
5080
+ addStackedArea(opts) {
5081
+ const n = opts.x.length;
5082
+ const cum = new Float64Array(n);
5083
+ const layers = [];
5084
+ for (const ser of opts.series) {
5085
+ const base = Float64Array.from(cum);
5086
+ const top = new Float64Array(n);
5087
+ for (let i = 0; i < n; i++) {
5088
+ top[i] = cum[i] + (ser.y[i] ?? 0);
5089
+ cum[i] = top[i];
5090
+ }
5091
+ layers.push(
5092
+ new AreaLayer(this.gl, { x: opts.x, y: top, base, color: ser.color, name: ser.name, yAxis: opts.yAxis })
5093
+ );
5094
+ }
5095
+ for (const l of layers) this.register(l);
5096
+ return layers;
5097
+ }
5098
+ /**
5099
+ * Add a Canvas2D annotation (span / band / box / label) drawn above the data.
5100
+ * Returns a disposer that removes just this annotation.
5101
+ */
5102
+ addAnnotation(a) {
5103
+ this.annotations.push(a);
5104
+ this.requestRender();
5105
+ return () => {
5106
+ const i = this.annotations.indexOf(a);
5107
+ if (i >= 0) {
5108
+ this.annotations.splice(i, 1);
5109
+ this.requestRender();
5110
+ }
5111
+ };
5112
+ }
5113
+ /** Remove all annotations. */
5114
+ clearAnnotations() {
5115
+ if (this.annotations.length === 0) return;
5116
+ this.annotations = [];
5117
+ this.requestRender();
5118
+ }
3732
5119
  /** Compute an STFT of `signal` and render it as a heatmap (time × frequency). */
3733
5120
  addHeatmapSpectrogram(signal, opts = {}) {
3734
5121
  const s = spectrogram(signal, opts);
@@ -3759,14 +5146,16 @@ var Plot = class {
3759
5146
  /** Register an additional named y axis. Series opt in via `addLine({ yAxis })`. */
3760
5147
  addYAxis(id, opts = {}) {
3761
5148
  if (this.yAxes.has(id)) throw new Error(`Y axis "${id}" already exists`);
3762
- const { type, domain, side, color, ...axisConfig } = opts;
5149
+ const { type, domain, factors, side, color, ...axisConfig } = opts;
5150
+ const cat = type === "categorical";
5151
+ const scale = makeScale(type ?? "linear", domain ?? [0, 1], factors);
3763
5152
  this.yAxes.set(id, {
3764
5153
  id,
3765
- scale: makeScale(type ?? "linear", domain ?? [0, 1]),
5154
+ scale,
3766
5155
  axis: new Axis(axisConfig),
3767
5156
  side: side ?? "right",
3768
- auto: domain == null,
3769
- initial: domain ?? null,
5157
+ auto: !cat && domain == null,
5158
+ initial: cat ? scale.domain : domain ?? null,
3770
5159
  color
3771
5160
  });
3772
5161
  this.autoscale();
@@ -3875,6 +5264,7 @@ var Plot = class {
3875
5264
  this.selectionDiv.remove();
3876
5265
  this.tooltip.remove();
3877
5266
  this.infoBox.remove();
5267
+ this.legendDiv.remove();
3878
5268
  for (const l of this.layers) l.dispose();
3879
5269
  this.container.removeChild(this.gridCanvas);
3880
5270
  this.container.removeChild(this.dataCanvas);
@@ -3914,8 +5304,18 @@ var Plot = class {
3914
5304
  const primary = this.primaryY();
3915
5305
  const ticksX = this.axisX.resolve(this.scaleX);
3916
5306
  const ticksYPrimary = primary.axis.resolve(primary.scale);
5307
+ const styleX = resolveAxisStyle(this.axisX.config, this.theme);
5308
+ const styleYPrimary = resolveAxisStyle(primary.axis.config, this.theme, primary.color);
3917
5309
  this.gridCtx.clearRect(0, 0, layout.cssWidth, layout.cssHeight);
3918
- drawGrid(this.gridCtx, region, this.scaleX, primary.scale, ticksX, ticksYPrimary, this.theme);
5310
+ if (this.borderFill) {
5311
+ this.gridCtx.fillStyle = this.borderFill;
5312
+ this.gridCtx.fillRect(0, 0, layout.cssWidth, layout.cssHeight);
5313
+ }
5314
+ if (this.bgFill) {
5315
+ this.gridCtx.fillStyle = this.bgFill;
5316
+ this.gridCtx.fillRect(region.left, region.top, region.width, region.height);
5317
+ }
5318
+ drawGrid(this.gridCtx, region, this.scaleX, primary.scale, ticksX, ticksYPrimary, styleX, styleYPrimary);
3919
5319
  const gl = this.gl;
3920
5320
  const devW = this.dataCanvas.width;
3921
5321
  const devH = this.dataCanvas.height;
@@ -3953,19 +5353,21 @@ var Plot = class {
3953
5353
  this.dataCtx.clearRect(0, 0, layout.cssWidth, layout.cssHeight);
3954
5354
  this.dataCtx.drawImage(this.sharedCanvas, 0, 0, layout.cssWidth, layout.cssHeight);
3955
5355
  this.axisCtx.clearRect(0, 0, layout.cssWidth, layout.cssHeight);
3956
- drawXAxis(this.axisCtx, region, this.scaleX, ticksX, this.theme, this.axisX.config.title);
5356
+ drawXAxis(this.axisCtx, region, this.scaleX, ticksX, styleX, this.axisX.config.title);
3957
5357
  const positions = this.yAxisPositions();
3958
5358
  for (const ya of this.yAxes.values()) {
3959
5359
  const pos = positions.get(ya.id);
3960
5360
  const ticks = ya.axis.resolve(ya.scale);
3961
- drawYAxis(this.axisCtx, region, ya.scale, ticks, this.theme, {
5361
+ const styleY = ya === primary ? styleYPrimary : resolveAxisStyle(ya.axis.config, this.theme, ya.color);
5362
+ drawYAxis(this.axisCtx, region, ya.scale, ticks, styleY, {
3962
5363
  x: pos.x,
3963
5364
  side: ya.side,
3964
5365
  title: ya.axis.config.title,
3965
- color: ya.color,
3966
5366
  titleX: pos.titleX
3967
5367
  });
3968
5368
  }
5369
+ if (this.title) drawTitle(this.axisCtx, region, this.title, this.isDark);
5370
+ if (this.annotations.length) this.renderAnnotations(region);
3969
5371
  if (this.crosshair && this.pressPx) {
3970
5372
  drawCrosshairXY(this.axisCtx, region, this.pressPx.x, this.pressPx.y, this.theme);
3971
5373
  }
@@ -3976,6 +5378,129 @@ var Plot = class {
3976
5378
  if (this.pointInfo === "hover") this.selected = null;
3977
5379
  }
3978
5380
  this.updateInfoBox(region);
5381
+ this.updateLegend(region);
5382
+ }
5383
+ /** Named series that can appear in the legend: any layer exposing name + colorCss. */
5384
+ legendEntries() {
5385
+ const out = [];
5386
+ for (const l of this.layers) {
5387
+ const a = l;
5388
+ if (typeof a.name === "string" && a.name && typeof a.colorCss === "string") {
5389
+ out.push({ name: a.name, colorCss: a.colorCss });
5390
+ }
5391
+ }
5392
+ return out;
5393
+ }
5394
+ /** Rebuild and position the legend overlay (or hide it). */
5395
+ updateLegend(region) {
5396
+ const div = this.legendDiv;
5397
+ const entries = this.legend ? this.legendEntries() : [];
5398
+ if (!this.legend || entries.length === 0) {
5399
+ div.style.display = "none";
5400
+ return;
5401
+ }
5402
+ const cfg = this.legend;
5403
+ if (cfg.background) div.style.background = cfg.background;
5404
+ if (cfg.border) div.style.border = `1px solid ${cfg.border}`;
5405
+ if (cfg.textColor) div.style.color = cfg.textColor;
5406
+ if (cfg.font) div.style.font = cfg.font;
5407
+ const horizontal = cfg.orientation === "horizontal";
5408
+ div.style.display = "flex";
5409
+ div.style.flexDirection = horizontal ? "row" : "column";
5410
+ div.style.gap = horizontal ? "12px" : "3px";
5411
+ div.replaceChildren();
5412
+ for (const e of entries) {
5413
+ const row = document.createElement("div");
5414
+ row.style.display = "flex";
5415
+ row.style.alignItems = "center";
5416
+ row.style.gap = "6px";
5417
+ const swatch = document.createElement("span");
5418
+ Object.assign(swatch.style, {
5419
+ width: "10px",
5420
+ height: "10px",
5421
+ borderRadius: "2px",
5422
+ background: e.colorCss,
5423
+ flex: "0 0 auto"
5424
+ });
5425
+ const label = document.createElement("span");
5426
+ label.textContent = e.name;
5427
+ row.appendChild(swatch);
5428
+ row.appendChild(label);
5429
+ div.appendChild(row);
5430
+ }
5431
+ const inset = 8;
5432
+ const pos = cfg.position ?? "top-right";
5433
+ const w = div.offsetWidth;
5434
+ const h = div.offsetHeight;
5435
+ const left = pos.endsWith("left") ? region.left + inset : region.left + region.width - w - inset;
5436
+ const top = pos.startsWith("top") ? region.top + inset : region.top + region.height - h - inset;
5437
+ div.style.left = `${Math.max(0, left)}px`;
5438
+ div.style.top = `${Math.max(0, top)}px`;
5439
+ }
5440
+ /** Draw all annotations, projected through the scales and clipped to the region. */
5441
+ renderAnnotations(region) {
5442
+ const ctx = this.axisCtx;
5443
+ const left = region.left, right = region.left + region.width;
5444
+ const top = region.top, bottom = region.top + region.height;
5445
+ const yScaleOf = (id) => (this.yAxes.get(id ?? "y") ?? this.primaryY()).scale;
5446
+ const px = (v) => pxX(region, this.scaleX.norm(v));
5447
+ const py = (s, v) => pxY(region, s.norm(v));
5448
+ ctx.save();
5449
+ ctx.beginPath();
5450
+ ctx.rect(left, top, right - left, bottom - top);
5451
+ ctx.clip();
5452
+ for (const a of this.annotations) {
5453
+ ctx.setLineDash([]);
5454
+ if (a.type === "span") {
5455
+ ctx.strokeStyle = a.color ?? this.theme.axis;
5456
+ ctx.lineWidth = a.width ?? 1;
5457
+ if (a.dash) ctx.setLineDash(a.dash);
5458
+ ctx.beginPath();
5459
+ if (a.dim === "x") {
5460
+ const x = Math.round(px(a.value)) + 0.5;
5461
+ ctx.moveTo(x, top);
5462
+ ctx.lineTo(x, bottom);
5463
+ } else {
5464
+ const y = Math.round(py(yScaleOf(a.yAxis), a.value)) + 0.5;
5465
+ ctx.moveTo(left, y);
5466
+ ctx.lineTo(right, y);
5467
+ }
5468
+ ctx.stroke();
5469
+ } else if (a.type === "band") {
5470
+ ctx.fillStyle = a.color ?? "rgba(59,130,246,0.15)";
5471
+ if (a.dim === "x") {
5472
+ const x0 = px(a.from), x1 = px(a.to);
5473
+ ctx.fillRect(Math.min(x0, x1), top, Math.abs(x1 - x0), bottom - top);
5474
+ } else {
5475
+ const s = yScaleOf(a.yAxis);
5476
+ const y0 = py(s, a.from), y1 = py(s, a.to);
5477
+ ctx.fillRect(left, Math.min(y0, y1), right - left, Math.abs(y1 - y0));
5478
+ }
5479
+ } else if (a.type === "box") {
5480
+ const s = yScaleOf(a.yAxis);
5481
+ const x0 = px(a.x[0]), x1 = px(a.x[1]);
5482
+ const y0 = py(s, a.y[0]), y1 = py(s, a.y[1]);
5483
+ const rx = Math.min(x0, x1), ry = Math.min(y0, y1);
5484
+ const rw = Math.abs(x1 - x0), rh = Math.abs(y1 - y0);
5485
+ if (a.color) {
5486
+ ctx.fillStyle = a.color;
5487
+ ctx.fillRect(rx, ry, rw, rh);
5488
+ }
5489
+ if (a.border) {
5490
+ ctx.strokeStyle = a.border;
5491
+ ctx.lineWidth = 1;
5492
+ ctx.strokeRect(rx + 0.5, ry + 0.5, rw, rh);
5493
+ }
5494
+ } else {
5495
+ const s = yScaleOf(a.yAxis);
5496
+ ctx.fillStyle = a.color ?? this.theme.text;
5497
+ ctx.font = a.font ?? this.theme.font;
5498
+ ctx.textAlign = a.align ?? "left";
5499
+ ctx.textBaseline = "middle";
5500
+ ctx.fillText(a.text, px(a.x), py(s, a.y));
5501
+ }
5502
+ }
5503
+ ctx.restore();
3979
5504
  }
3980
5505
  renderHover(region) {
3981
5506
  const cursor = this.hoverPx;
@@ -4999,154 +6524,1076 @@ function fmt(v) {
4999
6524
  return String(Math.round(v * 1e3) / 1e3);
5000
6525
  }
5001
6526
 
5002
- // src/plot3d/mat4.ts
5003
- function identity() {
5004
- const m = new Float32Array(16);
5005
- m[0] = m[5] = m[10] = m[15] = 1;
5006
- return m;
5007
- }
5008
- function multiply(a, b) {
5009
- const o = new Float32Array(16);
5010
- for (let c = 0; c < 4; c++) {
5011
- for (let r = 0; r < 4; r++) {
5012
- let s = 0;
5013
- for (let k = 0; k < 4; k++) s += a[k * 4 + r] * b[c * 4 + k];
5014
- o[c * 4 + r] = s;
5015
- }
5016
- }
5017
- return o;
5018
- }
5019
- function perspective(fovy, aspect, near, far) {
5020
- const f = 1 / Math.tan(fovy / 2);
5021
- const nf = 1 / (near - far);
5022
- const m = new Float32Array(16);
5023
- m[0] = f / aspect;
5024
- m[5] = f;
5025
- m[10] = (far + near) * nf;
5026
- m[11] = -1;
5027
- m[14] = 2 * far * near * nf;
5028
- return m;
5029
- }
5030
- var sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
5031
- var cross = (a, b) => [
5032
- a[1] * b[2] - a[2] * b[1],
5033
- a[2] * b[0] - a[0] * b[2],
5034
- a[0] * b[1] - a[1] * b[0]
5035
- ];
5036
- function normalize2(v) {
5037
- const l = Math.hypot(v[0], v[1], v[2]) || 1;
5038
- return [v[0] / l, v[1] / l, v[2] / l];
5039
- }
5040
- function lookAt(eye, center, up) {
5041
- const z = normalize2(sub(eye, center));
5042
- const x = normalize2(cross(up, z));
5043
- const y = cross(z, x);
5044
- const m = identity();
5045
- m[0] = x[0];
5046
- m[4] = x[1];
5047
- m[8] = x[2];
5048
- m[1] = y[0];
5049
- m[5] = y[1];
5050
- m[9] = y[2];
5051
- m[2] = z[0];
5052
- m[6] = z[1];
5053
- m[10] = z[2];
5054
- m[12] = -(x[0] * eye[0] + x[1] * eye[1] + x[2] * eye[2]);
5055
- m[13] = -(y[0] * eye[0] + y[1] * eye[1] + y[2] * eye[2]);
5056
- m[14] = -(z[0] * eye[0] + z[1] * eye[1] + z[2] * eye[2]);
5057
- return m;
5058
- }
5059
- function transformPoint(m, x, y, z) {
5060
- return [
5061
- m[0] * x + m[4] * y + m[8] * z + m[12],
5062
- m[1] * x + m[5] * y + m[9] * z + m[13],
5063
- m[2] * x + m[6] * y + m[10] * z + m[14],
5064
- m[3] * x + m[7] * y + m[11] * z + m[15]
6527
+ // src/plot3d/bar3d.ts
6528
+ function buildCube() {
6529
+ const faces = [
6530
+ { n: [1, 0, 0], v: [[0.5, 0, -0.5], [0.5, 1, -0.5], [0.5, 1, 0.5], [0.5, 0, -0.5], [0.5, 1, 0.5], [0.5, 0, 0.5]] },
6531
+ { n: [-1, 0, 0], v: [[-0.5, 0, 0.5], [-0.5, 1, 0.5], [-0.5, 1, -0.5], [-0.5, 0, 0.5], [-0.5, 1, -0.5], [-0.5, 0, -0.5]] },
6532
+ { n: [0, 1, 0], v: [[-0.5, 1, -0.5], [0.5, 1, -0.5], [0.5, 1, 0.5], [-0.5, 1, -0.5], [0.5, 1, 0.5], [-0.5, 1, 0.5]] },
6533
+ { n: [0, -1, 0], v: [[-0.5, 0, 0.5], [0.5, 0, 0.5], [0.5, 0, -0.5], [-0.5, 0, 0.5], [0.5, 0, -0.5], [-0.5, 0, -0.5]] },
6534
+ { n: [0, 0, 1], v: [[-0.5, 0, 0.5], [0.5, 0, 0.5], [0.5, 1, 0.5], [-0.5, 0, 0.5], [0.5, 1, 0.5], [-0.5, 1, 0.5]] },
6535
+ { n: [0, 0, -1], v: [[0.5, 0, -0.5], [-0.5, 0, -0.5], [-0.5, 1, -0.5], [0.5, 0, -0.5], [-0.5, 1, -0.5], [0.5, 1, -0.5]] }
5065
6536
  ];
6537
+ const out = [];
6538
+ for (const f of faces) for (const p of f.v) out.push(p[0], p[1], p[2], f.n[0], f.n[1], f.n[2]);
6539
+ return new Float32Array(out);
5066
6540
  }
5067
- function scaleTranslate(s, t) {
5068
- const m = identity();
5069
- m[0] = s[0];
5070
- m[5] = s[1];
5071
- m[10] = s[2];
5072
- m[12] = t[0];
5073
- m[13] = t[1];
5074
- m[14] = t[2];
5075
- return m;
5076
- }
5077
-
5078
- // src/plot3d/pointcloud.ts
5079
- var VERT9 = (
6541
+ var CUBE = buildCube();
6542
+ var VERT10 = (
5080
6543
  /* glsl */
5081
6544
  `#version 300 es
5082
6545
  precision highp float;
5083
- layout(location = 0) in vec3 aPos;
5084
- layout(location = 1) in vec3 aColor;
6546
+ layout(location = 0) in vec3 aCube; // unit cube (y in [0,1])
6547
+ layout(location = 1) in vec3 aNormal;
6548
+ layout(location = 2) in vec2 aBase; // instance x,z
6549
+ layout(location = 3) in float aHeight;
6550
+ layout(location = 4) in vec3 aColor;
5085
6551
  uniform mat4 uMVP;
5086
- uniform float uSize;
6552
+ uniform float uWidth;
5087
6553
  out vec3 vColor;
6554
+ out vec3 vN;
5088
6555
  void main() {
6556
+ vec3 world = vec3(aBase.x + aCube.x * uWidth, aCube.y * aHeight, aBase.y + aCube.z * uWidth);
5089
6557
  vColor = aColor;
5090
- gl_Position = uMVP * vec4(aPos, 1.0);
5091
- gl_PointSize = uSize;
6558
+ vN = aNormal;
6559
+ gl_Position = uMVP * vec4(world, 1.0);
5092
6560
  }`
5093
6561
  );
5094
- var FRAG11 = (
6562
+ var FRAG12 = (
5095
6563
  /* glsl */
5096
6564
  `#version 300 es
5097
6565
  precision highp float;
5098
6566
  in vec3 vColor;
6567
+ in vec3 vN;
6568
+ uniform vec3 uLightDir;
6569
+ uniform float uAmbient;
5099
6570
  out vec4 outColor;
5100
6571
  void main() {
5101
- vec2 d = gl_PointCoord - 0.5;
5102
- if (length(d) > 0.5) discard;
5103
- outColor = vec4(vColor, 1.0);
6572
+ float d = max(dot(normalize(vN), normalize(uLightDir)), 0.0);
6573
+ float shade = uAmbient + (1.0 - uAmbient) * d;
6574
+ outColor = vec4(vColor * shade, 1.0);
5104
6575
  }`
5105
6576
  );
5106
- var programCache10 = /* @__PURE__ */ new WeakMap();
5107
- function getProgram9(gl) {
5108
- let p = programCache10.get(gl);
6577
+ var programCache13 = /* @__PURE__ */ new WeakMap();
6578
+ function getProgram10(gl) {
6579
+ let p = programCache13.get(gl);
5109
6580
  if (!p) {
5110
- p = createProgram(gl, VERT9, FRAG11);
5111
- programCache10.set(gl, p);
6581
+ p = createProgram(gl, VERT10, FRAG12);
6582
+ programCache13.set(gl, p);
5112
6583
  }
5113
6584
  return p;
5114
6585
  }
5115
- var counter13 = 0;
5116
- var PointCloudLayer = class {
5117
- constructor(gl, opts) {
5118
- this.id = `points3d-${counter13++}`;
5119
- this.gl = gl;
5120
- this.program = getProgram9(gl);
5121
- this.size = opts.size ?? 4;
6586
+ function medianSpacing3(vals, n) {
6587
+ const s = Array.from({ length: n }, (_, i) => vals[i]).sort((a, b) => a - b);
6588
+ const diffs = [];
6589
+ for (let i = 1; i < n; i++) {
6590
+ const d = s[i] - s[i - 1];
6591
+ if (d > 1e-9) diffs.push(d);
6592
+ }
6593
+ if (!diffs.length) return 1;
6594
+ diffs.sort((a, b) => a - b);
6595
+ return diffs[Math.floor(diffs.length / 2)];
6596
+ }
6597
+ var counter17 = 0;
6598
+ var Bar3DLayer = class {
6599
+ constructor(gl, opts) {
6600
+ this.buffers = [];
6601
+ this.cInfo = null;
6602
+ this.lightDir = [0.5, 1, 0.35];
6603
+ this.ambient = 0.35;
6604
+ this.id = `bar3d-${counter17++}`;
6605
+ this.gl = gl;
6606
+ this.program = getProgram10(gl);
6607
+ this.name = opts.name;
5122
6608
  const n = Math.min(opts.x.length, opts.y.length, opts.z.length);
5123
6609
  this.count = n;
5124
- const base = opts.color != null ? Array.isArray(opts.color) ? opts.color : parseColor(opts.color) : [0.4, 0.7, 1, 1];
6610
+ const dx = medianSpacing3(opts.x, n), dz = medianSpacing3(opts.z, n);
6611
+ this.width = opts.width ?? Math.min(dx, dz) * 0.7;
6612
+ const base = opts.color != null ? Array.isArray(opts.color) ? opts.color : parseColor(opts.color) : [0.24, 0.55, 0.96, 1];
6613
+ const cvals = opts.colorBy?.values ?? (opts.colorBy ? opts.y : null);
5125
6614
  const cmap = opts.colorBy ? colormap(opts.colorBy.colormap ?? "viridis") : null;
5126
6615
  let lo = opts.colorBy?.domain?.[0] ?? Infinity;
5127
6616
  let hi = opts.colorBy?.domain?.[1] ?? -Infinity;
5128
- if (opts.colorBy && !opts.colorBy.domain) {
5129
- const v = opts.colorBy.values;
6617
+ if (cmap && cvals && !opts.colorBy?.domain) {
5130
6618
  for (let i = 0; i < n; i++) {
5131
- const t = v[i];
5132
- if (t < lo) lo = t;
5133
- if (t > hi) hi = t;
6619
+ const v = cvals[i];
6620
+ if (v < lo) lo = v;
6621
+ if (v > hi) hi = v;
5134
6622
  }
5135
6623
  }
5136
6624
  const span = hi - lo || 1;
5137
- const data = new Float32Array(n * 6);
5138
- let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity, minZ = Infinity, maxZ = -Infinity;
6625
+ if (cmap) this.cInfo = { colormap: opts.colorBy.colormap ?? "viridis", domain: [lo, hi], label: opts.name };
6626
+ else this.colorCss = opts.color != null && typeof opts.color === "string" ? opts.color : "#3b82f6";
6627
+ const inst = new Float32Array(n * 6);
6628
+ this.positions = new Float32Array(n * 3);
6629
+ let minX = Infinity, maxX = -Infinity, minZ = Infinity, maxZ = -Infinity, minY = 0, maxY = 0;
6630
+ const hw = this.width / 2;
5139
6631
  for (let i = 0; i < n; i++) {
5140
- const x = opts.x[i], y = opts.y[i], z = opts.z[i];
5141
- data[i * 6] = x;
5142
- data[i * 6 + 1] = y;
5143
- data[i * 6 + 2] = z;
6632
+ const x = opts.x[i], z = opts.z[i], h = opts.y[i];
6633
+ inst[i * 6] = x;
6634
+ inst[i * 6 + 1] = z;
6635
+ inst[i * 6 + 2] = h;
6636
+ this.positions[i * 3] = x;
6637
+ this.positions[i * 3 + 1] = h;
6638
+ this.positions[i * 3 + 2] = z;
5144
6639
  let c;
5145
- if (cmap && opts.colorBy) c = cmap((opts.colorBy.values[i] - lo) / span);
6640
+ if (cmap && cvals) c = cmap((cvals[i] - lo) / span);
5146
6641
  else c = [base[0], base[1], base[2]];
5147
- data[i * 6 + 3] = c[0];
5148
- data[i * 6 + 4] = c[1];
5149
- data[i * 6 + 5] = c[2];
6642
+ inst[i * 6 + 3] = c[0];
6643
+ inst[i * 6 + 4] = c[1];
6644
+ inst[i * 6 + 5] = c[2];
6645
+ if (x - hw < minX) minX = x - hw;
6646
+ if (x + hw > maxX) maxX = x + hw;
6647
+ if (z - hw < minZ) minZ = z - hw;
6648
+ if (z + hw > maxZ) maxZ = z + hw;
6649
+ if (h < minY) minY = h;
6650
+ if (h > maxY) maxY = h;
6651
+ }
6652
+ this.b3 = { x: [minX, maxX], y: [minY, maxY], z: [minZ, maxZ] };
6653
+ this.vao = gl.createVertexArray();
6654
+ gl.bindVertexArray(this.vao);
6655
+ const cubeBuf = gl.createBuffer();
6656
+ gl.bindBuffer(gl.ARRAY_BUFFER, cubeBuf);
6657
+ gl.bufferData(gl.ARRAY_BUFFER, CUBE, gl.STATIC_DRAW);
6658
+ gl.enableVertexAttribArray(0);
6659
+ gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 24, 0);
6660
+ gl.enableVertexAttribArray(1);
6661
+ gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 12);
6662
+ const instBuf = gl.createBuffer();
6663
+ gl.bindBuffer(gl.ARRAY_BUFFER, instBuf);
6664
+ gl.bufferData(gl.ARRAY_BUFFER, inst, gl.STATIC_DRAW);
6665
+ gl.enableVertexAttribArray(2);
6666
+ gl.vertexAttribPointer(2, 2, gl.FLOAT, false, 24, 0);
6667
+ gl.vertexAttribDivisor(2, 1);
6668
+ gl.enableVertexAttribArray(3);
6669
+ gl.vertexAttribPointer(3, 1, gl.FLOAT, false, 24, 8);
6670
+ gl.vertexAttribDivisor(3, 1);
6671
+ gl.enableVertexAttribArray(4);
6672
+ gl.vertexAttribPointer(4, 3, gl.FLOAT, false, 24, 12);
6673
+ gl.vertexAttribDivisor(4, 1);
6674
+ gl.bindVertexArray(null);
6675
+ this.buffers = [cubeBuf, instBuf];
6676
+ this.uniforms = uniformLocations(gl, this.program, ["uMVP", "uWidth", "uLightDir", "uAmbient"]);
6677
+ }
6678
+ bounds3() {
6679
+ return this.count ? this.b3 : null;
6680
+ }
6681
+ colorInfo() {
6682
+ return this.cInfo;
6683
+ }
6684
+ pickData() {
6685
+ return this.count ? { positions: this.positions } : null;
6686
+ }
6687
+ /** Set the light direction (world space) and ambient term (0..1). */
6688
+ setLight(dir, ambient) {
6689
+ this.lightDir = dir;
6690
+ this.ambient = ambient;
6691
+ }
6692
+ draw(gl, mvp) {
6693
+ if (this.count === 0) return;
6694
+ gl.useProgram(this.program);
6695
+ gl.uniformMatrix4fv(this.uniforms.uMVP, false, mvp);
6696
+ gl.uniform1f(this.uniforms.uWidth, this.width);
6697
+ gl.uniform3f(this.uniforms.uLightDir, this.lightDir[0], this.lightDir[1], this.lightDir[2]);
6698
+ gl.uniform1f(this.uniforms.uAmbient, this.ambient);
6699
+ gl.bindVertexArray(this.vao);
6700
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 36, this.count);
6701
+ gl.bindVertexArray(null);
6702
+ }
6703
+ dispose() {
6704
+ this.gl.deleteVertexArray(this.vao);
6705
+ for (const b of this.buffers) this.gl.deleteBuffer(b);
6706
+ }
6707
+ };
6708
+
6709
+ // src/plot3d/contour3d.ts
6710
+ var CASES2 = [
6711
+ [],
6712
+ [[3, 0]],
6713
+ [[0, 1]],
6714
+ [[3, 1]],
6715
+ [[1, 2]],
6716
+ [[3, 0], [1, 2]],
6717
+ [[0, 2]],
6718
+ [[3, 2]],
6719
+ [[2, 3]],
6720
+ [[2, 0]],
6721
+ [[0, 1], [2, 3]],
6722
+ [[2, 1]],
6723
+ [[1, 3]],
6724
+ [[1, 0]],
6725
+ [[0, 3]],
6726
+ []
6727
+ ];
6728
+ var VERT11 = (
6729
+ /* glsl */
6730
+ `#version 300 es
6731
+ precision highp float;
6732
+ layout(location = 0) in vec3 aPos;
6733
+ layout(location = 1) in vec3 aColor;
6734
+ uniform mat4 uMVP;
6735
+ out vec3 vColor;
6736
+ void main() { vColor = aColor; gl_Position = uMVP * vec4(aPos, 1.0); }`
6737
+ );
6738
+ var FRAG13 = (
6739
+ /* glsl */
6740
+ `#version 300 es
6741
+ precision highp float;
6742
+ in vec3 vColor;
6743
+ out vec4 outColor;
6744
+ void main() { outColor = vec4(vColor, 1.0); }`
6745
+ );
6746
+ var programCache14 = /* @__PURE__ */ new WeakMap();
6747
+ function getProgram11(gl) {
6748
+ let p = programCache14.get(gl);
6749
+ if (!p) {
6750
+ p = createProgram(gl, VERT11, FRAG13);
6751
+ programCache14.set(gl, p);
6752
+ }
6753
+ return p;
6754
+ }
6755
+ var counter18 = 0;
6756
+ var Contour3DLayer = class {
6757
+ constructor(gl, opts) {
6758
+ this.cInfo = null;
6759
+ this.id = `contour3d-${counter18++}`;
6760
+ this.gl = gl;
6761
+ this.program = getProgram11(gl);
6762
+ this.name = opts.name;
6763
+ const { cols, rows, values } = opts;
6764
+ const [x0, x1] = opts.extentX ?? [0, cols - 1];
6765
+ const [z0, z1] = opts.extentZ ?? [0, rows - 1];
6766
+ let vmin = Infinity, vmax = -Infinity;
6767
+ for (let i = 0; i < values.length; i++) {
6768
+ const v = values[i];
6769
+ if (v < vmin) vmin = v;
6770
+ if (v > vmax) vmax = v;
6771
+ }
6772
+ const lspan = vmax - vmin || 1;
6773
+ const count = typeof opts.levels === "number" ? opts.levels : 10;
6774
+ const levels = Array.isArray(opts.levels) ? opts.levels : Array.from({ length: count }, (_, i) => vmin + lspan * (i + 1) / (count + 1));
6775
+ const cmap = colormap(opts.colormap ?? "viridis");
6776
+ const fixed = opts.color != null ? Array.isArray(opts.color) ? opts.color : parseColor(opts.color) : null;
6777
+ if (!fixed) this.cInfo = { colormap: opts.colormap ?? "viridis", domain: [vmin, vmax], label: opts.name };
6778
+ const wx = (c) => x0 + c / (cols - 1) * (x1 - x0);
6779
+ const wz = (r) => z0 + r / (rows - 1) * (z1 - z0);
6780
+ const at = (c, r) => values[r * cols + c];
6781
+ const data = [];
6782
+ for (const L of levels) {
6783
+ const col = fixed ? [fixed[0], fixed[1], fixed[2]] : cmap((L - vmin) / lspan);
6784
+ for (let r = 0; r < rows - 1; r++) {
6785
+ for (let c = 0; c < cols - 1; c++) {
6786
+ const v0 = at(c, r), v1 = at(c + 1, r), v2 = at(c + 1, r + 1), v3 = at(c, r + 1);
6787
+ const idx = (v0 >= L ? 1 : 0) | (v1 >= L ? 2 : 0) | (v2 >= L ? 4 : 0) | (v3 >= L ? 8 : 0);
6788
+ const segs = CASES2[idx];
6789
+ if (segs.length === 0) continue;
6790
+ const lerp = (t, ax, az, bx, bz) => [ax + (bx - ax) * t, az + (bz - az) * t];
6791
+ const edge = (e) => {
6792
+ if (e === 0) return lerp((L - v0) / (v1 - v0 || 1e-9), wx(c), wz(r), wx(c + 1), wz(r));
6793
+ if (e === 1) return lerp((L - v1) / (v2 - v1 || 1e-9), wx(c + 1), wz(r), wx(c + 1), wz(r + 1));
6794
+ if (e === 2) return lerp((L - v2) / (v3 - v2 || 1e-9), wx(c + 1), wz(r + 1), wx(c), wz(r + 1));
6795
+ return lerp((L - v3) / (v0 - v3 || 1e-9), wx(c), wz(r + 1), wx(c), wz(r));
6796
+ };
6797
+ for (const [ea, eb] of segs) {
6798
+ const pa = edge(ea), pb = edge(eb);
6799
+ data.push(pa[0], L, pa[1], col[0], col[1], col[2]);
6800
+ data.push(pb[0], L, pb[1], col[0], col[1], col[2]);
6801
+ }
6802
+ }
6803
+ }
6804
+ }
6805
+ this.vertCount = data.length / 6;
6806
+ this.b3 = { x: [x0, x1], y: [vmin, vmax], z: [z0, z1] };
6807
+ this.vao = gl.createVertexArray();
6808
+ this.buffer = gl.createBuffer();
6809
+ gl.bindVertexArray(this.vao);
6810
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
6811
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
6812
+ gl.enableVertexAttribArray(0);
6813
+ gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 24, 0);
6814
+ gl.enableVertexAttribArray(1);
6815
+ gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 12);
6816
+ gl.bindVertexArray(null);
6817
+ this.uniforms = uniformLocations(gl, this.program, ["uMVP"]);
6818
+ }
6819
+ bounds3() {
6820
+ return this.vertCount ? this.b3 : null;
6821
+ }
6822
+ colorInfo() {
6823
+ return this.cInfo;
6824
+ }
6825
+ draw(gl, mvp) {
6826
+ if (this.vertCount === 0) return;
6827
+ gl.useProgram(this.program);
6828
+ gl.uniformMatrix4fv(this.uniforms.uMVP, false, mvp);
6829
+ gl.bindVertexArray(this.vao);
6830
+ gl.drawArrays(gl.LINES, 0, this.vertCount);
6831
+ gl.bindVertexArray(null);
6832
+ }
6833
+ dispose() {
6834
+ this.gl.deleteVertexArray(this.vao);
6835
+ this.gl.deleteBuffer(this.buffer);
6836
+ }
6837
+ };
6838
+
6839
+ // src/plot3d/marching-cubes.ts
6840
+ var EDGE_TABLE = [
6841
+ 0,
6842
+ 265,
6843
+ 515,
6844
+ 778,
6845
+ 1030,
6846
+ 1295,
6847
+ 1541,
6848
+ 1804,
6849
+ 2060,
6850
+ 2309,
6851
+ 2575,
6852
+ 2822,
6853
+ 3082,
6854
+ 3331,
6855
+ 3593,
6856
+ 3840,
6857
+ 400,
6858
+ 153,
6859
+ 915,
6860
+ 666,
6861
+ 1430,
6862
+ 1183,
6863
+ 1941,
6864
+ 1692,
6865
+ 2460,
6866
+ 2197,
6867
+ 2975,
6868
+ 2710,
6869
+ 3482,
6870
+ 3219,
6871
+ 3993,
6872
+ 3728,
6873
+ 560,
6874
+ 825,
6875
+ 51,
6876
+ 314,
6877
+ 1590,
6878
+ 1855,
6879
+ 1077,
6880
+ 1340,
6881
+ 2620,
6882
+ 2869,
6883
+ 2111,
6884
+ 2358,
6885
+ 3642,
6886
+ 3891,
6887
+ 3129,
6888
+ 3376,
6889
+ 928,
6890
+ 681,
6891
+ 419,
6892
+ 170,
6893
+ 1958,
6894
+ 1711,
6895
+ 1445,
6896
+ 1196,
6897
+ 2988,
6898
+ 2725,
6899
+ 2479,
6900
+ 2214,
6901
+ 4010,
6902
+ 3747,
6903
+ 3497,
6904
+ 3232,
6905
+ 1120,
6906
+ 1385,
6907
+ 1635,
6908
+ 1898,
6909
+ 102,
6910
+ 367,
6911
+ 613,
6912
+ 876,
6913
+ 3180,
6914
+ 3429,
6915
+ 3695,
6916
+ 3942,
6917
+ 2154,
6918
+ 2403,
6919
+ 2665,
6920
+ 2912,
6921
+ 1520,
6922
+ 1273,
6923
+ 2035,
6924
+ 1786,
6925
+ 502,
6926
+ 255,
6927
+ 1013,
6928
+ 764,
6929
+ 3580,
6930
+ 3317,
6931
+ 4095,
6932
+ 3830,
6933
+ 2554,
6934
+ 2291,
6935
+ 3065,
6936
+ 2800,
6937
+ 1616,
6938
+ 1881,
6939
+ 1107,
6940
+ 1370,
6941
+ 598,
6942
+ 863,
6943
+ 85,
6944
+ 348,
6945
+ 3676,
6946
+ 3925,
6947
+ 3167,
6948
+ 3414,
6949
+ 2650,
6950
+ 2899,
6951
+ 2137,
6952
+ 2384,
6953
+ 1984,
6954
+ 1737,
6955
+ 1475,
6956
+ 1226,
6957
+ 966,
6958
+ 719,
6959
+ 453,
6960
+ 204,
6961
+ 4044,
6962
+ 3781,
6963
+ 3535,
6964
+ 3270,
6965
+ 3018,
6966
+ 2755,
6967
+ 2505,
6968
+ 2240,
6969
+ 2240,
6970
+ 2505,
6971
+ 2755,
6972
+ 3018,
6973
+ 3270,
6974
+ 3535,
6975
+ 3781,
6976
+ 4044,
6977
+ 204,
6978
+ 453,
6979
+ 719,
6980
+ 966,
6981
+ 1226,
6982
+ 1475,
6983
+ 1737,
6984
+ 1984,
6985
+ 2384,
6986
+ 2137,
6987
+ 2899,
6988
+ 2650,
6989
+ 3414,
6990
+ 3167,
6991
+ 3925,
6992
+ 3676,
6993
+ 348,
6994
+ 85,
6995
+ 863,
6996
+ 598,
6997
+ 1370,
6998
+ 1107,
6999
+ 1881,
7000
+ 1616,
7001
+ 2800,
7002
+ 3065,
7003
+ 2291,
7004
+ 2554,
7005
+ 3830,
7006
+ 4095,
7007
+ 3317,
7008
+ 3580,
7009
+ 764,
7010
+ 1013,
7011
+ 255,
7012
+ 502,
7013
+ 1786,
7014
+ 2035,
7015
+ 1273,
7016
+ 1520,
7017
+ 2912,
7018
+ 2665,
7019
+ 2403,
7020
+ 2154,
7021
+ 3942,
7022
+ 3695,
7023
+ 3429,
7024
+ 3180,
7025
+ 876,
7026
+ 613,
7027
+ 367,
7028
+ 102,
7029
+ 1898,
7030
+ 1635,
7031
+ 1385,
7032
+ 1120,
7033
+ 3232,
7034
+ 3497,
7035
+ 3747,
7036
+ 4010,
7037
+ 2214,
7038
+ 2479,
7039
+ 2725,
7040
+ 2988,
7041
+ 1196,
7042
+ 1445,
7043
+ 1711,
7044
+ 1958,
7045
+ 170,
7046
+ 419,
7047
+ 681,
7048
+ 928,
7049
+ 3376,
7050
+ 3129,
7051
+ 3891,
7052
+ 3642,
7053
+ 2358,
7054
+ 2111,
7055
+ 2869,
7056
+ 2620,
7057
+ 1340,
7058
+ 1077,
7059
+ 1855,
7060
+ 1590,
7061
+ 314,
7062
+ 51,
7063
+ 825,
7064
+ 560,
7065
+ 3728,
7066
+ 3993,
7067
+ 3219,
7068
+ 3482,
7069
+ 2710,
7070
+ 2975,
7071
+ 2197,
7072
+ 2460,
7073
+ 1692,
7074
+ 1941,
7075
+ 1183,
7076
+ 1430,
7077
+ 666,
7078
+ 915,
7079
+ 153,
7080
+ 400,
7081
+ 3840,
7082
+ 3593,
7083
+ 3331,
7084
+ 3082,
7085
+ 2822,
7086
+ 2575,
7087
+ 2309,
7088
+ 2060,
7089
+ 1804,
7090
+ 1541,
7091
+ 1295,
7092
+ 1030,
7093
+ 778,
7094
+ 515,
7095
+ 265,
7096
+ 0
7097
+ ];
7098
+ var TRI_TABLE = [
7099
+ [],
7100
+ [0, 8, 3],
7101
+ [0, 1, 9],
7102
+ [1, 8, 3, 9, 8, 1],
7103
+ [1, 2, 10],
7104
+ [0, 8, 3, 1, 2, 10],
7105
+ [9, 2, 10, 0, 2, 9],
7106
+ [2, 8, 3, 2, 10, 8, 10, 9, 8],
7107
+ [3, 11, 2],
7108
+ [0, 11, 2, 8, 11, 0],
7109
+ [1, 9, 0, 2, 3, 11],
7110
+ [1, 11, 2, 1, 9, 11, 9, 8, 11],
7111
+ [3, 10, 1, 11, 10, 3],
7112
+ [0, 10, 1, 0, 8, 10, 8, 11, 10],
7113
+ [3, 9, 0, 3, 11, 9, 11, 10, 9],
7114
+ [9, 8, 10, 10, 8, 11],
7115
+ [4, 7, 8],
7116
+ [4, 3, 0, 7, 3, 4],
7117
+ [0, 1, 9, 8, 4, 7],
7118
+ [4, 1, 9, 4, 7, 1, 7, 3, 1],
7119
+ [1, 2, 10, 8, 4, 7],
7120
+ [3, 4, 7, 3, 0, 4, 1, 2, 10],
7121
+ [9, 2, 10, 9, 0, 2, 8, 4, 7],
7122
+ [2, 10, 9, 2, 9, 7, 2, 7, 3, 7, 9, 4],
7123
+ [8, 4, 7, 3, 11, 2],
7124
+ [11, 4, 7, 11, 2, 4, 2, 0, 4],
7125
+ [9, 0, 1, 8, 4, 7, 2, 3, 11],
7126
+ [4, 7, 11, 9, 4, 11, 9, 11, 2, 9, 2, 1],
7127
+ [3, 10, 1, 3, 11, 10, 7, 8, 4],
7128
+ [1, 11, 10, 1, 4, 11, 1, 0, 4, 7, 11, 4],
7129
+ [4, 7, 8, 9, 0, 11, 9, 11, 10, 11, 0, 3],
7130
+ [4, 7, 11, 4, 11, 9, 9, 11, 10],
7131
+ [9, 5, 4],
7132
+ [9, 5, 4, 0, 8, 3],
7133
+ [0, 5, 4, 1, 5, 0],
7134
+ [8, 5, 4, 8, 3, 5, 3, 1, 5],
7135
+ [1, 2, 10, 9, 5, 4],
7136
+ [3, 0, 8, 1, 2, 10, 4, 9, 5],
7137
+ [5, 2, 10, 5, 4, 2, 4, 0, 2],
7138
+ [2, 10, 5, 3, 2, 5, 3, 5, 4, 3, 4, 8],
7139
+ [9, 5, 4, 2, 3, 11],
7140
+ [0, 11, 2, 0, 8, 11, 4, 9, 5],
7141
+ [0, 5, 4, 0, 1, 5, 2, 3, 11],
7142
+ [2, 1, 5, 2, 5, 8, 2, 8, 11, 4, 8, 5],
7143
+ [10, 3, 11, 10, 1, 3, 9, 5, 4],
7144
+ [4, 9, 5, 0, 8, 1, 8, 10, 1, 8, 11, 10],
7145
+ [5, 4, 0, 5, 0, 11, 5, 11, 10, 11, 0, 3],
7146
+ [5, 4, 8, 5, 8, 10, 10, 8, 11],
7147
+ [9, 7, 8, 5, 7, 9],
7148
+ [9, 3, 0, 9, 5, 3, 5, 7, 3],
7149
+ [0, 7, 8, 0, 1, 7, 1, 5, 7],
7150
+ [1, 5, 3, 3, 5, 7],
7151
+ [9, 7, 8, 9, 5, 7, 10, 1, 2],
7152
+ [10, 1, 2, 9, 5, 0, 5, 3, 0, 5, 7, 3],
7153
+ [8, 0, 2, 8, 2, 5, 8, 5, 7, 10, 5, 2],
7154
+ [2, 10, 5, 2, 5, 3, 3, 5, 7],
7155
+ [7, 9, 5, 7, 8, 9, 3, 11, 2],
7156
+ [9, 5, 7, 9, 7, 2, 9, 2, 0, 2, 7, 11],
7157
+ [2, 3, 11, 0, 1, 8, 1, 7, 8, 1, 5, 7],
7158
+ [11, 2, 1, 11, 1, 7, 7, 1, 5],
7159
+ [9, 5, 8, 8, 5, 7, 10, 1, 3, 10, 3, 11],
7160
+ [5, 7, 0, 5, 0, 9, 7, 11, 0, 1, 0, 10, 11, 10, 0],
7161
+ [11, 10, 0, 11, 0, 3, 10, 5, 0, 8, 0, 7, 5, 7, 0],
7162
+ [11, 10, 5, 7, 11, 5],
7163
+ [10, 6, 5],
7164
+ [0, 8, 3, 5, 10, 6],
7165
+ [9, 0, 1, 5, 10, 6],
7166
+ [1, 8, 3, 1, 9, 8, 5, 10, 6],
7167
+ [1, 6, 5, 2, 6, 1],
7168
+ [1, 6, 5, 1, 2, 6, 3, 0, 8],
7169
+ [9, 6, 5, 9, 0, 6, 0, 2, 6],
7170
+ [5, 9, 8, 5, 8, 2, 5, 2, 6, 3, 2, 8],
7171
+ [2, 3, 11, 10, 6, 5],
7172
+ [11, 0, 8, 11, 2, 0, 10, 6, 5],
7173
+ [0, 1, 9, 2, 3, 11, 5, 10, 6],
7174
+ [5, 10, 6, 1, 9, 2, 9, 11, 2, 9, 8, 11],
7175
+ [6, 3, 11, 6, 5, 3, 5, 1, 3],
7176
+ [0, 8, 11, 0, 11, 5, 0, 5, 1, 5, 11, 6],
7177
+ [3, 11, 6, 0, 3, 6, 0, 6, 5, 0, 5, 9],
7178
+ [6, 5, 9, 6, 9, 11, 11, 9, 8],
7179
+ [5, 10, 6, 4, 7, 8],
7180
+ [4, 3, 0, 4, 7, 3, 6, 5, 10],
7181
+ [1, 9, 0, 5, 10, 6, 8, 4, 7],
7182
+ [10, 6, 5, 1, 9, 7, 1, 7, 3, 7, 9, 4],
7183
+ [6, 1, 2, 6, 5, 1, 4, 7, 8],
7184
+ [1, 2, 5, 5, 2, 6, 3, 0, 4, 3, 4, 7],
7185
+ [8, 4, 7, 9, 0, 5, 0, 6, 5, 0, 2, 6],
7186
+ [7, 3, 9, 7, 9, 4, 3, 2, 9, 5, 9, 6, 2, 6, 9],
7187
+ [3, 11, 2, 7, 8, 4, 10, 6, 5],
7188
+ [5, 10, 6, 4, 7, 2, 4, 2, 0, 2, 7, 11],
7189
+ [0, 1, 9, 4, 7, 8, 2, 3, 11, 5, 10, 6],
7190
+ [9, 2, 1, 9, 11, 2, 9, 4, 11, 7, 11, 4, 5, 10, 6],
7191
+ [8, 4, 7, 3, 11, 5, 3, 5, 1, 5, 11, 6],
7192
+ [5, 1, 11, 5, 11, 6, 1, 0, 11, 7, 11, 4, 0, 4, 11],
7193
+ [0, 5, 9, 0, 6, 5, 0, 3, 6, 11, 6, 3, 8, 4, 7],
7194
+ [6, 5, 9, 6, 9, 11, 4, 7, 9, 7, 11, 9],
7195
+ [10, 4, 9, 6, 4, 10],
7196
+ [4, 10, 6, 4, 9, 10, 0, 8, 3],
7197
+ [10, 0, 1, 10, 6, 0, 6, 4, 0],
7198
+ [8, 3, 1, 8, 1, 6, 8, 6, 4, 6, 1, 10],
7199
+ [1, 4, 9, 1, 2, 4, 2, 6, 4],
7200
+ [3, 0, 8, 1, 2, 9, 2, 4, 9, 2, 6, 4],
7201
+ [0, 2, 4, 4, 2, 6],
7202
+ [8, 3, 2, 8, 2, 4, 4, 2, 6],
7203
+ [10, 4, 9, 10, 6, 4, 11, 2, 3],
7204
+ [0, 8, 2, 2, 8, 11, 4, 9, 10, 4, 10, 6],
7205
+ [3, 11, 2, 0, 1, 6, 0, 6, 4, 6, 1, 10],
7206
+ [6, 4, 1, 6, 1, 10, 4, 8, 1, 2, 1, 11, 8, 11, 1],
7207
+ [9, 6, 4, 9, 3, 6, 9, 1, 3, 11, 6, 3],
7208
+ [8, 11, 1, 8, 1, 0, 11, 6, 1, 9, 1, 4, 6, 4, 1],
7209
+ [3, 11, 6, 3, 6, 0, 0, 6, 4],
7210
+ [6, 4, 8, 11, 6, 8],
7211
+ [7, 10, 6, 7, 8, 10, 8, 9, 10],
7212
+ [0, 7, 3, 0, 10, 7, 0, 9, 10, 6, 7, 10],
7213
+ [10, 6, 7, 1, 10, 7, 1, 7, 8, 1, 8, 0],
7214
+ [10, 6, 7, 10, 7, 1, 1, 7, 3],
7215
+ [1, 2, 6, 1, 6, 8, 1, 8, 9, 8, 6, 7],
7216
+ [2, 6, 9, 2, 9, 1, 6, 7, 9, 0, 9, 3, 7, 3, 9],
7217
+ [7, 8, 0, 7, 0, 6, 6, 0, 2],
7218
+ [7, 3, 2, 6, 7, 2],
7219
+ [2, 3, 11, 10, 6, 8, 10, 8, 9, 8, 6, 7],
7220
+ [2, 0, 7, 2, 7, 11, 0, 9, 7, 6, 7, 10, 9, 10, 7],
7221
+ [1, 8, 0, 1, 7, 8, 1, 10, 7, 6, 7, 10, 2, 3, 11],
7222
+ [11, 2, 1, 11, 1, 7, 10, 6, 1, 6, 7, 1],
7223
+ [8, 9, 6, 8, 6, 7, 9, 1, 6, 11, 6, 3, 1, 3, 6],
7224
+ [0, 9, 1, 11, 6, 7],
7225
+ [7, 8, 0, 7, 0, 6, 3, 11, 0, 11, 6, 0],
7226
+ [7, 11, 6],
7227
+ [7, 6, 11],
7228
+ [3, 0, 8, 11, 7, 6],
7229
+ [0, 1, 9, 11, 7, 6],
7230
+ [8, 1, 9, 8, 3, 1, 11, 7, 6],
7231
+ [10, 1, 2, 6, 11, 7],
7232
+ [1, 2, 10, 3, 0, 8, 6, 11, 7],
7233
+ [2, 9, 0, 2, 10, 9, 6, 11, 7],
7234
+ [6, 11, 7, 2, 10, 3, 10, 8, 3, 10, 9, 8],
7235
+ [7, 2, 3, 6, 2, 7],
7236
+ [7, 0, 8, 7, 6, 0, 6, 2, 0],
7237
+ [2, 7, 6, 2, 3, 7, 0, 1, 9],
7238
+ [1, 6, 2, 1, 8, 6, 1, 9, 8, 8, 7, 6],
7239
+ [10, 7, 6, 10, 1, 7, 1, 3, 7],
7240
+ [10, 7, 6, 1, 7, 10, 1, 8, 7, 1, 0, 8],
7241
+ [0, 3, 7, 0, 7, 10, 0, 10, 9, 6, 10, 7],
7242
+ [7, 6, 10, 7, 10, 8, 8, 10, 9],
7243
+ [6, 8, 4, 11, 8, 6],
7244
+ [3, 6, 11, 3, 0, 6, 0, 4, 6],
7245
+ [8, 6, 11, 8, 4, 6, 9, 0, 1],
7246
+ [9, 4, 6, 9, 6, 3, 9, 3, 1, 11, 3, 6],
7247
+ [6, 8, 4, 6, 11, 8, 2, 10, 1],
7248
+ [1, 2, 10, 3, 0, 11, 0, 6, 11, 0, 4, 6],
7249
+ [4, 11, 8, 4, 6, 11, 0, 2, 9, 2, 10, 9],
7250
+ [10, 9, 3, 10, 3, 2, 9, 4, 3, 11, 3, 6, 4, 6, 3],
7251
+ [8, 2, 3, 8, 4, 2, 4, 6, 2],
7252
+ [0, 4, 2, 4, 6, 2],
7253
+ [1, 9, 0, 2, 3, 4, 2, 4, 6, 4, 3, 8],
7254
+ [1, 9, 4, 1, 4, 2, 2, 4, 6],
7255
+ [8, 1, 3, 8, 6, 1, 8, 4, 6, 6, 10, 1],
7256
+ [10, 1, 0, 10, 0, 6, 6, 0, 4],
7257
+ [4, 6, 3, 4, 3, 8, 6, 10, 3, 0, 3, 9, 10, 9, 3],
7258
+ [10, 9, 4, 6, 10, 4],
7259
+ [4, 9, 5, 7, 6, 11],
7260
+ [0, 8, 3, 4, 9, 5, 11, 7, 6],
7261
+ [5, 0, 1, 5, 4, 0, 7, 6, 11],
7262
+ [11, 7, 6, 8, 3, 4, 3, 5, 4, 3, 1, 5],
7263
+ [9, 5, 4, 10, 1, 2, 7, 6, 11],
7264
+ [6, 11, 7, 1, 2, 10, 0, 8, 3, 4, 9, 5],
7265
+ [7, 6, 11, 5, 4, 10, 4, 2, 10, 4, 0, 2],
7266
+ [3, 4, 8, 3, 5, 4, 3, 2, 5, 10, 5, 2, 11, 7, 6],
7267
+ [7, 2, 3, 7, 6, 2, 5, 4, 9],
7268
+ [9, 5, 4, 0, 8, 6, 0, 6, 2, 6, 8, 7],
7269
+ [3, 6, 2, 3, 7, 6, 1, 5, 0, 5, 4, 0],
7270
+ [6, 2, 8, 6, 8, 7, 2, 1, 8, 4, 8, 5, 1, 5, 8],
7271
+ [9, 5, 4, 10, 1, 6, 1, 7, 6, 1, 3, 7],
7272
+ [1, 6, 10, 1, 7, 6, 1, 0, 7, 8, 7, 0, 9, 5, 4],
7273
+ [4, 0, 10, 4, 10, 5, 0, 3, 10, 6, 10, 7, 3, 7, 10],
7274
+ [7, 6, 10, 7, 10, 8, 5, 4, 10, 4, 8, 10],
7275
+ [6, 9, 5, 6, 11, 9, 11, 8, 9],
7276
+ [3, 6, 11, 0, 6, 3, 0, 5, 6, 0, 9, 5],
7277
+ [0, 11, 8, 0, 5, 11, 0, 1, 5, 5, 6, 11],
7278
+ [6, 11, 3, 6, 3, 5, 5, 3, 1],
7279
+ [1, 2, 10, 9, 5, 11, 9, 11, 8, 11, 5, 6],
7280
+ [0, 11, 3, 0, 6, 11, 0, 9, 6, 5, 6, 9, 1, 2, 10],
7281
+ [11, 8, 5, 11, 5, 6, 8, 0, 5, 10, 5, 2, 0, 2, 5],
7282
+ [6, 11, 3, 6, 3, 5, 2, 10, 3, 10, 5, 3],
7283
+ [5, 8, 9, 5, 2, 8, 5, 6, 2, 3, 8, 2],
7284
+ [9, 5, 6, 9, 6, 0, 0, 6, 2],
7285
+ [1, 5, 8, 1, 8, 0, 5, 6, 8, 3, 8, 2, 6, 2, 8],
7286
+ [1, 5, 6, 2, 1, 6],
7287
+ [1, 3, 6, 1, 6, 10, 3, 8, 6, 5, 6, 9, 8, 9, 6],
7288
+ [10, 1, 0, 10, 0, 6, 9, 5, 0, 5, 6, 0],
7289
+ [0, 3, 8, 5, 6, 10],
7290
+ [10, 5, 6],
7291
+ [11, 5, 10, 7, 5, 11],
7292
+ [11, 5, 10, 11, 7, 5, 8, 3, 0],
7293
+ [5, 11, 7, 5, 10, 11, 1, 9, 0],
7294
+ [10, 7, 5, 10, 11, 7, 9, 8, 1, 8, 3, 1],
7295
+ [11, 1, 2, 11, 7, 1, 7, 5, 1],
7296
+ [0, 8, 3, 1, 2, 7, 1, 7, 5, 7, 2, 11],
7297
+ [9, 7, 5, 9, 2, 7, 9, 0, 2, 2, 11, 7],
7298
+ [7, 5, 2, 7, 2, 11, 5, 9, 2, 3, 2, 8, 9, 8, 2],
7299
+ [2, 5, 10, 2, 3, 5, 3, 7, 5],
7300
+ [8, 2, 0, 8, 5, 2, 8, 7, 5, 10, 2, 5],
7301
+ [9, 0, 1, 5, 10, 3, 5, 3, 7, 3, 10, 2],
7302
+ [9, 8, 2, 9, 2, 1, 8, 7, 2, 10, 2, 5, 7, 5, 2],
7303
+ [1, 3, 5, 3, 7, 5],
7304
+ [0, 8, 7, 0, 7, 1, 1, 7, 5],
7305
+ [9, 0, 3, 9, 3, 5, 5, 3, 7],
7306
+ [9, 8, 7, 5, 9, 7],
7307
+ [5, 8, 4, 5, 10, 8, 10, 11, 8],
7308
+ [5, 0, 4, 5, 11, 0, 5, 10, 11, 11, 3, 0],
7309
+ [0, 1, 9, 8, 4, 10, 8, 10, 11, 10, 4, 5],
7310
+ [10, 11, 4, 10, 4, 5, 11, 3, 4, 9, 4, 1, 3, 1, 4],
7311
+ [2, 5, 1, 2, 8, 5, 2, 11, 8, 4, 5, 8],
7312
+ [0, 4, 11, 0, 11, 3, 4, 5, 11, 2, 11, 1, 5, 1, 11],
7313
+ [0, 2, 5, 0, 5, 9, 2, 11, 5, 4, 5, 8, 11, 8, 5],
7314
+ [9, 4, 5, 2, 11, 3],
7315
+ [2, 5, 10, 3, 5, 2, 3, 4, 5, 3, 8, 4],
7316
+ [5, 10, 2, 5, 2, 4, 4, 2, 0],
7317
+ [3, 10, 2, 3, 5, 10, 3, 8, 5, 4, 5, 8, 0, 1, 9],
7318
+ [5, 10, 2, 5, 2, 4, 1, 9, 2, 9, 4, 2],
7319
+ [8, 4, 5, 8, 5, 3, 3, 5, 1],
7320
+ [0, 4, 5, 1, 0, 5],
7321
+ [8, 4, 5, 8, 5, 3, 9, 0, 5, 0, 3, 5],
7322
+ [9, 4, 5],
7323
+ [4, 11, 7, 4, 9, 11, 9, 10, 11],
7324
+ [0, 8, 3, 4, 9, 7, 9, 11, 7, 9, 10, 11],
7325
+ [1, 10, 11, 1, 11, 4, 1, 4, 0, 7, 4, 11],
7326
+ [3, 1, 4, 3, 4, 8, 1, 10, 4, 7, 4, 11, 10, 11, 4],
7327
+ [4, 11, 7, 9, 11, 4, 9, 2, 11, 9, 1, 2],
7328
+ [9, 7, 4, 9, 11, 7, 9, 1, 11, 2, 11, 1, 0, 8, 3],
7329
+ [11, 7, 4, 11, 4, 2, 2, 4, 0],
7330
+ [11, 7, 4, 11, 4, 2, 8, 3, 4, 3, 2, 4],
7331
+ [2, 9, 10, 2, 7, 9, 2, 3, 7, 7, 4, 9],
7332
+ [9, 10, 7, 9, 7, 4, 10, 2, 7, 8, 7, 0, 2, 0, 7],
7333
+ [3, 7, 10, 3, 10, 2, 7, 4, 10, 1, 10, 0, 4, 0, 10],
7334
+ [1, 10, 2, 8, 7, 4],
7335
+ [4, 9, 1, 4, 1, 7, 7, 1, 3],
7336
+ [4, 9, 1, 4, 1, 7, 0, 8, 1, 8, 7, 1],
7337
+ [4, 0, 3, 7, 4, 3],
7338
+ [4, 8, 7],
7339
+ [9, 10, 8, 10, 11, 8],
7340
+ [3, 0, 9, 3, 9, 11, 11, 9, 10],
7341
+ [0, 1, 10, 0, 10, 8, 8, 10, 11],
7342
+ [3, 1, 10, 11, 3, 10],
7343
+ [1, 2, 11, 1, 11, 9, 9, 11, 8],
7344
+ [3, 0, 9, 3, 9, 11, 1, 2, 9, 2, 11, 9],
7345
+ [0, 2, 11, 8, 0, 11],
7346
+ [3, 2, 11],
7347
+ [2, 3, 8, 2, 8, 10, 10, 8, 9],
7348
+ [9, 10, 2, 0, 9, 2],
7349
+ [2, 3, 8, 2, 8, 10, 0, 1, 8, 1, 10, 8],
7350
+ [1, 10, 2],
7351
+ [1, 3, 8, 9, 1, 8],
7352
+ [0, 9, 1],
7353
+ [0, 3, 8],
7354
+ []
7355
+ ];
7356
+ var EDGE_CORNERS = [
7357
+ [0, 1],
7358
+ [1, 2],
7359
+ [2, 3],
7360
+ [3, 0],
7361
+ [4, 5],
7362
+ [5, 6],
7363
+ [6, 7],
7364
+ [7, 4],
7365
+ [0, 4],
7366
+ [1, 5],
7367
+ [2, 6],
7368
+ [3, 7]
7369
+ ];
7370
+ var CORNER = [
7371
+ [0, 0, 0],
7372
+ [1, 0, 0],
7373
+ [1, 1, 0],
7374
+ [0, 1, 0],
7375
+ [0, 0, 1],
7376
+ [1, 0, 1],
7377
+ [1, 1, 1],
7378
+ [0, 1, 1]
7379
+ ];
7380
+ function marchingCubes(values, dims, isoLevel, extent) {
7381
+ const [nx, ny, nz] = dims;
7382
+ const ex = extent?.x ?? [0, nx - 1];
7383
+ const ez = extent?.z ?? [0, nz - 1];
7384
+ const ey = extent?.y ?? [0, ny - 1];
7385
+ const wx = (x) => ex[0] + x / Math.max(1, nx - 1) * (ex[1] - ex[0]);
7386
+ const wy = (y) => ey[0] + y / Math.max(1, ny - 1) * (ey[1] - ey[0]);
7387
+ const wz = (z) => ez[0] + z / Math.max(1, nz - 1) * (ez[1] - ez[0]);
7388
+ const idx = (x, y, z) => x + y * nx + z * nx * ny;
7389
+ const val = (x, y, z) => values[idx(x, y, z)];
7390
+ const grad = (x, y, z) => {
7391
+ const gx = val(Math.min(nx - 1, x + 1), y, z) - val(Math.max(0, x - 1), y, z);
7392
+ const gy = val(x, Math.min(ny - 1, y + 1), z) - val(x, Math.max(0, y - 1), z);
7393
+ const gz = val(x, y, Math.min(nz - 1, z + 1)) - val(x, y, Math.max(0, z - 1));
7394
+ return [gx, gy, gz];
7395
+ };
7396
+ const positions = [];
7397
+ const normals = [];
7398
+ const cornerVal = new Array(8);
7399
+ const edgeP = new Array(12);
7400
+ const edgeN = new Array(12);
7401
+ for (let z = 0; z < nz - 1; z++) {
7402
+ for (let y = 0; y < ny - 1; y++) {
7403
+ for (let x = 0; x < nx - 1; x++) {
7404
+ let cubeIndex = 0;
7405
+ for (let i = 0; i < 8; i++) {
7406
+ const co = CORNER[i];
7407
+ cornerVal[i] = val(x + co[0], y + co[1], z + co[2]);
7408
+ if (cornerVal[i] < isoLevel) cubeIndex |= 1 << i;
7409
+ }
7410
+ const edges = EDGE_TABLE[cubeIndex];
7411
+ if (edges === 0) continue;
7412
+ for (let e = 0; e < 12; e++) {
7413
+ if (!(edges & 1 << e)) continue;
7414
+ const [a, b] = EDGE_CORNERS[e];
7415
+ const va = cornerVal[a], vb = cornerVal[b];
7416
+ const t = Math.abs(vb - va) < 1e-9 ? 0.5 : (isoLevel - va) / (vb - va);
7417
+ const ca = CORNER[a], cb = CORNER[b];
7418
+ const px = x + ca[0] + (cb[0] - ca[0]) * t;
7419
+ const py = y + ca[1] + (cb[1] - ca[1]) * t;
7420
+ const pz = z + ca[2] + (cb[2] - ca[2]) * t;
7421
+ edgeP[e] = [wx(px), wy(py), wz(pz)];
7422
+ const ga = grad(x + ca[0], y + ca[1], z + ca[2]);
7423
+ const gb = grad(x + cb[0], y + cb[1], z + cb[2]);
7424
+ let nxn = -(ga[0] + (gb[0] - ga[0]) * t);
7425
+ let nyn = -(ga[1] + (gb[1] - ga[1]) * t);
7426
+ let nzn = -(ga[2] + (gb[2] - ga[2]) * t);
7427
+ const nl = Math.hypot(nxn, nyn, nzn) || 1;
7428
+ nxn /= nl;
7429
+ nyn /= nl;
7430
+ nzn /= nl;
7431
+ edgeN[e] = [nxn, nyn, nzn];
7432
+ }
7433
+ const tris = TRI_TABLE[cubeIndex];
7434
+ for (let i = 0; i < tris.length; i += 3) {
7435
+ for (let k = 0; k < 3; k++) {
7436
+ const p = edgeP[tris[i + k]];
7437
+ const nrm = edgeN[tris[i + k]];
7438
+ positions.push(p[0], p[1], p[2]);
7439
+ normals.push(nrm[0], nrm[1], nrm[2]);
7440
+ }
7441
+ }
7442
+ }
7443
+ }
7444
+ }
7445
+ return { positions: new Float32Array(positions), normals: new Float32Array(normals) };
7446
+ }
7447
+
7448
+ // src/plot3d/isosurface.ts
7449
+ var VERT12 = (
7450
+ /* glsl */
7451
+ `#version 300 es
7452
+ precision highp float;
7453
+ layout(location = 0) in vec3 aPos;
7454
+ layout(location = 1) in vec3 aNormal;
7455
+ uniform mat4 uMVP;
7456
+ out vec3 vN;
7457
+ void main() { vN = aNormal; gl_Position = uMVP * vec4(aPos, 1.0); }`
7458
+ );
7459
+ var FRAG14 = (
7460
+ /* glsl */
7461
+ `#version 300 es
7462
+ precision highp float;
7463
+ in vec3 vN;
7464
+ uniform vec3 uColor;
7465
+ uniform vec3 uLightDir;
7466
+ uniform float uAmbient;
7467
+ uniform float uOpacity;
7468
+ out vec4 outColor;
7469
+ void main() {
7470
+ // Two-sided: isosurfaces are viewed from inside and out.
7471
+ float d = abs(dot(normalize(vN), normalize(uLightDir)));
7472
+ float shade = uAmbient + (1.0 - uAmbient) * d;
7473
+ outColor = vec4(uColor * shade * uOpacity, uOpacity);
7474
+ }`
7475
+ );
7476
+ var programCache15 = /* @__PURE__ */ new WeakMap();
7477
+ function getProgram12(gl) {
7478
+ let p = programCache15.get(gl);
7479
+ if (!p) {
7480
+ p = createProgram(gl, VERT12, FRAG14);
7481
+ programCache15.set(gl, p);
7482
+ }
7483
+ return p;
7484
+ }
7485
+ var counter19 = 0;
7486
+ var IsosurfaceLayer = class {
7487
+ constructor(gl, opts) {
7488
+ this.lightDir = [0.5, 1, 0.35];
7489
+ this.ambient = 0.35;
7490
+ this.id = `isosurface-${counter19++}`;
7491
+ this.gl = gl;
7492
+ this.program = getProgram12(gl);
7493
+ this.name = opts.name;
7494
+ const ci = opts.color ?? "#38bdf8";
7495
+ this.color = Array.isArray(ci) ? ci : parseColor(ci);
7496
+ this.colorCss = typeof ci === "string" ? ci : toColorCss(this.color);
7497
+ this.opacity = opts.opacity ?? 1;
7498
+ const { positions, normals } = marchingCubes(opts.values, opts.dims, opts.isoLevel, opts.extent);
7499
+ this.vertCount = positions.length / 3;
7500
+ const ex = opts.extent?.x ?? [0, opts.dims[0] - 1];
7501
+ const ey = opts.extent?.y ?? [0, opts.dims[1] - 1];
7502
+ const ez = opts.extent?.z ?? [0, opts.dims[2] - 1];
7503
+ this.b3 = { x: ex, y: ey, z: ez };
7504
+ const data = new Float32Array(this.vertCount * 6);
7505
+ for (let i = 0; i < this.vertCount; i++) {
7506
+ data[i * 6] = positions[i * 3];
7507
+ data[i * 6 + 1] = positions[i * 3 + 1];
7508
+ data[i * 6 + 2] = positions[i * 3 + 2];
7509
+ data[i * 6 + 3] = normals[i * 3];
7510
+ data[i * 6 + 4] = normals[i * 3 + 1];
7511
+ data[i * 6 + 5] = normals[i * 3 + 2];
7512
+ }
7513
+ this.vao = gl.createVertexArray();
7514
+ this.buffer = gl.createBuffer();
7515
+ gl.bindVertexArray(this.vao);
7516
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
7517
+ gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
7518
+ gl.enableVertexAttribArray(0);
7519
+ gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 24, 0);
7520
+ gl.enableVertexAttribArray(1);
7521
+ gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 12);
7522
+ gl.bindVertexArray(null);
7523
+ this.uniforms = uniformLocations(gl, this.program, ["uMVP", "uColor", "uLightDir", "uAmbient", "uOpacity"]);
7524
+ }
7525
+ bounds3() {
7526
+ return this.vertCount ? this.b3 : null;
7527
+ }
7528
+ /** Set the light direction (world space) and ambient term (0..1). */
7529
+ setLight(dir, ambient) {
7530
+ this.lightDir = dir;
7531
+ this.ambient = ambient;
7532
+ }
7533
+ draw(gl, mvp) {
7534
+ if (this.vertCount === 0) return;
7535
+ gl.useProgram(this.program);
7536
+ gl.uniformMatrix4fv(this.uniforms.uMVP, false, mvp);
7537
+ gl.uniform3f(this.uniforms.uColor, this.color[0], this.color[1], this.color[2]);
7538
+ gl.uniform3f(this.uniforms.uLightDir, this.lightDir[0], this.lightDir[1], this.lightDir[2]);
7539
+ gl.uniform1f(this.uniforms.uAmbient, this.ambient);
7540
+ gl.uniform1f(this.uniforms.uOpacity, this.opacity);
7541
+ gl.bindVertexArray(this.vao);
7542
+ gl.drawArrays(gl.TRIANGLES, 0, this.vertCount);
7543
+ gl.bindVertexArray(null);
7544
+ }
7545
+ dispose() {
7546
+ this.gl.deleteVertexArray(this.vao);
7547
+ this.gl.deleteBuffer(this.buffer);
7548
+ }
7549
+ };
7550
+
7551
+ // src/plot3d/line3d.ts
7552
+ var VERT13 = (
7553
+ /* glsl */
7554
+ `#version 300 es
7555
+ precision highp float;
7556
+ layout(location = 0) in vec3 aPos;
7557
+ uniform mat4 uMVP;
7558
+ void main() { gl_Position = uMVP * vec4(aPos, 1.0); }`
7559
+ );
7560
+ var FRAG15 = (
7561
+ /* glsl */
7562
+ `#version 300 es
7563
+ precision highp float;
7564
+ uniform vec4 uColor;
7565
+ out vec4 outColor;
7566
+ void main() { outColor = uColor; }`
7567
+ );
7568
+ var programCache16 = /* @__PURE__ */ new WeakMap();
7569
+ function getProgram13(gl) {
7570
+ let p = programCache16.get(gl);
7571
+ if (!p) {
7572
+ p = createProgram(gl, VERT13, FRAG15);
7573
+ programCache16.set(gl, p);
7574
+ }
7575
+ return p;
7576
+ }
7577
+ var counter20 = 0;
7578
+ var Line3DLayer = class {
7579
+ constructor(gl, opts) {
7580
+ this.id = `line3d-${counter20++}`;
7581
+ this.gl = gl;
7582
+ this.program = getProgram13(gl);
7583
+ this.name = opts.name;
7584
+ const ci = opts.color ?? "#38bdf8";
7585
+ this.color = Array.isArray(ci) ? ci : parseColor(ci);
7586
+ this.colorCss = typeof ci === "string" ? ci : toColorCss(this.color);
7587
+ const n = Math.min(opts.x.length, opts.y.length, opts.z.length);
7588
+ this.count = n;
7589
+ const data = new Float32Array(n * 3);
7590
+ this.positions = data;
7591
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity, minZ = Infinity, maxZ = -Infinity;
7592
+ for (let i = 0; i < n; i++) {
7593
+ const x = opts.x[i], y = opts.y[i], z = opts.z[i];
7594
+ data[i * 3] = x;
7595
+ data[i * 3 + 1] = y;
7596
+ data[i * 3 + 2] = z;
5150
7597
  if (x < minX) minX = x;
5151
7598
  if (x > maxX) maxX = x;
5152
7599
  if (y < minY) minY = y;
@@ -5155,30 +7602,425 @@ var PointCloudLayer = class {
5155
7602
  if (z > maxZ) maxZ = z;
5156
7603
  }
5157
7604
  this.b3 = { x: [minX, maxX], y: [minY, maxY], z: [minZ, maxZ] };
7605
+ this.vao = gl.createVertexArray();
7606
+ this.buffer = gl.createBuffer();
7607
+ gl.bindVertexArray(this.vao);
7608
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
7609
+ gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
7610
+ gl.enableVertexAttribArray(0);
7611
+ gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
7612
+ gl.bindVertexArray(null);
7613
+ this.uniforms = uniformLocations(gl, this.program, ["uMVP", "uColor"]);
7614
+ }
7615
+ bounds3() {
7616
+ return this.count ? this.b3 : null;
7617
+ }
7618
+ pickData() {
7619
+ return this.count ? { positions: this.positions } : null;
7620
+ }
7621
+ /** Stream a new path. Call `plot.refresh()` afterwards to re-fit + redraw. */
7622
+ setData(x, y, z) {
7623
+ const n = Math.min(x.length, y.length, z.length);
7624
+ this.count = n;
7625
+ const data = new Float32Array(n * 3);
7626
+ this.positions = data;
7627
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity, minZ = Infinity, maxZ = -Infinity;
7628
+ for (let i = 0; i < n; i++) {
7629
+ const px = x[i], py = y[i], pz = z[i];
7630
+ data[i * 3] = px;
7631
+ data[i * 3 + 1] = py;
7632
+ data[i * 3 + 2] = pz;
7633
+ if (px < minX) minX = px;
7634
+ if (px > maxX) maxX = px;
7635
+ if (py < minY) minY = py;
7636
+ if (py > maxY) maxY = py;
7637
+ if (pz < minZ) minZ = pz;
7638
+ if (pz > maxZ) maxZ = pz;
7639
+ }
7640
+ this.b3 = { x: [minX, maxX], y: [minY, maxY], z: [minZ, maxZ] };
7641
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
7642
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, data, this.gl.DYNAMIC_DRAW);
7643
+ }
7644
+ draw(gl, mvp) {
7645
+ if (this.count < 2) return;
7646
+ gl.useProgram(this.program);
7647
+ gl.uniformMatrix4fv(this.uniforms.uMVP, false, mvp);
7648
+ gl.uniform4f(this.uniforms.uColor, this.color[0], this.color[1], this.color[2], this.color[3]);
7649
+ gl.bindVertexArray(this.vao);
7650
+ gl.drawArrays(gl.LINE_STRIP, 0, this.count);
7651
+ gl.bindVertexArray(null);
7652
+ }
7653
+ dispose() {
7654
+ this.gl.deleteVertexArray(this.vao);
7655
+ this.gl.deleteBuffer(this.buffer);
7656
+ }
7657
+ };
7658
+
7659
+ // src/plot3d/mat4.ts
7660
+ function identity() {
7661
+ const m = new Float32Array(16);
7662
+ m[0] = m[5] = m[10] = m[15] = 1;
7663
+ return m;
7664
+ }
7665
+ function multiply(a, b) {
7666
+ const o = new Float32Array(16);
7667
+ for (let c = 0; c < 4; c++) {
7668
+ for (let r = 0; r < 4; r++) {
7669
+ let s = 0;
7670
+ for (let k = 0; k < 4; k++) s += a[k * 4 + r] * b[c * 4 + k];
7671
+ o[c * 4 + r] = s;
7672
+ }
7673
+ }
7674
+ return o;
7675
+ }
7676
+ function perspective(fovy, aspect, near, far) {
7677
+ const f = 1 / Math.tan(fovy / 2);
7678
+ const nf = 1 / (near - far);
7679
+ const m = new Float32Array(16);
7680
+ m[0] = f / aspect;
7681
+ m[5] = f;
7682
+ m[10] = (far + near) * nf;
7683
+ m[11] = -1;
7684
+ m[14] = 2 * far * near * nf;
7685
+ return m;
7686
+ }
7687
+ var sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
7688
+ var cross = (a, b) => [
7689
+ a[1] * b[2] - a[2] * b[1],
7690
+ a[2] * b[0] - a[0] * b[2],
7691
+ a[0] * b[1] - a[1] * b[0]
7692
+ ];
7693
+ function normalize2(v) {
7694
+ const l = Math.hypot(v[0], v[1], v[2]) || 1;
7695
+ return [v[0] / l, v[1] / l, v[2] / l];
7696
+ }
7697
+ function lookAt(eye, center, up) {
7698
+ const z = normalize2(sub(eye, center));
7699
+ const x = normalize2(cross(up, z));
7700
+ const y = cross(z, x);
7701
+ const m = identity();
7702
+ m[0] = x[0];
7703
+ m[4] = x[1];
7704
+ m[8] = x[2];
7705
+ m[1] = y[0];
7706
+ m[5] = y[1];
7707
+ m[9] = y[2];
7708
+ m[2] = z[0];
7709
+ m[6] = z[1];
7710
+ m[10] = z[2];
7711
+ m[12] = -(x[0] * eye[0] + x[1] * eye[1] + x[2] * eye[2]);
7712
+ m[13] = -(y[0] * eye[0] + y[1] * eye[1] + y[2] * eye[2]);
7713
+ m[14] = -(z[0] * eye[0] + z[1] * eye[1] + z[2] * eye[2]);
7714
+ return m;
7715
+ }
7716
+ function transformPoint(m, x, y, z) {
7717
+ return [
7718
+ m[0] * x + m[4] * y + m[8] * z + m[12],
7719
+ m[1] * x + m[5] * y + m[9] * z + m[13],
7720
+ m[2] * x + m[6] * y + m[10] * z + m[14],
7721
+ m[3] * x + m[7] * y + m[11] * z + m[15]
7722
+ ];
7723
+ }
7724
+ function scaleTranslate(s, t) {
7725
+ const m = identity();
7726
+ m[0] = s[0];
7727
+ m[5] = s[1];
7728
+ m[10] = s[2];
7729
+ m[12] = t[0];
7730
+ m[13] = t[1];
7731
+ m[14] = t[2];
7732
+ return m;
7733
+ }
7734
+
7735
+ // src/plot3d/pointcloud.ts
7736
+ var VERT14 = (
7737
+ /* glsl */
7738
+ `#version 300 es
7739
+ precision highp float;
7740
+ layout(location = 0) in vec3 aPos;
7741
+ layout(location = 1) in vec3 aColor;
7742
+ layout(location = 2) in float aSize;
7743
+ uniform mat4 uMVP;
7744
+ uniform float uSize;
7745
+ out vec3 vColor;
7746
+ void main() {
7747
+ vColor = aColor;
7748
+ gl_Position = uMVP * vec4(aPos, 1.0);
7749
+ gl_PointSize = aSize > 0.0 ? aSize : uSize;
7750
+ }`
7751
+ );
7752
+ var FRAG16 = (
7753
+ /* glsl */
7754
+ `#version 300 es
7755
+ precision highp float;
7756
+ in vec3 vColor;
7757
+ out vec4 outColor;
7758
+ void main() {
7759
+ vec2 d = gl_PointCoord - 0.5;
7760
+ if (length(d) > 0.5) discard;
7761
+ outColor = vec4(vColor, 1.0);
7762
+ }`
7763
+ );
7764
+ var programCache17 = /* @__PURE__ */ new WeakMap();
7765
+ function getProgram14(gl) {
7766
+ let p = programCache17.get(gl);
7767
+ if (!p) {
7768
+ p = createProgram(gl, VERT14, FRAG16);
7769
+ programCache17.set(gl, p);
7770
+ }
7771
+ return p;
7772
+ }
7773
+ var counter21 = 0;
7774
+ var PointCloudLayer = class {
7775
+ constructor(gl, opts) {
7776
+ this.b3 = { x: [0, 0], y: [0, 0], z: [0, 0] };
7777
+ this.cInfo = null;
7778
+ this.id = `points3d-${counter21++}`;
7779
+ this.gl = gl;
7780
+ this.program = getProgram14(gl);
7781
+ this.size = opts.size ?? 4;
7782
+ this.name = opts.name;
7783
+ this.labels = opts.labels;
7784
+ const n = Math.min(opts.x.length, opts.y.length, opts.z.length);
7785
+ this.count = n;
7786
+ const base = opts.color != null ? Array.isArray(opts.color) ? opts.color : parseColor(opts.color) : [0.4, 0.7, 1, 1];
7787
+ const lut = opts.colorBy ? colormapLUT(opts.colorBy.colormap ?? "viridis") : null;
7788
+ let lo = opts.colorBy?.domain?.[0] ?? Infinity;
7789
+ let hi = opts.colorBy?.domain?.[1] ?? -Infinity;
7790
+ if (opts.colorBy && !opts.colorBy.domain) {
7791
+ const v = opts.colorBy.values;
7792
+ for (let i = 0; i < n; i++) {
7793
+ const t = v[i];
7794
+ if (t < lo) lo = t;
7795
+ if (t > hi) hi = t;
7796
+ }
7797
+ }
7798
+ const span = hi - lo || 1;
7799
+ if (opts.colorBy) this.cInfo = { colormap: opts.colorBy.colormap ?? "viridis", domain: [lo, hi], label: opts.name };
7800
+ else this.colorCss = toColorCss(base);
7801
+ this.data = new Float32Array(n * 7);
7802
+ this.positions = new Float32Array(n * 3);
7803
+ for (let i = 0; i < n; i++) {
7804
+ const x = opts.x[i], y = opts.y[i], z = opts.z[i];
7805
+ this.data[i * 7] = x;
7806
+ this.data[i * 7 + 1] = y;
7807
+ this.data[i * 7 + 2] = z;
7808
+ this.positions[i * 3] = x;
7809
+ this.positions[i * 3 + 1] = y;
7810
+ this.positions[i * 3 + 2] = z;
7811
+ if (lut && opts.colorBy) {
7812
+ let t = (opts.colorBy.values[i] - lo) / span;
7813
+ t = t <= 0 ? 0 : t >= 1 ? 1 : t;
7814
+ const j = (t * 255 | 0) * 3;
7815
+ this.data[i * 7 + 3] = lut[j];
7816
+ this.data[i * 7 + 4] = lut[j + 1];
7817
+ this.data[i * 7 + 5] = lut[j + 2];
7818
+ } else {
7819
+ this.data[i * 7 + 3] = base[0];
7820
+ this.data[i * 7 + 4] = base[1];
7821
+ this.data[i * 7 + 5] = base[2];
7822
+ }
7823
+ this.data[i * 7 + 6] = opts.sizes ? opts.sizes[i] : 0;
7824
+ }
7825
+ this.updateBounds();
5158
7826
  const vao = gl.createVertexArray();
5159
7827
  const buffer = gl.createBuffer();
5160
7828
  this.vao = vao;
5161
7829
  this.buffer = buffer;
5162
7830
  gl.bindVertexArray(vao);
5163
7831
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
5164
- gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
7832
+ gl.bufferData(gl.ARRAY_BUFFER, this.data, gl.STATIC_DRAW);
7833
+ gl.enableVertexAttribArray(0);
7834
+ gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 28, 0);
7835
+ gl.enableVertexAttribArray(1);
7836
+ gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 28, 12);
7837
+ gl.enableVertexAttribArray(2);
7838
+ gl.vertexAttribPointer(2, 1, gl.FLOAT, false, 28, 24);
7839
+ gl.bindVertexArray(null);
7840
+ this.uniforms = uniformLocations(gl, this.program, ["uMVP", "uSize"]);
7841
+ }
7842
+ updateBounds() {
7843
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity, minZ = Infinity, maxZ = -Infinity;
7844
+ for (let i = 0; i < this.count; i++) {
7845
+ const x = this.positions[i * 3], y = this.positions[i * 3 + 1], z = this.positions[i * 3 + 2];
7846
+ if (x < minX) minX = x;
7847
+ if (x > maxX) maxX = x;
7848
+ if (y < minY) minY = y;
7849
+ if (y > maxY) maxY = y;
7850
+ if (z < minZ) minZ = z;
7851
+ if (z > maxZ) maxZ = z;
7852
+ }
7853
+ this.b3 = { x: [minX, maxX], y: [minY, maxY], z: [minZ, maxZ] };
7854
+ }
7855
+ /** Stream new point positions (same count keeps colors/sizes). Call `plot.refresh()` after. */
7856
+ setData(x, y, z) {
7857
+ const n = Math.min(x.length, y.length, z.length, this.count);
7858
+ for (let i = 0; i < n; i++) {
7859
+ const px = x[i], py = y[i], pz = z[i];
7860
+ this.data[i * 7] = px;
7861
+ this.data[i * 7 + 1] = py;
7862
+ this.data[i * 7 + 2] = pz;
7863
+ this.positions[i * 3] = px;
7864
+ this.positions[i * 3 + 1] = py;
7865
+ this.positions[i * 3 + 2] = pz;
7866
+ }
7867
+ this.updateBounds();
7868
+ this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.buffer);
7869
+ this.gl.bufferData(this.gl.ARRAY_BUFFER, this.data, this.gl.DYNAMIC_DRAW);
7870
+ }
7871
+ bounds3() {
7872
+ return this.count ? this.b3 : null;
7873
+ }
7874
+ colorInfo() {
7875
+ return this.cInfo;
7876
+ }
7877
+ pickData() {
7878
+ if (!this.count) return null;
7879
+ return { positions: this.positions, label: this.labels ? (i) => String(this.labels[i]) : void 0 };
7880
+ }
7881
+ draw(gl, mvp) {
7882
+ if (this.count === 0) return;
7883
+ gl.useProgram(this.program);
7884
+ gl.uniformMatrix4fv(this.uniforms.uMVP, false, mvp);
7885
+ gl.uniform1f(this.uniforms.uSize, this.size);
7886
+ gl.bindVertexArray(this.vao);
7887
+ gl.drawArrays(gl.POINTS, 0, this.count);
7888
+ gl.bindVertexArray(null);
7889
+ }
7890
+ dispose() {
7891
+ this.gl.deleteVertexArray(this.vao);
7892
+ this.gl.deleteBuffer(this.buffer);
7893
+ }
7894
+ };
7895
+
7896
+ // src/plot3d/quiver3d.ts
7897
+ var VERT15 = (
7898
+ /* glsl */
7899
+ `#version 300 es
7900
+ precision highp float;
7901
+ layout(location = 0) in vec3 aPos;
7902
+ layout(location = 1) in vec3 aColor;
7903
+ uniform mat4 uMVP;
7904
+ out vec3 vColor;
7905
+ void main() { vColor = aColor; gl_Position = uMVP * vec4(aPos, 1.0); }`
7906
+ );
7907
+ var FRAG17 = (
7908
+ /* glsl */
7909
+ `#version 300 es
7910
+ precision highp float;
7911
+ in vec3 vColor;
7912
+ out vec4 outColor;
7913
+ void main() { outColor = vec4(vColor, 1.0); }`
7914
+ );
7915
+ var programCache18 = /* @__PURE__ */ new WeakMap();
7916
+ function getProgram15(gl) {
7917
+ let p = programCache18.get(gl);
7918
+ if (!p) {
7919
+ p = createProgram(gl, VERT15, FRAG17);
7920
+ programCache18.set(gl, p);
7921
+ }
7922
+ return p;
7923
+ }
7924
+ function norm(x, y, z) {
7925
+ const l = Math.hypot(x, y, z) || 1;
7926
+ return [x / l, y / l, z / l];
7927
+ }
7928
+ function cross2(a, b) {
7929
+ return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
7930
+ }
7931
+ var counter22 = 0;
7932
+ var Quiver3DLayer = class {
7933
+ constructor(gl, opts) {
7934
+ this.cInfo = null;
7935
+ this.id = `quiver3d-${counter22++}`;
7936
+ this.gl = gl;
7937
+ this.program = getProgram15(gl);
7938
+ this.name = opts.name;
7939
+ const scale = opts.scale ?? 1;
7940
+ const headSize = opts.headSize ?? 0.28;
7941
+ const n = Math.min(opts.x.length, opts.y.length, opts.z.length, opts.u.length, opts.v.length, opts.w.length);
7942
+ const base = opts.color != null ? Array.isArray(opts.color) ? opts.color : parseColor(opts.color) : [0.5, 0.8, 1, 1];
7943
+ const cmap = opts.colorBy ? colormap(opts.colorBy.colormap ?? "viridis") : null;
7944
+ let lo = opts.colorBy?.domain?.[0] ?? Infinity;
7945
+ let hi = opts.colorBy?.domain?.[1] ?? -Infinity;
7946
+ const mag = (i) => Math.hypot(opts.u[i], opts.v[i], opts.w[i]);
7947
+ if (cmap && !opts.colorBy?.domain) {
7948
+ for (let i = 0; i < n; i++) {
7949
+ const m = mag(i);
7950
+ if (m < lo) lo = m;
7951
+ if (m > hi) hi = m;
7952
+ }
7953
+ }
7954
+ const span = hi - lo || 1;
7955
+ if (cmap) this.cInfo = { colormap: opts.colorBy.colormap ?? "viridis", domain: [lo, hi], label: opts.name };
7956
+ else this.colorCss = typeof opts.color === "string" ? opts.color : toColorCss(base);
7957
+ const data = [];
7958
+ this.positions = new Float32Array(n * 3);
7959
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity, minZ = Infinity, maxZ = -Infinity;
7960
+ const bump = (x, y, z) => {
7961
+ if (x < minX) minX = x;
7962
+ if (x > maxX) maxX = x;
7963
+ if (y < minY) minY = y;
7964
+ if (y > maxY) maxY = y;
7965
+ if (z < minZ) minZ = z;
7966
+ if (z > maxZ) maxZ = z;
7967
+ };
7968
+ for (let i = 0; i < n; i++) {
7969
+ const bx = opts.x[i], by = opts.y[i], bz = opts.z[i];
7970
+ const ux = opts.u[i] * scale, uy = opts.v[i] * scale, uz = opts.w[i] * scale;
7971
+ const tx = bx + ux, ty = by + uy, tz = bz + uz;
7972
+ this.positions[i * 3] = bx;
7973
+ this.positions[i * 3 + 1] = by;
7974
+ this.positions[i * 3 + 2] = bz;
7975
+ const c = cmap ? cmap((mag(i) - lo) / span) : [base[0], base[1], base[2]];
7976
+ const seg = (x0, y0, z0, x1, y1, z1) => {
7977
+ data.push(x0, y0, z0, c[0], c[1], c[2], x1, y1, z1, c[0], c[1], c[2]);
7978
+ };
7979
+ seg(bx, by, bz, tx, ty, tz);
7980
+ bump(bx, by, bz);
7981
+ bump(tx, ty, tz);
7982
+ const len = Math.hypot(ux, uy, uz);
7983
+ if (len > 1e-6) {
7984
+ const dir = norm(ux, uy, uz);
7985
+ const ref = Math.abs(dir[1]) > 0.9 ? [1, 0, 0] : [0, 1, 0];
7986
+ const p1 = norm(...cross2(dir, ref));
7987
+ const p2 = norm(...cross2(dir, p1));
7988
+ const hl = len * headSize, hw = hl * 0.5;
7989
+ const backX = tx - dir[0] * hl, backY = ty - dir[1] * hl, backZ = tz - dir[2] * hl;
7990
+ for (const p of [p1, p2, [-p1[0], -p1[1], -p1[2]], [-p2[0], -p2[1], -p2[2]]]) {
7991
+ seg(tx, ty, tz, backX + p[0] * hw, backY + p[1] * hw, backZ + p[2] * hw);
7992
+ }
7993
+ }
7994
+ }
7995
+ this.vertCount = data.length / 6;
7996
+ this.b3 = { x: [minX, maxX], y: [minY, maxY], z: [minZ, maxZ] };
7997
+ this.vao = gl.createVertexArray();
7998
+ this.buffer = gl.createBuffer();
7999
+ gl.bindVertexArray(this.vao);
8000
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
8001
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
5165
8002
  gl.enableVertexAttribArray(0);
5166
8003
  gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 24, 0);
5167
8004
  gl.enableVertexAttribArray(1);
5168
8005
  gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 12);
5169
8006
  gl.bindVertexArray(null);
5170
- this.uniforms = uniformLocations(gl, this.program, ["uMVP", "uSize"]);
8007
+ this.uniforms = uniformLocations(gl, this.program, ["uMVP"]);
5171
8008
  }
5172
8009
  bounds3() {
5173
- return this.count ? this.b3 : null;
8010
+ return this.vertCount ? this.b3 : null;
8011
+ }
8012
+ colorInfo() {
8013
+ return this.cInfo;
8014
+ }
8015
+ pickData() {
8016
+ return this.positions.length ? { positions: this.positions } : null;
5174
8017
  }
5175
8018
  draw(gl, mvp) {
5176
- if (this.count === 0) return;
8019
+ if (this.vertCount === 0) return;
5177
8020
  gl.useProgram(this.program);
5178
8021
  gl.uniformMatrix4fv(this.uniforms.uMVP, false, mvp);
5179
- gl.uniform1f(this.uniforms.uSize, this.size);
5180
8022
  gl.bindVertexArray(this.vao);
5181
- gl.drawArrays(gl.POINTS, 0, this.count);
8023
+ gl.drawArrays(gl.LINES, 0, this.vertCount);
5182
8024
  gl.bindVertexArray(null);
5183
8025
  }
5184
8026
  dispose() {
@@ -5188,7 +8030,7 @@ var PointCloudLayer = class {
5188
8030
  };
5189
8031
 
5190
8032
  // src/plot3d/surface.ts
5191
- var VERT10 = (
8033
+ var VERT16 = (
5192
8034
  /* glsl */
5193
8035
  `#version 300 es
5194
8036
  precision highp float;
@@ -5200,7 +8042,7 @@ out vec3 vColor;
5200
8042
  out vec3 vN;
5201
8043
  void main() { vColor = aColor; vN = aNormal; gl_Position = uMVP * vec4(aPos, 1.0); }`
5202
8044
  );
5203
- var FRAG12 = (
8045
+ var FRAG18 = (
5204
8046
  /* glsl */
5205
8047
  `#version 300 es
5206
8048
  precision highp float;
@@ -5215,23 +8057,54 @@ void main() {
5215
8057
  outColor = vec4(vColor * shade, 1.0);
5216
8058
  }`
5217
8059
  );
5218
- var programCache11 = /* @__PURE__ */ new WeakMap();
5219
- function getProgram10(gl) {
5220
- let p = programCache11.get(gl);
8060
+ var WF_VERT = (
8061
+ /* glsl */
8062
+ `#version 300 es
8063
+ precision highp float;
8064
+ layout(location = 0) in vec3 aPos;
8065
+ layout(location = 1) in vec3 aColor;
8066
+ uniform mat4 uMVP;
8067
+ out vec3 vColor;
8068
+ void main() { vColor = aColor; gl_Position = uMVP * vec4(aPos, 1.0); }`
8069
+ );
8070
+ var WF_FRAG = (
8071
+ /* glsl */
8072
+ `#version 300 es
8073
+ precision highp float;
8074
+ in vec3 vColor;
8075
+ out vec4 outColor;
8076
+ void main() { outColor = vec4(vColor, 1.0); }`
8077
+ );
8078
+ var programCache19 = /* @__PURE__ */ new WeakMap();
8079
+ function getProgram16(gl) {
8080
+ let p = programCache19.get(gl);
5221
8081
  if (!p) {
5222
- p = createProgram(gl, VERT10, FRAG12);
5223
- programCache11.set(gl, p);
8082
+ p = createProgram(gl, VERT16, FRAG18);
8083
+ programCache19.set(gl, p);
5224
8084
  }
5225
8085
  return p;
5226
8086
  }
5227
- var counter14 = 0;
8087
+ var wireCache = /* @__PURE__ */ new WeakMap();
8088
+ function getWireProgram(gl) {
8089
+ let p = wireCache.get(gl);
8090
+ if (!p) {
8091
+ p = createProgram(gl, WF_VERT, WF_FRAG);
8092
+ wireCache.set(gl, p);
8093
+ }
8094
+ return p;
8095
+ }
8096
+ var counter23 = 0;
5228
8097
  var SurfaceLayer = class {
5229
8098
  constructor(gl, opts) {
8099
+ this.vDomain = [0, 1];
5230
8100
  this.lightDir = [0.5, 1, 0.35];
5231
8101
  this.ambient = 0.35;
5232
- this.id = `surface-${counter14++}`;
8102
+ this.id = `surface-${counter23++}`;
5233
8103
  this.gl = gl;
5234
- this.program = getProgram10(gl);
8104
+ this.wireframe = opts.wireframe ?? false;
8105
+ this.program = this.wireframe ? getWireProgram(gl) : getProgram16(gl);
8106
+ this.name = opts.name;
8107
+ this.cmapName = opts.colormap ?? "viridis";
5235
8108
  const { cols, rows, values } = opts;
5236
8109
  const [x0, x1] = opts.extentX ?? [0, cols - 1];
5237
8110
  const [z0, z1] = opts.extentZ ?? [0, rows - 1];
@@ -5256,42 +8129,71 @@ var SurfaceLayer = class {
5256
8129
  return [-dzdx / nl, 1 / nl, -dzdz / nl];
5257
8130
  };
5258
8131
  const data = [];
5259
- const vert = (c, r) => {
5260
- const [nx, ny, nz] = normalAt(c, r);
5261
- const [cr, cg, cb] = cmap((at(c, r) - vmin) / span);
5262
- data.push(wx(c), at(c, r), wz(r), nx, ny, nz, cr, cg, cb);
5263
- };
5264
- for (let r = 0; r < rows - 1; r++) {
5265
- for (let c = 0; c < cols - 1; c++) {
5266
- vert(c, r);
5267
- vert(c + 1, r);
5268
- vert(c + 1, r + 1);
5269
- vert(c, r);
5270
- vert(c + 1, r + 1);
5271
- vert(c, r + 1);
5272
- }
5273
- }
5274
- this.vertexCount = data.length / 9;
5275
8132
  this.b3 = { x: [x0, x1], y: [vmin, vmax], z: [z0, z1] };
8133
+ this.vDomain = [vmin, vmax];
5276
8134
  const vao = gl.createVertexArray();
5277
8135
  const buffer = gl.createBuffer();
5278
8136
  this.vao = vao;
5279
8137
  this.buffer = buffer;
5280
8138
  gl.bindVertexArray(vao);
5281
8139
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
5282
- gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
5283
- gl.enableVertexAttribArray(0);
5284
- gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 36, 0);
5285
- gl.enableVertexAttribArray(1);
5286
- gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 36, 12);
5287
- gl.enableVertexAttribArray(2);
5288
- gl.vertexAttribPointer(2, 3, gl.FLOAT, false, 36, 24);
8140
+ if (this.wireframe) {
8141
+ const vertL = (c, r) => {
8142
+ const [cr, cg, cb] = cmap((at(c, r) - vmin) / span);
8143
+ data.push(wx(c), at(c, r), wz(r), cr, cg, cb);
8144
+ };
8145
+ for (let r = 0; r < rows; r++) {
8146
+ for (let c = 0; c < cols; c++) {
8147
+ if (c < cols - 1) {
8148
+ vertL(c, r);
8149
+ vertL(c + 1, r);
8150
+ }
8151
+ if (r < rows - 1) {
8152
+ vertL(c, r);
8153
+ vertL(c, r + 1);
8154
+ }
8155
+ }
8156
+ }
8157
+ this.vertexCount = data.length / 6;
8158
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
8159
+ gl.enableVertexAttribArray(0);
8160
+ gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 24, 0);
8161
+ gl.enableVertexAttribArray(1);
8162
+ gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 12);
8163
+ } else {
8164
+ const vert = (c, r) => {
8165
+ const [nx, ny, nz] = normalAt(c, r);
8166
+ const [cr, cg, cb] = cmap((at(c, r) - vmin) / span);
8167
+ data.push(wx(c), at(c, r), wz(r), nx, ny, nz, cr, cg, cb);
8168
+ };
8169
+ for (let r = 0; r < rows - 1; r++) {
8170
+ for (let c = 0; c < cols - 1; c++) {
8171
+ vert(c, r);
8172
+ vert(c + 1, r);
8173
+ vert(c + 1, r + 1);
8174
+ vert(c, r);
8175
+ vert(c + 1, r + 1);
8176
+ vert(c, r + 1);
8177
+ }
8178
+ }
8179
+ this.vertexCount = data.length / 9;
8180
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
8181
+ gl.enableVertexAttribArray(0);
8182
+ gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 36, 0);
8183
+ gl.enableVertexAttribArray(1);
8184
+ gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 36, 12);
8185
+ gl.enableVertexAttribArray(2);
8186
+ gl.vertexAttribPointer(2, 3, gl.FLOAT, false, 36, 24);
8187
+ }
5289
8188
  gl.bindVertexArray(null);
5290
8189
  this.uniforms = uniformLocations(gl, this.program, ["uMVP", "uLightDir", "uAmbient"]);
5291
8190
  }
5292
8191
  bounds3() {
5293
8192
  return this.b3;
5294
8193
  }
8194
+ colorInfo() {
8195
+ return { colormap: this.cmapName, domain: this.vDomain, label: this.name };
8196
+ }
5295
8197
  /** Set the light direction (world space) and ambient term (0..1). */
5296
8198
  setLight(dir, ambient) {
5297
8199
  this.lightDir = dir;
@@ -5300,15 +8202,221 @@ var SurfaceLayer = class {
5300
8202
  draw(gl, mvp) {
5301
8203
  gl.useProgram(this.program);
5302
8204
  gl.uniformMatrix4fv(this.uniforms.uMVP, false, mvp);
5303
- gl.uniform3f(this.uniforms.uLightDir, this.lightDir[0], this.lightDir[1], this.lightDir[2]);
5304
- gl.uniform1f(this.uniforms.uAmbient, this.ambient);
8205
+ if (this.wireframe) {
8206
+ gl.bindVertexArray(this.vao);
8207
+ gl.drawArrays(gl.LINES, 0, this.vertexCount);
8208
+ } else {
8209
+ gl.uniform3f(this.uniforms.uLightDir, this.lightDir[0], this.lightDir[1], this.lightDir[2]);
8210
+ gl.uniform1f(this.uniforms.uAmbient, this.ambient);
8211
+ gl.bindVertexArray(this.vao);
8212
+ gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
8213
+ }
8214
+ gl.bindVertexArray(null);
8215
+ }
8216
+ dispose() {
8217
+ this.gl.deleteVertexArray(this.vao);
8218
+ this.gl.deleteBuffer(this.buffer);
8219
+ }
8220
+ };
8221
+
8222
+ // src/plot3d/volume.ts
8223
+ var STEPS = 160;
8224
+ var VERT17 = (
8225
+ /* glsl */
8226
+ `#version 300 es
8227
+ precision highp float;
8228
+ layout(location = 0) in vec3 aPos; // world position of the box corner
8229
+ layout(location = 1) in vec3 aLocal; // [0,1]^3 texture coord of the corner
8230
+ uniform mat4 uMVP;
8231
+ out vec3 vLocal;
8232
+ void main() { vLocal = aLocal; gl_Position = uMVP * vec4(aPos, 1.0); }`
8233
+ );
8234
+ var FRAG19 = (
8235
+ /* glsl */
8236
+ `#version 300 es
8237
+ precision highp float;
8238
+ in vec3 vLocal;
8239
+ uniform highp sampler3D uVolume;
8240
+ uniform sampler2D uLUT;
8241
+ uniform vec3 uCamLocal;
8242
+ uniform float uDensity;
8243
+ out vec4 outColor;
8244
+
8245
+ void main() {
8246
+ vec3 dir = normalize(vLocal - uCamLocal);
8247
+ vec3 invDir = 1.0 / dir;
8248
+ vec3 t0 = (vec3(0.0) - uCamLocal) * invDir;
8249
+ vec3 t1 = (vec3(1.0) - uCamLocal) * invDir;
8250
+ vec3 tmin = min(t0, t1), tmax = max(t0, t1);
8251
+ float tNear = max(max(tmin.x, tmin.y), tmin.z);
8252
+ float tFar = min(min(tmax.x, tmax.y), tmax.z);
8253
+ tNear = max(tNear, 0.0);
8254
+ if (tFar <= tNear) discard;
8255
+
8256
+ float dt = (tFar - tNear) / float(${STEPS});
8257
+ vec4 acc = vec4(0.0);
8258
+ for (int i = 0; i < ${STEPS}; i++) {
8259
+ float t = tNear + (float(i) + 0.5) * dt;
8260
+ vec3 p = uCamLocal + dir * t;
8261
+ float v = texture(uVolume, p).r; // normalized 0..1
8262
+ vec3 col = texture(uLUT, vec2(v, 0.5)).rgb;
8263
+ float a = clamp(v * uDensity * dt * 120.0, 0.0, 1.0);
8264
+ acc.rgb += (1.0 - acc.a) * col * a;
8265
+ acc.a += (1.0 - acc.a) * a;
8266
+ if (acc.a > 0.98) break;
8267
+ }
8268
+ if (acc.a < 0.001) discard;
8269
+ outColor = acc; // premultiplied
8270
+ }`
8271
+ );
8272
+ var programCache20 = /* @__PURE__ */ new WeakMap();
8273
+ function getProgram17(gl) {
8274
+ let p = programCache20.get(gl);
8275
+ if (!p) {
8276
+ p = createProgram(gl, VERT17, FRAG19);
8277
+ programCache20.set(gl, p);
8278
+ }
8279
+ return p;
8280
+ }
8281
+ var CUBE_CORNERS = [
8282
+ [0, 0, 0],
8283
+ [1, 0, 0],
8284
+ [1, 1, 0],
8285
+ [0, 1, 0],
8286
+ [0, 0, 1],
8287
+ [1, 0, 1],
8288
+ [1, 1, 1],
8289
+ [0, 1, 1]
8290
+ ];
8291
+ var CUBE_FACES = [
8292
+ [0, 1, 2, 0, 2, 3],
8293
+ [4, 6, 5, 4, 7, 6],
8294
+ [0, 4, 5, 0, 5, 1],
8295
+ [3, 2, 6, 3, 6, 7],
8296
+ [0, 3, 7, 0, 7, 4],
8297
+ [1, 5, 6, 1, 6, 2]
8298
+ ];
8299
+ var counter24 = 0;
8300
+ var VolumeLayer = class {
8301
+ constructor(gl, opts) {
8302
+ this.camLocal = [0.5, 0.5, 2];
8303
+ this.id = `volume-${counter24++}`;
8304
+ this.gl = gl;
8305
+ this.program = getProgram17(gl);
8306
+ this.name = opts.name;
8307
+ this.density = opts.density ?? 1;
8308
+ this.cmapName = opts.colormap ?? "viridis";
8309
+ const [nx, ny, nz] = opts.dims;
8310
+ const ex = opts.extent?.x ?? [0, nx - 1];
8311
+ const ey = opts.extent?.y ?? [0, ny - 1];
8312
+ const ez = opts.extent?.z ?? [0, nz - 1];
8313
+ this.ext = { x: ex, y: ey, z: ez };
8314
+ this.b3 = { x: ex, y: ey, z: ez };
8315
+ let vmin = opts.domain?.[0] ?? Infinity;
8316
+ let vmax = opts.domain?.[1] ?? -Infinity;
8317
+ if (!opts.domain) {
8318
+ for (let i = 0; i < opts.values.length; i++) {
8319
+ const v = opts.values[i];
8320
+ if (v < vmin) vmin = v;
8321
+ if (v > vmax) vmax = v;
8322
+ }
8323
+ }
8324
+ this.vDomain = [vmin, vmax];
8325
+ const span = vmax - vmin || 1;
8326
+ const voxels = new Uint8Array(nx * ny * nz);
8327
+ for (let i = 0; i < voxels.length; i++) voxels[i] = Math.max(0, Math.min(255, Math.round((opts.values[i] - vmin) / span * 255)));
8328
+ this.tex = gl.createTexture();
8329
+ gl.bindTexture(gl.TEXTURE_3D, this.tex);
8330
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
8331
+ gl.texImage3D(gl.TEXTURE_3D, 0, gl.R8, nx, ny, nz, 0, gl.RED, gl.UNSIGNED_BYTE, voxels);
8332
+ gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
8333
+ gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
8334
+ gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
8335
+ gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
8336
+ gl.texParameteri(gl.TEXTURE_3D, gl.TEXTURE_WRAP_R, gl.CLAMP_TO_EDGE);
8337
+ gl.bindTexture(gl.TEXTURE_3D, null);
8338
+ const cmap = colormap(this.cmapName);
8339
+ const lutPix = new Uint8Array(256 * 4);
8340
+ for (let i = 0; i < 256; i++) {
8341
+ const [r, g, b] = cmap(i / 255);
8342
+ lutPix[i * 4] = Math.round(r * 255);
8343
+ lutPix[i * 4 + 1] = Math.round(g * 255);
8344
+ lutPix[i * 4 + 2] = Math.round(b * 255);
8345
+ lutPix[i * 4 + 3] = 255;
8346
+ }
8347
+ this.lut = gl.createTexture();
8348
+ gl.bindTexture(gl.TEXTURE_2D, this.lut);
8349
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 256, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, lutPix);
8350
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
8351
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
8352
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
8353
+ gl.bindTexture(gl.TEXTURE_2D, null);
8354
+ const data = [];
8355
+ for (const face of CUBE_FACES) {
8356
+ for (const ci of face) {
8357
+ const c = CUBE_CORNERS[ci];
8358
+ const wxp = ex[0] + c[0] * (ex[1] - ex[0]);
8359
+ const wyp = ey[0] + c[1] * (ey[1] - ey[0]);
8360
+ const wzp = ez[0] + c[2] * (ez[1] - ez[0]);
8361
+ data.push(wxp, wyp, wzp, c[0], c[1], c[2]);
8362
+ }
8363
+ }
8364
+ this.vao = gl.createVertexArray();
8365
+ this.buffer = gl.createBuffer();
5305
8366
  gl.bindVertexArray(this.vao);
5306
- gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
8367
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer);
8368
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
8369
+ gl.enableVertexAttribArray(0);
8370
+ gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 24, 0);
8371
+ gl.enableVertexAttribArray(1);
8372
+ gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 24, 12);
8373
+ gl.bindVertexArray(null);
8374
+ this.uniforms = uniformLocations(gl, this.program, ["uMVP", "uVolume", "uLUT", "uCamLocal", "uDensity"]);
8375
+ }
8376
+ bounds3() {
8377
+ return this.b3;
8378
+ }
8379
+ colorInfo() {
8380
+ return { colormap: this.cmapName, domain: this.vDomain, label: this.name };
8381
+ }
8382
+ /** Set the camera position in world space; converted to the volume's [0,1] local space. */
8383
+ setCamera(worldEye) {
8384
+ const { x, y, z } = this.ext;
8385
+ this.camLocal = [
8386
+ (worldEye[0] - x[0]) / (x[1] - x[0] || 1),
8387
+ (worldEye[1] - y[0]) / (y[1] - y[0] || 1),
8388
+ (worldEye[2] - z[0]) / (z[1] - z[0] || 1)
8389
+ ];
8390
+ }
8391
+ draw(gl, mvp) {
8392
+ gl.useProgram(this.program);
8393
+ gl.uniformMatrix4fv(this.uniforms.uMVP, false, mvp);
8394
+ gl.uniform3f(this.uniforms.uCamLocal, this.camLocal[0], this.camLocal[1], this.camLocal[2]);
8395
+ gl.uniform1f(this.uniforms.uDensity, this.density);
8396
+ gl.activeTexture(gl.TEXTURE0);
8397
+ gl.bindTexture(gl.TEXTURE_3D, this.tex);
8398
+ gl.uniform1i(this.uniforms.uVolume, 0);
8399
+ gl.activeTexture(gl.TEXTURE1);
8400
+ gl.bindTexture(gl.TEXTURE_2D, this.lut);
8401
+ gl.uniform1i(this.uniforms.uLUT, 1);
8402
+ gl.enable(gl.BLEND);
8403
+ gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
8404
+ gl.depthMask(false);
8405
+ gl.enable(gl.CULL_FACE);
8406
+ gl.cullFace(gl.FRONT);
8407
+ gl.bindVertexArray(this.vao);
8408
+ gl.drawArrays(gl.TRIANGLES, 0, 36);
5307
8409
  gl.bindVertexArray(null);
8410
+ gl.disable(gl.CULL_FACE);
8411
+ gl.depthMask(true);
8412
+ gl.disable(gl.BLEND);
8413
+ gl.activeTexture(gl.TEXTURE0);
5308
8414
  }
5309
8415
  dispose() {
5310
8416
  this.gl.deleteVertexArray(this.vao);
5311
8417
  this.gl.deleteBuffer(this.buffer);
8418
+ this.gl.deleteTexture(this.tex);
8419
+ this.gl.deleteTexture(this.lut);
5312
8420
  }
5313
8421
  };
5314
8422
 
@@ -5412,11 +8520,24 @@ var Plot3D = class {
5412
8520
  this.frameRequested = false;
5413
8521
  this.tickCount = 0;
5414
8522
  this.labels = [];
8523
+ /** Tick positions in cube space [-1,1], per axis, for the grid planes. */
8524
+ this.tickCube = { x: [], y: [], z: [] };
8525
+ this.gridVao = null;
8526
+ this.gridBuf = null;
8527
+ /** Normalize params (world→cube: `cube = s*world + t`), for the volume camera. */
8528
+ this.norm = { sx: 1, sy: 1, sz: 1, tx: 0, ty: 0, tz: 0 };
8529
+ /** Auto-orbit speed (rad/frame); 0 = off. Paused while the pointer is down. */
8530
+ this.autoRotateSpeed = 0;
8531
+ this.rotating = false;
8532
+ this.interacting = false;
5415
8533
  // Lighting.
5416
8534
  this.lightAz = 0.9;
5417
8535
  this.lightEl = 0.9;
5418
8536
  this.ambient = 0.35;
5419
8537
  this.controlsEl = null;
8538
+ this.resetBtn = null;
8539
+ /** Screen position (device px) of the picked point, for the highlight ring. */
8540
+ this.hoverHit = null;
5420
8541
  this.container = container;
5421
8542
  if (getComputedStyle(container).position === "static") container.style.position = "relative";
5422
8543
  this.canvas = document.createElement("canvas");
@@ -5430,7 +8551,86 @@ var Plot3D = class {
5430
8551
  this.azimuth = options.azimuth ?? 0.7;
5431
8552
  this.elevation = options.elevation ?? 0.5;
5432
8553
  this.distance = options.distance ?? 3.6;
8554
+ this.initialAz = this.azimuth;
8555
+ this.initialEl = this.elevation;
8556
+ this.initialDist = this.distance;
5433
8557
  this.axisLabels = options.axisLabels ?? {};
8558
+ this.title = options.title;
8559
+ this.showLegend = options.legend ?? false;
8560
+ this.colorbarOpt = options.colorbar ?? true;
8561
+ this.hoverEnabled = options.hover !== false;
8562
+ this.gridPlanes = options.gridPlanes !== false;
8563
+ this.legendDiv = document.createElement("div");
8564
+ Object.assign(this.legendDiv.style, {
8565
+ position: "absolute",
8566
+ top: "8px",
8567
+ right: "8px",
8568
+ zIndex: "5",
8569
+ display: "none",
8570
+ pointerEvents: "none",
8571
+ padding: "6px 8px",
8572
+ borderRadius: "6px",
8573
+ font: "12px system-ui, sans-serif",
8574
+ lineHeight: "1.5",
8575
+ background: "rgba(15,23,42,0.8)",
8576
+ color: "#e2e8f0",
8577
+ border: "1px solid rgba(148,163,184,0.25)"
8578
+ });
8579
+ container.appendChild(this.legendDiv);
8580
+ this.colorbarDiv = document.createElement("div");
8581
+ Object.assign(this.colorbarDiv.style, {
8582
+ position: "absolute",
8583
+ bottom: "12px",
8584
+ right: "10px",
8585
+ zIndex: "5",
8586
+ display: "none",
8587
+ pointerEvents: "none",
8588
+ font: "10px system-ui, sans-serif",
8589
+ color: "#cbd5e1",
8590
+ textAlign: "center"
8591
+ });
8592
+ container.appendChild(this.colorbarDiv);
8593
+ this.tooltip = document.createElement("div");
8594
+ Object.assign(this.tooltip.style, {
8595
+ position: "absolute",
8596
+ display: "none",
8597
+ zIndex: "6",
8598
+ pointerEvents: "none",
8599
+ padding: "5px 7px",
8600
+ borderRadius: "6px",
8601
+ font: "12px system-ui, sans-serif",
8602
+ whiteSpace: "nowrap",
8603
+ background: "rgba(15,23,42,0.92)",
8604
+ color: "#e2e8f0",
8605
+ border: "1px solid rgba(148,163,184,0.3)",
8606
+ boxShadow: "0 4px 12px rgba(0,0,0,0.3)"
8607
+ });
8608
+ container.appendChild(this.tooltip);
8609
+ if (options.resetButton !== false) {
8610
+ const btn = document.createElement("button");
8611
+ btn.type = "button";
8612
+ btn.title = "Reset view";
8613
+ btn.textContent = "\u2302";
8614
+ Object.assign(btn.style, {
8615
+ position: "absolute",
8616
+ bottom: "8px",
8617
+ left: "8px",
8618
+ zIndex: "6",
8619
+ width: "26px",
8620
+ height: "26px",
8621
+ padding: "0",
8622
+ cursor: "pointer",
8623
+ borderRadius: "6px",
8624
+ font: "15px system-ui, sans-serif",
8625
+ lineHeight: "24px",
8626
+ background: "rgba(15,23,42,0.8)",
8627
+ color: "#cbd5e1",
8628
+ border: "1px solid rgba(148,163,184,0.25)"
8629
+ });
8630
+ btn.addEventListener("click", () => this.resetView());
8631
+ container.appendChild(btn);
8632
+ this.resetBtn = btn;
8633
+ }
5434
8634
  this.lineProgram = createProgram(this.gl, LINE_VERT, LINE_FRAG);
5435
8635
  this.lineUniforms = uniformLocations(this.gl, this.lineProgram, ["uVP", "uColor"]);
5436
8636
  this.boxVao = this.gl.createVertexArray();
@@ -5439,11 +8639,15 @@ var Plot3D = class {
5439
8639
  this.tickVao = this.gl.createVertexArray();
5440
8640
  this.tickBuf = this.gl.createBuffer();
5441
8641
  this.bindLineVao(this.tickVao, this.tickBuf, new Float32Array(0));
8642
+ this.gridVao = this.gl.createVertexArray();
8643
+ this.gridBuf = this.gl.createBuffer();
8644
+ this.bindLineVao(this.gridVao, this.gridBuf, new Float32Array(0));
5442
8645
  if (options.lightControls) this.buildControls();
5443
8646
  this.resizeObserver = new ResizeObserver(() => this.resize());
5444
8647
  this.resizeObserver.observe(container);
5445
8648
  this.attachControls();
5446
8649
  this.resize();
8650
+ if (options.autoRotate) this.setAutoRotate(options.autoRotate);
5447
8651
  }
5448
8652
  bindLineVao(vao, buf, data) {
5449
8653
  const gl = this.gl;
@@ -5469,6 +8673,56 @@ var Plot3D = class {
5469
8673
  this.requestRender();
5470
8674
  return l;
5471
8675
  }
8676
+ /** A 3D polyline / path. */
8677
+ addLine3D(opts) {
8678
+ const l = new Line3DLayer(this.gl, opts);
8679
+ this.layers.push(l);
8680
+ this.recompute();
8681
+ this.requestRender();
8682
+ return l;
8683
+ }
8684
+ /** 3D bars (columns) on an x/z grid, lit and optionally colormapped. */
8685
+ addBar3D(opts) {
8686
+ const l = new Bar3DLayer(this.gl, opts);
8687
+ l.setLight(this.lightDir(), this.ambient);
8688
+ this.layers.push(l);
8689
+ this.recompute();
8690
+ this.requestRender();
8691
+ return l;
8692
+ }
8693
+ /** A 3D vector field (arrows), optionally colored by magnitude. */
8694
+ addQuiver3D(opts) {
8695
+ const l = new Quiver3DLayer(this.gl, opts);
8696
+ this.layers.push(l);
8697
+ this.recompute();
8698
+ this.requestRender();
8699
+ return l;
8700
+ }
8701
+ /** 3D iso-height contour lines of a grid, stacked at their level heights. */
8702
+ addContour3D(opts) {
8703
+ const l = new Contour3DLayer(this.gl, opts);
8704
+ this.layers.push(l);
8705
+ this.recompute();
8706
+ this.requestRender();
8707
+ return l;
8708
+ }
8709
+ /** A marching-cubes isosurface of a 3D scalar volume (lit, solid-colored). */
8710
+ addIsosurface(opts) {
8711
+ const l = new IsosurfaceLayer(this.gl, opts);
8712
+ l.setLight(this.lightDir(), this.ambient);
8713
+ this.layers.push(l);
8714
+ this.recompute();
8715
+ this.requestRender();
8716
+ return l;
8717
+ }
8718
+ /** Direct volume rendering (GPU raymarch) of a 3D scalar field. */
8719
+ addVolume(opts) {
8720
+ const l = new VolumeLayer(this.gl, opts);
8721
+ this.layers.push(l);
8722
+ this.recompute();
8723
+ this.requestRender();
8724
+ return l;
8725
+ }
5472
8726
  /** Update the light direction (azimuth/elevation, radians) and ambient (0..1). */
5473
8727
  setLight(params) {
5474
8728
  if (params.azimuth != null) this.lightAz = params.azimuth;
@@ -5484,15 +8738,125 @@ var Plot3D = class {
5484
8738
  const el = this.lightEl, az = this.lightAz;
5485
8739
  return [Math.cos(el) * Math.cos(az), Math.sin(el), Math.cos(el) * Math.sin(az)];
5486
8740
  }
8741
+ /** Auto-orbit the camera. `true` = default speed, number = radians/frame, `false`/0 stops. */
8742
+ setAutoRotate(speed) {
8743
+ this.autoRotateSpeed = speed === true ? 6e-3 : speed === false ? 0 : speed;
8744
+ if (this.autoRotateSpeed !== 0 && !this.rotating) {
8745
+ this.rotating = true;
8746
+ const loop = () => {
8747
+ if (this.autoRotateSpeed === 0) {
8748
+ this.rotating = false;
8749
+ return;
8750
+ }
8751
+ if (!this.interacting) {
8752
+ this.azimuth += this.autoRotateSpeed;
8753
+ this.render();
8754
+ }
8755
+ requestAnimationFrame(loop);
8756
+ };
8757
+ requestAnimationFrame(loop);
8758
+ }
8759
+ }
8760
+ /** Restore the initial camera orientation and zoom. */
8761
+ resetView() {
8762
+ this.azimuth = this.initialAz;
8763
+ this.elevation = this.initialEl;
8764
+ this.distance = this.initialDist;
8765
+ this.requestRender();
8766
+ }
8767
+ /** Nearest pickable point to the cursor. Screen coords are device px. */
8768
+ pick(clientX, clientY) {
8769
+ const { mvp } = this.viewProj();
8770
+ const w = this.canvas.width, h = this.canvas.height;
8771
+ const rect = this.canvas.getBoundingClientRect();
8772
+ const cx = (clientX - rect.left) * this.dpr;
8773
+ const cy = (clientY - rect.top) * this.dpr;
8774
+ const gate = 14 * this.dpr;
8775
+ let bestD = gate * gate;
8776
+ let best = null;
8777
+ for (const l of this.layers) {
8778
+ const pd = l.pickData?.();
8779
+ if (!pd) continue;
8780
+ const pos = pd.positions;
8781
+ const n = pos.length / 3;
8782
+ for (let i = 0; i < n; i++) {
8783
+ const c = transformPoint(mvp, pos[i * 3], pos[i * 3 + 1], pos[i * 3 + 2]);
8784
+ if (c[3] <= 0) continue;
8785
+ const sx = (c[0] / c[3] * 0.5 + 0.5) * w;
8786
+ const sy = (1 - (c[1] / c[3] * 0.5 + 0.5)) * h;
8787
+ const dx = sx - cx, dy = sy - cy;
8788
+ const d = dx * dx + dy * dy;
8789
+ if (d < bestD) {
8790
+ bestD = d;
8791
+ best = {
8792
+ sx,
8793
+ sy,
8794
+ label: pd.label ? pd.label(i) : `${defaultFormat(pos[i * 3])}, ${defaultFormat(pos[i * 3 + 1])}, ${defaultFormat(pos[i * 3 + 2])}`
8795
+ };
8796
+ }
8797
+ }
8798
+ }
8799
+ return best;
8800
+ }
8801
+ /** Show/hide the hover tooltip + highlight ring for the point under the cursor. */
8802
+ updateHover(clientX, clientY) {
8803
+ if (!this.hoverEnabled) return;
8804
+ const hit = this.pick(clientX, clientY);
8805
+ const had = this.hoverHit !== null;
8806
+ if (!hit) {
8807
+ this.tooltip.style.display = "none";
8808
+ if (had) {
8809
+ this.hoverHit = null;
8810
+ this.requestRender();
8811
+ }
8812
+ return;
8813
+ }
8814
+ this.hoverHit = { sx: hit.sx, sy: hit.sy };
8815
+ this.requestRender();
8816
+ const rect = this.canvas.getBoundingClientRect();
8817
+ this.tooltip.textContent = hit.label;
8818
+ this.tooltip.style.display = "block";
8819
+ const lx = clientX - rect.left, ly = clientY - rect.top;
8820
+ const cw = this.container.clientWidth, ch = this.container.clientHeight;
8821
+ const tw = this.tooltip.offsetWidth, th = this.tooltip.offsetHeight;
8822
+ let left = lx + 14;
8823
+ if (left + tw > cw) left = lx - tw - 14;
8824
+ let top = ly + 14;
8825
+ if (top + th > ch) top = ly - th - 14;
8826
+ this.tooltip.style.left = `${Math.max(0, left)}px`;
8827
+ this.tooltip.style.top = `${Math.max(0, top)}px`;
8828
+ }
8829
+ /** Remove a layer, re-fit, and redraw. */
8830
+ removeLayer(layer) {
8831
+ const i = this.layers.indexOf(layer);
8832
+ if (i >= 0) {
8833
+ this.layers.splice(i, 1);
8834
+ layer.dispose();
8835
+ this.recompute();
8836
+ this.requestRender();
8837
+ }
8838
+ }
8839
+ /** Re-fit the axes to the data and redraw — call after a layer's `setData(...)`. */
8840
+ refresh() {
8841
+ this.recompute();
8842
+ this.requestRender();
8843
+ }
5487
8844
  destroy() {
5488
8845
  this.resizeObserver.disconnect();
8846
+ this.autoRotateSpeed = 0;
5489
8847
  for (const l of this.layers) l.dispose();
5490
8848
  this.gl.deleteProgram(this.lineProgram);
5491
8849
  this.gl.deleteVertexArray(this.boxVao);
5492
8850
  this.gl.deleteVertexArray(this.tickVao);
8851
+ if (this.gridVao) this.gl.deleteVertexArray(this.gridVao);
5493
8852
  this.gl.deleteBuffer(this.boxBuf);
5494
8853
  this.gl.deleteBuffer(this.tickBuf);
8854
+ if (this.gridBuf) this.gl.deleteBuffer(this.gridBuf);
5495
8855
  this.controlsEl?.remove();
8856
+ this.legendDiv.remove();
8857
+ this.colorbarDiv.remove();
8858
+ this.tooltip.remove();
8859
+ this.resetBtn?.remove();
5496
8860
  this.container.removeChild(this.canvas);
5497
8861
  }
5498
8862
  recompute() {
@@ -5514,6 +8878,7 @@ var Plot3D = class {
5514
8878
  };
5515
8879
  const [sx, tx] = axis(b.x), [sy, ty] = axis(b.y), [sz, tz] = axis(b.z);
5516
8880
  this.normalize = scaleTranslate([sx, sy, sz], [tx, ty, tz]);
8881
+ this.norm = { sx, sy, sz, tx, ty, tz };
5517
8882
  this.buildAxes();
5518
8883
  }
5519
8884
  /** Build tick-mark line geometry + tick/title labels from the data bounds. */
@@ -5523,22 +8888,27 @@ var Plot3D = class {
5523
8888
  const cube = (r, v) => 2 * (v - r[0]) / (r[1] - r[0] || 1) - 1;
5524
8889
  const seg = [];
5525
8890
  const labels = [];
8891
+ const tc = { x: [], y: [], z: [] };
5526
8892
  const mark = (x0, y0, z0, x1, y1, z1) => seg.push(x0, y0, z0, x1, y1, z1);
5527
8893
  for (const t of autoTicks(b.x[0], b.x[1], 5)) {
5528
8894
  const cx = cube(b.x, t.value);
8895
+ tc.x.push(cx);
5529
8896
  mark(cx, -1, -1, cx, -1, -1.06);
5530
8897
  labels.push({ p: [cx, -1, -1.16], text: defaultFormat(t.value) });
5531
8898
  }
5532
8899
  for (const t of autoTicks(b.y[0], b.y[1], 5)) {
5533
8900
  const cy = cube(b.y, t.value);
8901
+ tc.y.push(cy);
5534
8902
  mark(-1, cy, -1, -1.06, cy, -1);
5535
8903
  labels.push({ p: [-1.16, cy, -1], text: defaultFormat(t.value) });
5536
8904
  }
5537
8905
  for (const t of autoTicks(b.z[0], b.z[1], 5)) {
5538
8906
  const cz = cube(b.z, t.value);
8907
+ tc.z.push(cz);
5539
8908
  mark(-1, -1, cz, -1.06, -1, cz);
5540
8909
  labels.push({ p: [-1.16, -1, cz], text: defaultFormat(t.value) });
5541
8910
  }
8911
+ this.tickCube = tc;
5542
8912
  if (this.axisLabels.x) labels.push({ p: [0, -1.25, -1.3], text: this.axisLabels.x, title: true });
5543
8913
  if (this.axisLabels.y) labels.push({ p: [-1.35, 0, -1], text: this.axisLabels.y, title: true });
5544
8914
  if (this.axisLabels.z) labels.push({ p: [-1.3, -1.25, 0], text: this.axisLabels.z, title: true });
@@ -5573,7 +8943,31 @@ var Plot3D = class {
5573
8943
  ];
5574
8944
  const view = lookAt(eye, [0, 0, 0], [0, 1, 0]);
5575
8945
  const vp = multiply(proj, view);
5576
- return { vp, mvp: multiply(vp, this.normalize) };
8946
+ return { vp, mvp: multiply(vp, this.normalize), eye };
8947
+ }
8948
+ /** Build + draw grid lines on the 3 back walls (cube space) for the current eye. */
8949
+ drawGridPlanes(vp, eye) {
8950
+ const tc = this.tickCube;
8951
+ if (!this.gridVao || tc.x.length + tc.y.length + tc.z.length === 0) return;
8952
+ const sx = eye[0] > 0 ? -1 : 1;
8953
+ const sy = eye[1] > 0 ? -1 : 1;
8954
+ const sz = eye[2] > 0 ? -1 : 1;
8955
+ const seg = [];
8956
+ const L = (x0, y0, z0, x1, y1, z1) => seg.push(x0, y0, z0, x1, y1, z1);
8957
+ for (const cy of tc.y) L(sx, cy, -1, sx, cy, 1);
8958
+ for (const cz of tc.z) L(sx, -1, cz, sx, 1, cz);
8959
+ for (const cx of tc.x) L(cx, sy, -1, cx, sy, 1);
8960
+ for (const cz of tc.z) L(-1, sy, cz, 1, sy, cz);
8961
+ for (const cx of tc.x) L(cx, -1, sz, cx, 1, sz);
8962
+ for (const cy of tc.y) L(-1, cy, sz, 1, cy, sz);
8963
+ const gl = this.gl;
8964
+ gl.bindVertexArray(this.gridVao);
8965
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.gridBuf);
8966
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(seg), gl.DYNAMIC_DRAW);
8967
+ gl.uniformMatrix4fv(this.lineUniforms.uVP, false, vp);
8968
+ gl.uniform4f(this.lineUniforms.uColor, 0.55, 0.6, 0.72, 0.22);
8969
+ gl.drawArrays(gl.LINES, 0, seg.length / 3);
8970
+ gl.bindVertexArray(null);
5577
8971
  }
5578
8972
  render() {
5579
8973
  const gl = this.gl;
@@ -5583,8 +8977,9 @@ var Plot3D = class {
5583
8977
  gl.viewport(0, 0, w, h);
5584
8978
  gl.clearColor(this.bg[0], this.bg[1], this.bg[2], this.bg[3]);
5585
8979
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
5586
- const { vp, mvp } = this.viewProj();
8980
+ const { vp, mvp, eye } = this.viewProj();
5587
8981
  gl.useProgram(this.lineProgram);
8982
+ if (this.gridPlanes) this.drawGridPlanes(vp, eye);
5588
8983
  gl.uniformMatrix4fv(this.lineUniforms.uVP, false, vp);
5589
8984
  gl.uniform4f(this.lineUniforms.uColor, 0.6, 0.65, 0.75, 0.4);
5590
8985
  gl.bindVertexArray(this.boxVao);
@@ -5595,10 +8990,119 @@ var Plot3D = class {
5595
8990
  gl.drawArrays(gl.LINES, 0, this.tickCount);
5596
8991
  }
5597
8992
  gl.bindVertexArray(null);
5598
- for (const l of this.layers) l.draw(gl, mvp);
8993
+ const n = this.norm;
8994
+ const worldEye = [(eye[0] - n.tx) / n.sx, (eye[1] - n.ty) / n.sy, (eye[2] - n.tz) / n.sz];
8995
+ for (const l of this.layers) {
8996
+ if ("setCamera" in l) l.setCamera(worldEye);
8997
+ l.draw(gl, mvp);
8998
+ }
5599
8999
  this.displayCtx.clearRect(0, 0, w, h);
5600
9000
  this.displayCtx.drawImage(this.sharedCanvas, 0, 0);
5601
9001
  this.drawLabels(vp, w, h);
9002
+ if (this.title) {
9003
+ const ctx = this.displayCtx;
9004
+ ctx.save();
9005
+ ctx.font = `600 ${15 * this.dpr}px system-ui, sans-serif`;
9006
+ ctx.fillStyle = "#e2e8f0";
9007
+ ctx.textAlign = "center";
9008
+ ctx.textBaseline = "top";
9009
+ ctx.fillText(this.title, w / 2, 10 * this.dpr);
9010
+ ctx.restore();
9011
+ }
9012
+ if (this.hoverHit) {
9013
+ const ctx = this.displayCtx;
9014
+ ctx.save();
9015
+ ctx.strokeStyle = "#e2e8f0";
9016
+ ctx.lineWidth = 2 * this.dpr;
9017
+ ctx.beginPath();
9018
+ ctx.arc(this.hoverHit.sx, this.hoverHit.sy, 7 * this.dpr, 0, Math.PI * 2);
9019
+ ctx.stroke();
9020
+ ctx.restore();
9021
+ }
9022
+ this.updateLegend();
9023
+ this.updateColorbar();
9024
+ }
9025
+ /** DOM legend of named, solid-colored layers. */
9026
+ updateLegend() {
9027
+ const div = this.legendDiv;
9028
+ const entries = this.showLegend ? this.layers.filter((l) => typeof l.name === "string" && !!l.name && typeof l.colorCss === "string") : [];
9029
+ if (entries.length === 0) {
9030
+ div.style.display = "none";
9031
+ return;
9032
+ }
9033
+ div.replaceChildren();
9034
+ for (const e of entries) {
9035
+ const row = document.createElement("div");
9036
+ Object.assign(row.style, { display: "flex", alignItems: "center", gap: "6px" });
9037
+ const sw = document.createElement("span");
9038
+ Object.assign(sw.style, { width: "10px", height: "10px", borderRadius: "2px", background: e.colorCss, flex: "0 0 auto" });
9039
+ const label = document.createElement("span");
9040
+ label.textContent = e.name;
9041
+ row.appendChild(sw);
9042
+ row.appendChild(label);
9043
+ div.appendChild(row);
9044
+ }
9045
+ div.style.display = "block";
9046
+ }
9047
+ /** DOM colorbar for the first colormapped layer (gradient + value range). */
9048
+ updateColorbar() {
9049
+ const div = this.colorbarDiv;
9050
+ if (this.colorbarOpt === false) {
9051
+ div.style.display = "none";
9052
+ return;
9053
+ }
9054
+ let info = null;
9055
+ for (const l of this.layers) {
9056
+ const ci = l.colorInfo?.();
9057
+ if (ci) {
9058
+ info = ci;
9059
+ break;
9060
+ }
9061
+ }
9062
+ if (!info) {
9063
+ div.style.display = "none";
9064
+ return;
9065
+ }
9066
+ const cmap = colormap(info.colormap);
9067
+ const stops = [];
9068
+ for (let i = 0; i <= 12; i++) {
9069
+ const t = i / 12;
9070
+ const [r, g, b] = cmap(t);
9071
+ stops.push(`rgb(${Math.round(r * 255)},${Math.round(g * 255)},${Math.round(b * 255)}) ${t * 100}%`);
9072
+ }
9073
+ const label = (typeof this.colorbarOpt === "object" ? this.colorbarOpt.label : void 0) ?? info.label;
9074
+ div.replaceChildren();
9075
+ if (label) {
9076
+ const cap = document.createElement("div");
9077
+ cap.textContent = label;
9078
+ cap.style.marginBottom = "3px";
9079
+ div.appendChild(cap);
9080
+ }
9081
+ const rowWrap = document.createElement("div");
9082
+ Object.assign(rowWrap.style, { display: "flex", alignItems: "stretch", gap: "4px", justifyContent: "flex-end" });
9083
+ const bar = document.createElement("div");
9084
+ Object.assign(bar.style, {
9085
+ width: "10px",
9086
+ height: "90px",
9087
+ borderRadius: "2px",
9088
+ background: `linear-gradient(to top, ${stops.join(",")})`,
9089
+ border: "1px solid rgba(148,163,184,0.3)"
9090
+ });
9091
+ const ticks = document.createElement("div");
9092
+ Object.assign(ticks.style, { display: "flex", flexDirection: "column", justifyContent: "space-between" });
9093
+ const hi = document.createElement("span");
9094
+ hi.textContent = defaultFormat(info.domain[1]);
9095
+ const mid = document.createElement("span");
9096
+ mid.textContent = defaultFormat((info.domain[0] + info.domain[1]) / 2);
9097
+ const lo = document.createElement("span");
9098
+ lo.textContent = defaultFormat(info.domain[0]);
9099
+ ticks.appendChild(hi);
9100
+ ticks.appendChild(mid);
9101
+ ticks.appendChild(lo);
9102
+ rowWrap.appendChild(ticks);
9103
+ rowWrap.appendChild(bar);
9104
+ div.appendChild(rowWrap);
9105
+ div.style.display = "block";
5602
9106
  }
5603
9107
  drawLabels(vp, w, h) {
5604
9108
  if (this.labels.length === 0) return;
@@ -5625,21 +9129,35 @@ var Plot3D = class {
5625
9129
  let dragging = false, lastX = 0, lastY = 0;
5626
9130
  el.addEventListener("pointerdown", (e) => {
5627
9131
  dragging = true;
9132
+ this.interacting = true;
5628
9133
  lastX = e.clientX;
5629
9134
  lastY = e.clientY;
5630
9135
  el.setPointerCapture(e.pointerId);
5631
9136
  el.style.cursor = "grabbing";
9137
+ this.tooltip.style.display = "none";
9138
+ this.hoverHit = null;
5632
9139
  });
5633
9140
  el.addEventListener("pointermove", (e) => {
5634
- if (!dragging) return;
9141
+ if (!dragging) {
9142
+ this.updateHover(e.clientX, e.clientY);
9143
+ return;
9144
+ }
5635
9145
  this.azimuth -= (e.clientX - lastX) * 0.01;
5636
9146
  this.elevation += (e.clientY - lastY) * 0.01;
5637
9147
  lastX = e.clientX;
5638
9148
  lastY = e.clientY;
5639
9149
  this.requestRender();
5640
9150
  });
9151
+ el.addEventListener("pointerleave", () => {
9152
+ this.tooltip.style.display = "none";
9153
+ if (this.hoverHit) {
9154
+ this.hoverHit = null;
9155
+ this.requestRender();
9156
+ }
9157
+ });
5641
9158
  const end = (e) => {
5642
9159
  dragging = false;
9160
+ this.interacting = false;
5643
9161
  if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);
5644
9162
  el.style.cursor = "grab";
5645
9163
  };
@@ -5697,20 +9215,30 @@ var Plot3D = class {
5697
9215
  export {
5698
9216
  AreaLayer,
5699
9217
  Axis,
9218
+ Bar3DLayer,
5700
9219
  BarLayer,
5701
9220
  BoxLayer,
5702
9221
  CandlestickLayer,
9222
+ CategoricalScale,
9223
+ Contour3DLayer,
5703
9224
  ContourLayer,
5704
9225
  ErrorBarLayer,
9226
+ GraphLayer,
5705
9227
  HeatmapLayer,
5706
9228
  HexbinLayer,
9229
+ ImageLayer,
9230
+ IsosurfaceLayer,
9231
+ Line3DLayer,
5707
9232
  LineLayer,
5708
9233
  LinearScale,
5709
9234
  LogScale,
9235
+ PatchesLayer,
9236
+ PieLayer,
5710
9237
  Plot,
5711
9238
  Plot3D,
5712
9239
  PointCloudLayer,
5713
9240
  PolarPlot,
9241
+ Quiver3DLayer,
5714
9242
  QuiverLayer,
5715
9243
  ScatterLayer,
5716
9244
  StemLayer,
@@ -5718,20 +9246,26 @@ export {
5718
9246
  TRANSFORM_GLSL,
5719
9247
  TRANSFORM_UNIFORMS,
5720
9248
  TimeScale,
9249
+ VolumeLayer,
5721
9250
  autoTicks,
5722
9251
  boxStats,
5723
9252
  colormap,
9253
+ colormapLUT,
5724
9254
  createProgram,
5725
9255
  createToolbar,
5726
9256
  darkTheme,
5727
9257
  defaultFormat,
9258
+ earcut,
5728
9259
  fft,
9260
+ forceLayout,
5729
9261
  histogram,
5730
9262
  kde,
5731
9263
  lightTheme,
5732
9264
  makeScale,
9265
+ marchingCubes,
5733
9266
  parseColor,
5734
9267
  quantileSorted,
9268
+ resolveAxisStyle,
5735
9269
  resolveTicks,
5736
9270
  setTransformUniforms,
5737
9271
  spectrogram,