@photonviz/core 0.1.0 → 0.2.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
@@ -82,10 +82,10 @@ var Axis = class {
82
82
  }
83
83
  ticks = major;
84
84
  }
85
- const fmt = this.config.format ?? ((v) => scale.formatTick(v));
85
+ const fmt2 = this.config.format ?? ((v) => scale.formatTick(v));
86
86
  return ticks.filter((t) => t.value >= min && t.value <= max).map((t) => ({
87
87
  value: t.value,
88
- label: t.minor ? "" : t.label ?? fmt(t.value),
88
+ label: t.minor ? "" : t.label ?? fmt2(t.value),
89
89
  minor: t.minor ?? false,
90
90
  grid: t.grid ?? !t.minor
91
91
  }));
@@ -854,6 +854,196 @@ var BoxLayer = class {
854
854
  }
855
855
  };
856
856
 
857
+ // src/layers/candlestick.ts
858
+ var BODY_VERT = (
859
+ /* glsl */
860
+ `#version 300 es
861
+ precision highp float;
862
+ layout(location = 0) in vec2 aCorner; // unit rect [0,1]^2
863
+ layout(location = 1) in vec4 aRect; // (x0,y0,x1,y1) offset data space
864
+ layout(location = 2) in vec4 aColor;
865
+ ${TRANSFORM_GLSL}
866
+ out vec4 vColor;
867
+ void main() {
868
+ vec2 p = mix(aRect.xy, aRect.zw, aCorner);
869
+ vColor = aColor;
870
+ gl_Position = vec4(dataToClip(p), 0.0, 1.0);
871
+ }`
872
+ );
873
+ var WICK_VERT = (
874
+ /* glsl */
875
+ `#version 300 es
876
+ precision highp float;
877
+ layout(location = 0) in vec2 aCorner; // (along 0..1, side -1..1)
878
+ layout(location = 1) in vec4 aSeg; // (x0,y0,x1,y1) offset data space
879
+ layout(location = 2) in vec4 aColor;
880
+ uniform vec2 uResolution;
881
+ uniform float uWidth;
882
+ ${TRANSFORM_GLSL}
883
+ out vec4 vColor;
884
+ void main() {
885
+ vec2 s0 = (dataToClip(aSeg.xy) * 0.5 + 0.5) * uResolution;
886
+ vec2 s1 = (dataToClip(aSeg.zw) * 0.5 + 0.5) * uResolution;
887
+ vec2 d = s1 - s0;
888
+ float len = length(d);
889
+ vec2 dir = len > 1e-6 ? d / len : vec2(1.0, 0.0);
890
+ vec2 nrm = vec2(-dir.y, dir.x);
891
+ vec2 pos = mix(s0, s1, aCorner.x) + nrm * (aCorner.y * uWidth * 0.5);
892
+ vColor = aColor;
893
+ gl_Position = vec4((pos / uResolution) * 2.0 - 1.0, 0.0, 1.0);
894
+ }`
895
+ );
896
+ var FRAG4 = (
897
+ /* glsl */
898
+ `#version 300 es
899
+ precision highp float;
900
+ in vec4 vColor;
901
+ out vec4 outColor;
902
+ void main() { outColor = vec4(vColor.rgb * vColor.a, vColor.a); }`
903
+ );
904
+ var RECT_CORNERS = new Float32Array([0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1]);
905
+ var SEG_CORNERS = new Float32Array([0, -1, 1, -1, 1, 1, 0, -1, 1, 1, 0, 1]);
906
+ var cache = /* @__PURE__ */ new WeakMap();
907
+ function programs(gl) {
908
+ let p = cache.get(gl);
909
+ if (!p) {
910
+ p = { body: createProgram(gl, BODY_VERT, FRAG4), wick: createProgram(gl, WICK_VERT, FRAG4) };
911
+ cache.set(gl, p);
912
+ }
913
+ return p;
914
+ }
915
+ function medianSpacing2(x, n) {
916
+ if (n < 2) return 1;
917
+ const diffs = [];
918
+ for (let i = 1; i < n; i++) diffs.push(Math.abs(x[i] - x[i - 1]));
919
+ diffs.sort((a, b) => a - b);
920
+ return diffs[Math.floor(diffs.length / 2)] || 1;
921
+ }
922
+ var counter4 = 0;
923
+ var CandlestickLayer = class {
924
+ constructor(gl, opts) {
925
+ this.buffers = [];
926
+ this.xRef = 0;
927
+ this.yRef = 0;
928
+ this.xBounds = [0, 0];
929
+ this.yBounds = [0, 0];
930
+ this.id = `candlestick-${counter4++}`;
931
+ this.gl = gl;
932
+ this.progs = programs(gl);
933
+ this.name = opts.name ?? this.id;
934
+ this.yAxis = opts.yAxis ?? "y";
935
+ this.wickWidth = opts.wickWidth ?? 1.5;
936
+ const up = toColor(opts.upColor ?? "#26a69a");
937
+ const down = toColor(opts.downColor ?? "#ef5350");
938
+ this.colorCss = toColorCss(up);
939
+ const n = Math.min(opts.x.length, opts.open.length, opts.high.length, opts.low.length, opts.close.length);
940
+ this.count = n;
941
+ const width = opts.width ?? medianSpacing2(opts.x, n) * 0.7;
942
+ this.xRef = n > 0 ? opts.x[0] : 0;
943
+ this.yRef = n > 0 ? opts.open[0] : 0;
944
+ const rects = new Float32Array(n * 4);
945
+ const wicks = new Float32Array(n * 4);
946
+ const colors = new Float32Array(n * 4);
947
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
948
+ for (let i = 0; i < n; i++) {
949
+ const cx = opts.x[i], o = opts.open[i], h = opts.high[i], l = opts.low[i], c = opts.close[i];
950
+ const x0 = cx - width / 2, x1 = cx + width / 2;
951
+ rects[i * 4] = x0 - this.xRef;
952
+ rects[i * 4 + 1] = o - this.yRef;
953
+ rects[i * 4 + 2] = x1 - this.xRef;
954
+ rects[i * 4 + 3] = c - this.yRef;
955
+ wicks[i * 4] = cx - this.xRef;
956
+ wicks[i * 4 + 1] = l - this.yRef;
957
+ wicks[i * 4 + 2] = cx - this.xRef;
958
+ wicks[i * 4 + 3] = h - this.yRef;
959
+ const col = c >= o ? up : down;
960
+ colors[i * 4] = col[0];
961
+ colors[i * 4 + 1] = col[1];
962
+ colors[i * 4 + 2] = col[2];
963
+ colors[i * 4 + 3] = col[3];
964
+ minX = Math.min(minX, x0);
965
+ maxX = Math.max(maxX, x1);
966
+ minY = Math.min(minY, l);
967
+ maxY = Math.max(maxY, h);
968
+ }
969
+ this.xBounds = [minX, maxX];
970
+ this.yBounds = [minY, maxY];
971
+ const cornerRect = gl.createBuffer();
972
+ gl.bindBuffer(gl.ARRAY_BUFFER, cornerRect);
973
+ gl.bufferData(gl.ARRAY_BUFFER, RECT_CORNERS, gl.STATIC_DRAW);
974
+ const cornerSeg = gl.createBuffer();
975
+ gl.bindBuffer(gl.ARRAY_BUFFER, cornerSeg);
976
+ gl.bufferData(gl.ARRAY_BUFFER, SEG_CORNERS, gl.STATIC_DRAW);
977
+ const rectBuf = gl.createBuffer();
978
+ gl.bindBuffer(gl.ARRAY_BUFFER, rectBuf);
979
+ gl.bufferData(gl.ARRAY_BUFFER, rects, gl.STATIC_DRAW);
980
+ const wickBuf = gl.createBuffer();
981
+ gl.bindBuffer(gl.ARRAY_BUFFER, wickBuf);
982
+ gl.bufferData(gl.ARRAY_BUFFER, wicks, gl.STATIC_DRAW);
983
+ const colorBuf = gl.createBuffer();
984
+ gl.bindBuffer(gl.ARRAY_BUFFER, colorBuf);
985
+ gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
986
+ this.buffers = [cornerRect, cornerSeg, rectBuf, wickBuf, colorBuf];
987
+ this.bodyVao = gl.createVertexArray();
988
+ gl.bindVertexArray(this.bodyVao);
989
+ gl.bindBuffer(gl.ARRAY_BUFFER, cornerRect);
990
+ gl.enableVertexAttribArray(0);
991
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
992
+ gl.bindBuffer(gl.ARRAY_BUFFER, rectBuf);
993
+ gl.enableVertexAttribArray(1);
994
+ gl.vertexAttribPointer(1, 4, gl.FLOAT, false, 0, 0);
995
+ gl.vertexAttribDivisor(1, 1);
996
+ gl.bindBuffer(gl.ARRAY_BUFFER, colorBuf);
997
+ gl.enableVertexAttribArray(2);
998
+ gl.vertexAttribPointer(2, 4, gl.FLOAT, false, 0, 0);
999
+ gl.vertexAttribDivisor(2, 1);
1000
+ this.wickVao = gl.createVertexArray();
1001
+ gl.bindVertexArray(this.wickVao);
1002
+ gl.bindBuffer(gl.ARRAY_BUFFER, cornerSeg);
1003
+ gl.enableVertexAttribArray(0);
1004
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
1005
+ gl.bindBuffer(gl.ARRAY_BUFFER, wickBuf);
1006
+ gl.enableVertexAttribArray(1);
1007
+ gl.vertexAttribPointer(1, 4, gl.FLOAT, false, 0, 0);
1008
+ gl.vertexAttribDivisor(1, 1);
1009
+ gl.bindBuffer(gl.ARRAY_BUFFER, colorBuf);
1010
+ gl.enableVertexAttribArray(2);
1011
+ gl.vertexAttribPointer(2, 4, gl.FLOAT, false, 0, 0);
1012
+ gl.vertexAttribDivisor(2, 1);
1013
+ gl.bindVertexArray(null);
1014
+ this.uBody = uniformLocations(gl, this.progs.body, [...TRANSFORM_UNIFORMS]);
1015
+ this.uWick = uniformLocations(gl, this.progs.wick, [...TRANSFORM_UNIFORMS, "uResolution", "uWidth"]);
1016
+ }
1017
+ bounds() {
1018
+ if (this.count === 0) return null;
1019
+ return { x: this.xBounds, y: this.yBounds };
1020
+ }
1021
+ draw(state) {
1022
+ if (this.count === 0) return;
1023
+ const gl = state.gl;
1024
+ gl.useProgram(this.progs.wick);
1025
+ setTransformUniforms(gl, this.uWick, state.x, state.y, this.xRef, this.yRef);
1026
+ gl.uniform2f(this.uWick.uResolution, state.pixelWidth, state.pixelHeight);
1027
+ gl.uniform1f(this.uWick.uWidth, this.wickWidth * state.dpr);
1028
+ gl.bindVertexArray(this.wickVao);
1029
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this.count);
1030
+ gl.bindVertexArray(null);
1031
+ gl.useProgram(this.progs.body);
1032
+ setTransformUniforms(gl, this.uBody, state.x, state.y, this.xRef, this.yRef);
1033
+ gl.bindVertexArray(this.bodyVao);
1034
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this.count);
1035
+ gl.bindVertexArray(null);
1036
+ }
1037
+ dispose() {
1038
+ this.gl.deleteVertexArray(this.bodyVao);
1039
+ this.gl.deleteVertexArray(this.wickVao);
1040
+ for (const b of this.buffers) this.gl.deleteBuffer(b);
1041
+ }
1042
+ };
1043
+ function toColor(input) {
1044
+ return Array.isArray(input) ? input : parseColor(input);
1045
+ }
1046
+
857
1047
  // src/color/colormap.ts
858
1048
  var ANCHORS = {
859
1049
  viridis: [
@@ -917,7 +1107,7 @@ ${TRANSFORM_GLSL}
917
1107
  out vec4 vColor;
918
1108
  void main() { vColor = aColor; gl_Position = vec4(dataToClip(aPos), 0.0, 1.0); }`
919
1109
  );
920
- var FRAG4 = (
1110
+ var FRAG5 = (
921
1111
  /* glsl */
922
1112
  `#version 300 es
923
1113
  precision highp float;
@@ -947,15 +1137,15 @@ var programCache4 = /* @__PURE__ */ new WeakMap();
947
1137
  function getProgram4(gl) {
948
1138
  let p = programCache4.get(gl);
949
1139
  if (!p) {
950
- p = createProgram(gl, VERT4, FRAG4);
1140
+ p = createProgram(gl, VERT4, FRAG5);
951
1141
  programCache4.set(gl, p);
952
1142
  }
953
1143
  return p;
954
1144
  }
955
- var counter4 = 0;
1145
+ var counter5 = 0;
956
1146
  var ContourLayer = class {
957
1147
  constructor(gl, opts) {
958
- this.id = `contour-${counter4++}`;
1148
+ this.id = `contour-${counter5++}`;
959
1149
  this.gl = gl;
960
1150
  this.program = getProgram4(gl);
961
1151
  this.yAxis = opts.yAxis ?? "y";
@@ -1036,6 +1226,230 @@ var ContourLayer = class {
1036
1226
  }
1037
1227
  };
1038
1228
 
1229
+ // src/layers/errorbar.ts
1230
+ var SEG_VERT = (
1231
+ /* glsl */
1232
+ `#version 300 es
1233
+ precision highp float;
1234
+ layout(location = 0) in vec2 aCorner; // (along 0..1, side -1..1)
1235
+ layout(location = 1) in vec4 aSeg; // (x0,y0,x1,y1) offset data space
1236
+ uniform vec2 uResolution;
1237
+ uniform float uWidth;
1238
+ ${TRANSFORM_GLSL}
1239
+ void main() {
1240
+ vec2 s0 = (dataToClip(aSeg.xy) * 0.5 + 0.5) * uResolution;
1241
+ vec2 s1 = (dataToClip(aSeg.zw) * 0.5 + 0.5) * uResolution;
1242
+ vec2 d = s1 - s0;
1243
+ float len = length(d);
1244
+ vec2 dir = len > 1e-6 ? d / len : vec2(1.0, 0.0);
1245
+ vec2 nrm = vec2(-dir.y, dir.x);
1246
+ vec2 pos = mix(s0, s1, aCorner.x) + nrm * (aCorner.y * uWidth * 0.5);
1247
+ gl_Position = vec4((pos / uResolution) * 2.0 - 1.0, 0.0, 1.0);
1248
+ }`
1249
+ );
1250
+ var CAP_VERT = (
1251
+ /* glsl */
1252
+ `#version 300 es
1253
+ precision highp float;
1254
+ layout(location = 0) in vec2 aCorner; // unit quad [-1,1]^2
1255
+ layout(location = 1) in vec3 aCap; // (cx, cy, orient) offset data space
1256
+ uniform vec2 uResolution;
1257
+ uniform float uCapSize;
1258
+ uniform float uWidth;
1259
+ ${TRANSFORM_GLSL}
1260
+ void main() {
1261
+ vec2 c = (dataToClip(aCap.xy) * 0.5 + 0.5) * uResolution;
1262
+ vec2 h = aCap.z < 0.5 ? vec2(uCapSize * 0.5, uWidth * 0.5) : vec2(uWidth * 0.5, uCapSize * 0.5);
1263
+ vec2 pos = c + aCorner * h;
1264
+ gl_Position = vec4((pos / uResolution) * 2.0 - 1.0, 0.0, 1.0);
1265
+ }`
1266
+ );
1267
+ var BAND_VERT = (
1268
+ /* glsl */
1269
+ `#version 300 es
1270
+ precision highp float;
1271
+ layout(location = 0) in vec2 aPos; // offset data space
1272
+ ${TRANSFORM_GLSL}
1273
+ void main() { gl_Position = vec4(dataToClip(aPos), 0.0, 1.0); }`
1274
+ );
1275
+ var SOLID_FRAG = (
1276
+ /* glsl */
1277
+ `#version 300 es
1278
+ precision highp float;
1279
+ uniform vec4 uColor;
1280
+ out vec4 outColor;
1281
+ void main() { outColor = vec4(uColor.rgb * uColor.a, uColor.a); }`
1282
+ );
1283
+ var SEG_CORNERS2 = new Float32Array([0, -1, 1, -1, 1, 1, 0, -1, 1, 1, 0, 1]);
1284
+ var QUAD_CORNERS = new Float32Array([-1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1]);
1285
+ var cache2 = /* @__PURE__ */ new WeakMap();
1286
+ function programs2(gl) {
1287
+ let p = cache2.get(gl);
1288
+ if (!p) {
1289
+ p = {
1290
+ seg: createProgram(gl, SEG_VERT, SOLID_FRAG),
1291
+ cap: createProgram(gl, CAP_VERT, SOLID_FRAG),
1292
+ band: createProgram(gl, BAND_VERT, SOLID_FRAG)
1293
+ };
1294
+ cache2.set(gl, p);
1295
+ }
1296
+ return p;
1297
+ }
1298
+ var errAt = (e, i) => e == null ? 0 : typeof e === "number" ? e : e[i] ?? 0;
1299
+ var counter6 = 0;
1300
+ var ErrorBarLayer = class {
1301
+ constructor(gl, opts) {
1302
+ this.buffers = [];
1303
+ this.segCount = 0;
1304
+ this.capCount = 0;
1305
+ this.bandVerts = 0;
1306
+ this.xRef = 0;
1307
+ this.yRef = 0;
1308
+ this.xBounds = [0, 0];
1309
+ this.yBounds = [0, 0];
1310
+ this.id = `errorbar-${counter6++}`;
1311
+ this.gl = gl;
1312
+ this.progs = programs2(gl);
1313
+ const colorInput = opts.color ?? "#3b82f6";
1314
+ this.color = Array.isArray(colorInput) ? colorInput : parseColor(colorInput);
1315
+ this.colorCss = typeof colorInput === "string" ? colorInput : toColorCss(this.color);
1316
+ this.name = opts.name ?? this.id;
1317
+ this.yAxis = opts.yAxis ?? "y";
1318
+ this.width = opts.width ?? 1.5;
1319
+ this.capSize = opts.capSize ?? 6;
1320
+ this.bandOpacity = opts.bandOpacity ?? 0.2;
1321
+ this.showBand = opts.band === true;
1322
+ this.showWhiskers = opts.whiskers ?? true;
1323
+ const n = Math.min(opts.x.length, opts.y.length);
1324
+ this.xRef = n > 0 ? opts.x[0] : 0;
1325
+ this.yRef = n > 0 ? opts.y[0] : 0;
1326
+ const segs = [];
1327
+ const caps = [];
1328
+ const lowPts = [];
1329
+ const highPts = [];
1330
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
1331
+ const hasY = opts.yerr != null || opts.yerrLow != null || opts.yerrHigh != null;
1332
+ const hasX = opts.xerr != null;
1333
+ for (let i = 0; i < n; i++) {
1334
+ const x = opts.x[i], y = opts.y[i];
1335
+ const eyLo = opts.yerrLow != null ? errAt(opts.yerrLow, i) : errAt(opts.yerr, i);
1336
+ const eyHi = opts.yerrHigh != null ? errAt(opts.yerrHigh, i) : errAt(opts.yerr, i);
1337
+ const ex = errAt(opts.xerr, i);
1338
+ const yLo = y - eyLo, yHi = y + eyHi;
1339
+ const xLo = x - ex, xHi = x + ex;
1340
+ if (hasY) {
1341
+ segs.push(x - this.xRef, yLo - this.yRef, x - this.xRef, yHi - this.yRef);
1342
+ caps.push(x - this.xRef, yLo - this.yRef, 0, x - this.xRef, yHi - this.yRef, 0);
1343
+ }
1344
+ if (hasX) {
1345
+ segs.push(xLo - this.xRef, y - this.yRef, xHi - this.xRef, y - this.yRef);
1346
+ caps.push(xLo - this.xRef, y - this.yRef, 1, xHi - this.xRef, y - this.yRef, 1);
1347
+ }
1348
+ lowPts.push([x - this.xRef, yLo - this.yRef]);
1349
+ highPts.push([x - this.xRef, yHi - this.yRef]);
1350
+ minX = Math.min(minX, xLo);
1351
+ maxX = Math.max(maxX, xHi);
1352
+ minY = Math.min(minY, yLo);
1353
+ maxY = Math.max(maxY, yHi);
1354
+ }
1355
+ this.xBounds = [minX, maxX];
1356
+ this.yBounds = [minY, maxY];
1357
+ this.segCount = segs.length / 4;
1358
+ this.capCount = this.capSize > 0 ? caps.length / 3 : 0;
1359
+ const band = [];
1360
+ for (let i = 0; i < n; i++) {
1361
+ band.push(highPts[i][0], highPts[i][1], lowPts[i][0], lowPts[i][1]);
1362
+ }
1363
+ this.bandVerts = n * 2;
1364
+ const cornerSeg = gl.createBuffer();
1365
+ gl.bindBuffer(gl.ARRAY_BUFFER, cornerSeg);
1366
+ gl.bufferData(gl.ARRAY_BUFFER, SEG_CORNERS2, gl.STATIC_DRAW);
1367
+ const cornerQuad = gl.createBuffer();
1368
+ gl.bindBuffer(gl.ARRAY_BUFFER, cornerQuad);
1369
+ gl.bufferData(gl.ARRAY_BUFFER, QUAD_CORNERS, gl.STATIC_DRAW);
1370
+ const segBuf = gl.createBuffer();
1371
+ gl.bindBuffer(gl.ARRAY_BUFFER, segBuf);
1372
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(segs), gl.STATIC_DRAW);
1373
+ const capBuf = gl.createBuffer();
1374
+ gl.bindBuffer(gl.ARRAY_BUFFER, capBuf);
1375
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(caps), gl.STATIC_DRAW);
1376
+ const bandBuf = gl.createBuffer();
1377
+ gl.bindBuffer(gl.ARRAY_BUFFER, bandBuf);
1378
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(band), gl.STATIC_DRAW);
1379
+ this.buffers = [cornerSeg, cornerQuad, segBuf, capBuf, bandBuf];
1380
+ this.segVao = gl.createVertexArray();
1381
+ gl.bindVertexArray(this.segVao);
1382
+ gl.bindBuffer(gl.ARRAY_BUFFER, cornerSeg);
1383
+ gl.enableVertexAttribArray(0);
1384
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
1385
+ gl.bindBuffer(gl.ARRAY_BUFFER, segBuf);
1386
+ gl.enableVertexAttribArray(1);
1387
+ gl.vertexAttribPointer(1, 4, gl.FLOAT, false, 0, 0);
1388
+ gl.vertexAttribDivisor(1, 1);
1389
+ this.capVao = gl.createVertexArray();
1390
+ gl.bindVertexArray(this.capVao);
1391
+ gl.bindBuffer(gl.ARRAY_BUFFER, cornerQuad);
1392
+ gl.enableVertexAttribArray(0);
1393
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
1394
+ gl.bindBuffer(gl.ARRAY_BUFFER, capBuf);
1395
+ gl.enableVertexAttribArray(1);
1396
+ gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);
1397
+ gl.vertexAttribDivisor(1, 1);
1398
+ this.bandVao = gl.createVertexArray();
1399
+ gl.bindVertexArray(this.bandVao);
1400
+ gl.bindBuffer(gl.ARRAY_BUFFER, bandBuf);
1401
+ gl.enableVertexAttribArray(0);
1402
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
1403
+ gl.bindVertexArray(null);
1404
+ this.uSeg = uniformLocations(gl, this.progs.seg, [...TRANSFORM_UNIFORMS, "uColor", "uResolution", "uWidth"]);
1405
+ this.uCap = uniformLocations(gl, this.progs.cap, [...TRANSFORM_UNIFORMS, "uColor", "uResolution", "uCapSize", "uWidth"]);
1406
+ this.uBand = uniformLocations(gl, this.progs.band, [...TRANSFORM_UNIFORMS, "uColor"]);
1407
+ }
1408
+ bounds() {
1409
+ if (this.segCount === 0 && this.bandVerts === 0) return null;
1410
+ return { x: this.xBounds, y: this.yBounds };
1411
+ }
1412
+ draw(state) {
1413
+ const gl = state.gl;
1414
+ const [r, g, b, a] = this.color;
1415
+ if (this.showBand && this.bandVerts >= 4) {
1416
+ gl.useProgram(this.progs.band);
1417
+ setTransformUniforms(gl, this.uBand, state.x, state.y, this.xRef, this.yRef);
1418
+ gl.uniform4f(this.uBand.uColor, r, g, b, a * this.bandOpacity);
1419
+ gl.bindVertexArray(this.bandVao);
1420
+ gl.drawArrays(gl.TRIANGLE_STRIP, 0, this.bandVerts);
1421
+ gl.bindVertexArray(null);
1422
+ }
1423
+ if (this.showWhiskers && this.segCount > 0) {
1424
+ gl.useProgram(this.progs.seg);
1425
+ setTransformUniforms(gl, this.uSeg, state.x, state.y, this.xRef, this.yRef);
1426
+ gl.uniform4f(this.uSeg.uColor, r, g, b, a);
1427
+ gl.uniform2f(this.uSeg.uResolution, state.pixelWidth, state.pixelHeight);
1428
+ gl.uniform1f(this.uSeg.uWidth, this.width * state.dpr);
1429
+ gl.bindVertexArray(this.segVao);
1430
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this.segCount);
1431
+ gl.bindVertexArray(null);
1432
+ if (this.capCount > 0) {
1433
+ gl.useProgram(this.progs.cap);
1434
+ setTransformUniforms(gl, this.uCap, state.x, state.y, this.xRef, this.yRef);
1435
+ gl.uniform4f(this.uCap.uColor, r, g, b, a);
1436
+ gl.uniform2f(this.uCap.uResolution, state.pixelWidth, state.pixelHeight);
1437
+ gl.uniform1f(this.uCap.uCapSize, this.capSize * state.dpr);
1438
+ gl.uniform1f(this.uCap.uWidth, this.width * state.dpr);
1439
+ gl.bindVertexArray(this.capVao);
1440
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this.capCount);
1441
+ gl.bindVertexArray(null);
1442
+ }
1443
+ }
1444
+ }
1445
+ dispose() {
1446
+ this.gl.deleteVertexArray(this.segVao);
1447
+ this.gl.deleteVertexArray(this.capVao);
1448
+ this.gl.deleteVertexArray(this.bandVao);
1449
+ for (const b of this.buffers) this.gl.deleteBuffer(b);
1450
+ }
1451
+ };
1452
+
1039
1453
  // src/layers/heatmap.ts
1040
1454
  var VERT5 = (
1041
1455
  /* glsl */
@@ -1050,7 +1464,7 @@ void main() {
1050
1464
  gl_Position = vec4(dataToClip(aPos), 0.0, 1.0);
1051
1465
  }`
1052
1466
  );
1053
- var FRAG5 = (
1467
+ var FRAG6 = (
1054
1468
  /* glsl */
1055
1469
  `#version 300 es
1056
1470
  precision highp float;
@@ -1066,15 +1480,15 @@ var programCache5 = /* @__PURE__ */ new WeakMap();
1066
1480
  function getProgram5(gl) {
1067
1481
  let p = programCache5.get(gl);
1068
1482
  if (!p) {
1069
- p = createProgram(gl, VERT5, FRAG5);
1483
+ p = createProgram(gl, VERT5, FRAG6);
1070
1484
  programCache5.set(gl, p);
1071
1485
  }
1072
1486
  return p;
1073
1487
  }
1074
- var counter5 = 0;
1488
+ var counter7 = 0;
1075
1489
  var HeatmapLayer = class {
1076
1490
  constructor(gl, opts) {
1077
- this.id = `heatmap-${counter5++}`;
1491
+ this.id = `heatmap-${counter7++}`;
1078
1492
  this.gl = gl;
1079
1493
  this.program = getProgram5(gl);
1080
1494
  this.yAxis = opts.yAxis ?? "y";
@@ -1191,7 +1605,7 @@ void main() {
1191
1605
  gl_Position = vec4(dataToClip(aCenter + aCorner * uRadius), 0.0, 1.0);
1192
1606
  }`
1193
1607
  );
1194
- var FRAG6 = (
1608
+ var FRAG7 = (
1195
1609
  /* glsl */
1196
1610
  `#version 300 es
1197
1611
  precision highp float;
@@ -1214,12 +1628,12 @@ var programCache6 = /* @__PURE__ */ new WeakMap();
1214
1628
  function getProgram6(gl) {
1215
1629
  let p = programCache6.get(gl);
1216
1630
  if (!p) {
1217
- p = createProgram(gl, VERT6, FRAG6);
1631
+ p = createProgram(gl, VERT6, FRAG7);
1218
1632
  programCache6.set(gl, p);
1219
1633
  }
1220
1634
  return p;
1221
1635
  }
1222
- var counter6 = 0;
1636
+ var counter8 = 0;
1223
1637
  var HexbinLayer = class {
1224
1638
  constructor(gl, opts) {
1225
1639
  this.buffers = [];
@@ -1227,7 +1641,7 @@ var HexbinLayer = class {
1227
1641
  this.yRef = 0;
1228
1642
  this.xBounds = [0, 0];
1229
1643
  this.yBounds = [0, 0];
1230
- this.id = `hexbin-${counter6++}`;
1644
+ this.id = `hexbin-${counter8++}`;
1231
1645
  this.gl = gl;
1232
1646
  this.program = getProgram6(gl);
1233
1647
  this.yAxis = opts.yAxis ?? "y";
@@ -1325,29 +1739,264 @@ var HexbinLayer = class {
1325
1739
  }
1326
1740
  };
1327
1741
 
1328
- // src/layers/line.ts
1329
- var VERT7 = (
1742
+ // src/layers/line-util.ts
1743
+ function decimateIndices(ys, i0, i1, cols) {
1744
+ const out = [i0];
1745
+ const visN = i1 - i0;
1746
+ for (let b = 0; b < cols; b++) {
1747
+ const lo = i0 + Math.floor(visN * b / cols);
1748
+ const hi = i0 + Math.floor(visN * (b + 1) / cols);
1749
+ if (hi <= lo) continue;
1750
+ let iMin = lo, iMax = lo;
1751
+ for (let i = lo; i < hi; i++) {
1752
+ if (ys[i] < ys[iMin]) iMin = i;
1753
+ if (ys[i] > ys[iMax]) iMax = i;
1754
+ }
1755
+ if (iMin < iMax) out.push(iMin, iMax);
1756
+ else out.push(iMax, iMin);
1757
+ }
1758
+ out.push(i1);
1759
+ return out;
1760
+ }
1761
+
1762
+ // src/layers/gpu-decimate.ts
1763
+ var DEC_VERT = (
1330
1764
  /* glsl */
1331
1765
  `#version 300 es
1332
1766
  precision highp float;
1333
- layout(location = 0) in vec2 aCorner; // (along 0..1, side -1..1)
1334
- layout(location = 1) in vec2 aP0;
1335
- layout(location = 2) in vec2 aP1;
1336
- uniform vec2 uResolution;
1337
- uniform float uWidth;
1338
- uniform float uRound;
1339
- ${TRANSFORM_GLSL}
1340
- out vec2 vPix;
1341
- out vec2 vS0;
1342
- out vec2 vS1;
1767
+ uniform sampler2D uPoints;
1768
+ uniform int uTexW;
1769
+ uniform int uI0;
1770
+ uniform int uI1;
1771
+ uniform int uCols;
1772
+ uniform int uMaxIter; // per-column iteration cap; stride-samples beyond it
1773
+ out vec2 vOut;
1774
+
1775
+ vec2 fetchPoint(int i) {
1776
+ return texelFetch(uPoints, ivec2(i % uTexW, i / uTexW), 0).xy;
1777
+ }
1778
+
1343
1779
  void main() {
1344
- vec2 s0 = (dataToClip(aP0) * 0.5 + 0.5) * uResolution;
1345
- vec2 s1 = (dataToClip(aP1) * 0.5 + 0.5) * uResolution;
1346
- vec2 d = s1 - s0;
1347
- float len = length(d);
1348
- vec2 dir = len > 1e-6 ? d / len : vec2(1.0, 0.0);
1349
- vec2 nrm = vec2(-dir.y, dir.x);
1350
- float hw = uWidth * 0.5 + 1.5; // half width + AA margin
1780
+ int v = gl_VertexID;
1781
+ int last = 2 * uCols + 1;
1782
+ int idx;
1783
+ if (v == 0) {
1784
+ idx = uI0; // left endpoint, so the line reaches the edge
1785
+ } else if (v >= last) {
1786
+ idx = uI1; // right endpoint
1787
+ } else {
1788
+ int col = (v - 1) / 2;
1789
+ int which = (v - 1) - col * 2; // 0 => min-index extreme, 1 => max-index extreme
1790
+ // Float division avoids int32 overflow of visN*col for huge series.
1791
+ float fvisN = float(uI1 - uI0);
1792
+ int lo = uI0 + int(floor(fvisN * float(col) / float(uCols)));
1793
+ int hi = uI0 + int(floor(fvisN * float(col + 1) / float(uCols)));
1794
+ if (hi <= lo) {
1795
+ idx = lo;
1796
+ } else {
1797
+ int stride = 1;
1798
+ int span = hi - lo;
1799
+ if (span > uMaxIter) stride = (span + uMaxIter - 1) / uMaxIter;
1800
+ int iMin = lo, iMax = lo;
1801
+ float yMin = fetchPoint(lo).y, yMax = yMin;
1802
+ for (int i = lo; i < hi; i += stride) {
1803
+ float y = fetchPoint(i).y;
1804
+ if (y < yMin) { yMin = y; iMin = i; }
1805
+ if (y > yMax) { yMax = y; iMax = i; }
1806
+ }
1807
+ // Emit in index order (matches decimateIndices) to preserve envelope shape.
1808
+ idx = (which == 0) ? min(iMin, iMax) : max(iMin, iMax);
1809
+ }
1810
+ }
1811
+ vOut = fetchPoint(idx);
1812
+ gl_Position = vec4(0.0, 0.0, 0.0, 1.0); // rasterizer-discarded
1813
+ }`
1814
+ );
1815
+ var DEC_FRAG = (
1816
+ /* glsl */
1817
+ `#version 300 es
1818
+ precision highp float;
1819
+ out vec4 o;
1820
+ void main() { o = vec4(0.0); }`
1821
+ );
1822
+ var MAX_ITER = 4096;
1823
+ function compile(gl, type, src) {
1824
+ const sh = gl.createShader(type);
1825
+ gl.shaderSource(sh, src);
1826
+ gl.compileShader(sh);
1827
+ if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {
1828
+ const log = gl.getShaderInfoLog(sh);
1829
+ gl.deleteShader(sh);
1830
+ throw new Error(`Decimation shader compile error:
1831
+ ${log}`);
1832
+ }
1833
+ return sh;
1834
+ }
1835
+ var programCache7 = /* @__PURE__ */ new WeakMap();
1836
+ function getDecProgram(gl) {
1837
+ if (programCache7.has(gl)) return programCache7.get(gl);
1838
+ let result = null;
1839
+ try {
1840
+ const vs = compile(gl, gl.VERTEX_SHADER, DEC_VERT);
1841
+ const fs = compile(gl, gl.FRAGMENT_SHADER, DEC_FRAG);
1842
+ const program = gl.createProgram();
1843
+ gl.attachShader(program, vs);
1844
+ gl.attachShader(program, fs);
1845
+ gl.transformFeedbackVaryings(program, ["vOut"], gl.INTERLEAVED_ATTRIBS);
1846
+ gl.linkProgram(program);
1847
+ gl.deleteShader(vs);
1848
+ gl.deleteShader(fs);
1849
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
1850
+ gl.deleteProgram(program);
1851
+ } else {
1852
+ const u = {};
1853
+ for (const name of ["uPoints", "uTexW", "uI0", "uI1", "uCols", "uMaxIter"]) {
1854
+ u[name] = gl.getUniformLocation(program, name);
1855
+ }
1856
+ result = { program, u };
1857
+ }
1858
+ } catch {
1859
+ result = null;
1860
+ }
1861
+ programCache7.set(gl, result);
1862
+ return result;
1863
+ }
1864
+ var GpuDecimator = class {
1865
+ // point capacity currently allocated in the draw buffer
1866
+ constructor(gl) {
1867
+ this.tex = null;
1868
+ this.tf = null;
1869
+ this.emptyVao = null;
1870
+ this.texW = 0;
1871
+ this.decCapacity = -1;
1872
+ this.gl = gl;
1873
+ this.prog = getDecProgram(gl);
1874
+ this.supported = this.prog != null;
1875
+ }
1876
+ /**
1877
+ * (Re)upload the series into the point texture. `data` is interleaved
1878
+ * (x-xRef, y-yRef) float32, exactly as the draw buffer stores it. Returns
1879
+ * false and disables the GPU path if the texture can't be allocated.
1880
+ */
1881
+ setPoints(data, n) {
1882
+ if (!this.supported || n === 0) return this.supported;
1883
+ const gl = this.gl;
1884
+ const maxTex = gl.getParameter(gl.MAX_TEXTURE_SIZE);
1885
+ const texW = Math.min(maxTex, Math.max(1, Math.ceil(Math.sqrt(n))));
1886
+ const texH = Math.ceil(n / texW);
1887
+ if (texH > maxTex) return false;
1888
+ this.texW = texW;
1889
+ const packed = data.length >= texW * texH * 2 ? data : new Float32Array(texW * texH * 2);
1890
+ if (packed !== data) packed.set(data.subarray(0, n * 2));
1891
+ try {
1892
+ if (!this.tex) this.tex = gl.createTexture();
1893
+ gl.bindTexture(gl.TEXTURE_2D, this.tex);
1894
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RG32F, texW, texH, 0, gl.RG, gl.FLOAT, packed);
1895
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
1896
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
1897
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
1898
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
1899
+ gl.bindTexture(gl.TEXTURE_2D, null);
1900
+ } catch {
1901
+ return false;
1902
+ }
1903
+ if (!this.tf) this.tf = gl.createTransformFeedback();
1904
+ if (!this.emptyVao) this.emptyVao = gl.createVertexArray();
1905
+ return true;
1906
+ }
1907
+ /**
1908
+ * Run the reduction for the visible window `[i0, i1]` into `decBuf`, matching
1909
+ * the CPU envelope layout: left endpoint, `cols` min/max pairs, right endpoint.
1910
+ * Returns the emitted point count, or null if the GPU path can't run this call.
1911
+ */
1912
+ run(i0, i1, cols, decBuf) {
1913
+ if (!this.supported || !this.tex || !this.prog) return null;
1914
+ const gl = this.gl;
1915
+ const outCount = 2 * cols + 2;
1916
+ if (this.decCapacity < outCount) {
1917
+ gl.bindBuffer(gl.ARRAY_BUFFER, decBuf);
1918
+ gl.bufferData(gl.ARRAY_BUFFER, outCount * 2 * 4, gl.DYNAMIC_COPY);
1919
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
1920
+ this.decCapacity = outCount;
1921
+ }
1922
+ const { program, u } = this.prog;
1923
+ gl.useProgram(program);
1924
+ gl.activeTexture(gl.TEXTURE0);
1925
+ gl.bindTexture(gl.TEXTURE_2D, this.tex);
1926
+ gl.uniform1i(u.uPoints, 0);
1927
+ gl.uniform1i(u.uTexW, this.texW);
1928
+ gl.uniform1i(u.uI0, i0);
1929
+ gl.uniform1i(u.uI1, i1);
1930
+ gl.uniform1i(u.uCols, cols);
1931
+ gl.uniform1i(u.uMaxIter, MAX_ITER);
1932
+ gl.bindVertexArray(this.emptyVao);
1933
+ gl.enable(gl.RASTERIZER_DISCARD);
1934
+ gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, this.tf);
1935
+ gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, decBuf);
1936
+ gl.beginTransformFeedback(gl.POINTS);
1937
+ gl.drawArrays(gl.POINTS, 0, outCount);
1938
+ gl.endTransformFeedback();
1939
+ gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);
1940
+ gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
1941
+ gl.disable(gl.RASTERIZER_DISCARD);
1942
+ gl.bindVertexArray(null);
1943
+ gl.bindTexture(gl.TEXTURE_2D, null);
1944
+ return outCount;
1945
+ }
1946
+ dispose() {
1947
+ const gl = this.gl;
1948
+ if (this.tex) gl.deleteTexture(this.tex);
1949
+ if (this.tf) gl.deleteTransformFeedback(this.tf);
1950
+ if (this.emptyVao) gl.deleteVertexArray(this.emptyVao);
1951
+ this.tex = null;
1952
+ this.tf = null;
1953
+ this.emptyVao = null;
1954
+ }
1955
+ };
1956
+
1957
+ // src/layers/pick.ts
1958
+ function pickNearest(xs, ys, count, mode, cursorPx, cursorPy, project, gatePx = Infinity) {
1959
+ if (count === 0) return null;
1960
+ let best = -1;
1961
+ let bestDist = Infinity;
1962
+ for (let i = 0; i < count; i++) {
1963
+ const [px, py] = project(xs[i], ys[i]);
1964
+ const dx = px - cursorPx;
1965
+ const dy = py - cursorPy;
1966
+ const d = mode === "x" ? Math.abs(dx) : mode === "y" ? Math.abs(dy) : Math.hypot(dx, dy);
1967
+ if (d < bestDist) {
1968
+ bestDist = d;
1969
+ best = i;
1970
+ }
1971
+ }
1972
+ if (best < 0 || bestDist > gatePx) return null;
1973
+ return { x: xs[best], y: ys[best], index: best };
1974
+ }
1975
+
1976
+ // src/layers/line.ts
1977
+ var GPU_DECIMATE_MIN = 2e5;
1978
+ var VERT7 = (
1979
+ /* glsl */
1980
+ `#version 300 es
1981
+ precision highp float;
1982
+ layout(location = 0) in vec2 aCorner; // (along 0..1, side -1..1)
1983
+ layout(location = 1) in vec2 aP0;
1984
+ layout(location = 2) in vec2 aP1;
1985
+ uniform vec2 uResolution;
1986
+ uniform float uWidth;
1987
+ uniform float uRound;
1988
+ ${TRANSFORM_GLSL}
1989
+ out vec2 vPix;
1990
+ out vec2 vS0;
1991
+ out vec2 vS1;
1992
+ void main() {
1993
+ vec2 s0 = (dataToClip(aP0) * 0.5 + 0.5) * uResolution;
1994
+ vec2 s1 = (dataToClip(aP1) * 0.5 + 0.5) * uResolution;
1995
+ vec2 d = s1 - s0;
1996
+ float len = length(d);
1997
+ vec2 dir = len > 1e-6 ? d / len : vec2(1.0, 0.0);
1998
+ vec2 nrm = vec2(-dir.y, dir.x);
1999
+ float hw = uWidth * 0.5 + 1.5; // half width + AA margin
1351
2000
  float ext = uRound > 0.5 ? hw : 0.0; // extend for round caps
1352
2001
  vec2 endpoint = mix(s0, s1, aCorner.x);
1353
2002
  vec2 outward = (aCorner.x < 0.5 ? -dir : dir) * ext;
@@ -1356,7 +2005,7 @@ void main() {
1356
2005
  gl_Position = vec4((pos / uResolution) * 2.0 - 1.0, 0.0, 1.0);
1357
2006
  }`
1358
2007
  );
1359
- var FRAG7 = (
2008
+ var FRAG8 = (
1360
2009
  /* glsl */
1361
2010
  `#version 300 es
1362
2011
  precision highp float;
@@ -1385,13 +2034,112 @@ void main() {
1385
2034
  outColor = vec4(uColor.rgb * uColor.a, uColor.a) * alpha;
1386
2035
  }`
1387
2036
  );
2037
+ var JOIN_VERT = (
2038
+ /* glsl */
2039
+ `#version 300 es
2040
+ precision highp float;
2041
+ layout(location = 0) in vec2 aPrev;
2042
+ layout(location = 1) in vec2 aP0;
2043
+ layout(location = 2) in vec2 aNext;
2044
+ uniform vec2 uResolution;
2045
+ uniform float uWidth;
2046
+ uniform float uMiter; // >0.5 => miter, else bevel
2047
+ uniform float uMiterLimit;
2048
+ ${TRANSFORM_GLSL}
2049
+ out vec2 vPix;
2050
+ flat out vec2 vE0;
2051
+ flat out vec2 vE1;
2052
+ flat out vec2 vInner;
2053
+ void main() {
2054
+ vec2 sp = (dataToClip(aPrev) * 0.5 + 0.5) * uResolution;
2055
+ vec2 s0 = (dataToClip(aP0) * 0.5 + 0.5) * uResolution;
2056
+ vec2 sn = (dataToClip(aNext) * 0.5 + 0.5) * uResolution;
2057
+ vec2 din = s0 - sp;
2058
+ vec2 dout = sn - s0;
2059
+ float inl = length(din), outl = length(dout);
2060
+ vec2 inN = vec2(0.0), outN = vec2(0.0);
2061
+ float crs = 0.0;
2062
+ bool ok = inl > 1e-6 && outl > 1e-6;
2063
+ if (ok) {
2064
+ din /= inl; dout /= outl;
2065
+ inN = vec2(-din.y, din.x);
2066
+ outN = vec2(-dout.y, dout.x);
2067
+ crs = din.x * dout.y - din.y * dout.x;
2068
+ ok = abs(crs) > 1e-6; // collinear => no notch
2069
+ }
2070
+ if (!ok) { // degenerate: cull the wedge
2071
+ vE0 = vec2(0.0); vE1 = vec2(0.0); vInner = vec2(0.0); vPix = vec2(0.0);
2072
+ gl_Position = vec4(2.0, 2.0, 2.0, 1.0);
2073
+ return;
2074
+ }
2075
+ float hw = uWidth * 0.5;
2076
+ float outerSign = crs > 0.0 ? -1.0 : 1.0;
2077
+ vec2 A = s0 + inN * hw * outerSign;
2078
+ vec2 B = s0 + outN * hw * outerSign;
2079
+ vec2 apex = 0.5 * (A + B); // bevel midpoint
2080
+ if (uMiter > 0.5) {
2081
+ vec2 mN = inN + outN;
2082
+ float ml = length(mN);
2083
+ if (ml > 1e-6) {
2084
+ mN /= ml;
2085
+ float denom = dot(mN, outN);
2086
+ if (denom > 1e-3) {
2087
+ float miterLen = 1.0 / denom;
2088
+ if (miterLen <= uMiterLimit) apex = s0 + mN * outerSign * hw * miterLen;
2089
+ }
2090
+ }
2091
+ }
2092
+ int vid = gl_VertexID;
2093
+ vec2 pos;
2094
+ if (vid == 0 || vid == 3) pos = s0;
2095
+ else if (vid == 1) pos = A;
2096
+ else if (vid == 2 || vid == 4) pos = apex;
2097
+ else pos = B; // vid == 5
2098
+ if (vid < 3) { vE0 = A; vE1 = apex; } // triangle [s0, A, apex]
2099
+ else { vE0 = apex; vE1 = B; } // triangle [s0, apex, B]
2100
+ vInner = s0;
2101
+ vPix = pos;
2102
+ gl_Position = vec4((pos / uResolution) * 2.0 - 1.0, 0.0, 1.0);
2103
+ }`
2104
+ );
2105
+ var JOIN_FRAG = (
2106
+ /* glsl */
2107
+ `#version 300 es
2108
+ precision highp float;
2109
+ in vec2 vPix;
2110
+ flat in vec2 vE0;
2111
+ flat in vec2 vE1;
2112
+ flat in vec2 vInner;
2113
+ uniform vec4 uColor;
2114
+ out vec4 outColor;
2115
+ void main() {
2116
+ vec2 e = vE1 - vE0;
2117
+ float el = length(e);
2118
+ if (el < 1e-6) discard;
2119
+ vec2 n = vec2(-e.y, e.x) / el;
2120
+ float sideInner = dot(vInner - vE0, n) >= 0.0 ? 1.0 : -1.0;
2121
+ float d = dot(vPix - vE0, n) * sideInner; // >0 inside the outer edge
2122
+ float alpha = clamp(d + 0.5, 0.0, 1.0);
2123
+ if (alpha <= 0.0) discard;
2124
+ outColor = vec4(uColor.rgb * uColor.a, uColor.a) * alpha;
2125
+ }`
2126
+ );
1388
2127
  var CORNERS2 = new Float32Array([0, -1, 1, -1, 1, 1, 0, -1, 1, 1, 0, 1]);
1389
- var programCache7 = /* @__PURE__ */ new WeakMap();
2128
+ var programCache8 = /* @__PURE__ */ new WeakMap();
1390
2129
  function getProgram7(gl) {
1391
- let p = programCache7.get(gl);
2130
+ let p = programCache8.get(gl);
1392
2131
  if (!p) {
1393
- p = createProgram(gl, VERT7, FRAG7);
1394
- programCache7.set(gl, p);
2132
+ p = createProgram(gl, VERT7, FRAG8);
2133
+ programCache8.set(gl, p);
2134
+ }
2135
+ return p;
2136
+ }
2137
+ var joinProgramCache = /* @__PURE__ */ new WeakMap();
2138
+ function getJoinProgram(gl) {
2139
+ let p = joinProgramCache.get(gl);
2140
+ if (!p) {
2141
+ p = createProgram(gl, JOIN_VERT, JOIN_FRAG);
2142
+ joinProgramCache.set(gl, p);
1395
2143
  }
1396
2144
  return p;
1397
2145
  }
@@ -1424,20 +2172,24 @@ function upperBound(a, v) {
1424
2172
  }
1425
2173
  return lo;
1426
2174
  }
1427
- var counter7 = 0;
2175
+ var counter9 = 0;
1428
2176
  var LineLayer = class {
1429
2177
  constructor(gl, opts) {
2178
+ this.gpuDec = null;
1430
2179
  this.xRef = 0;
1431
2180
  this.yRef = 0;
1432
2181
  this.xBounds = [0, 0];
1433
2182
  this.yBounds = [0, 0];
1434
2183
  this.decKey = "";
1435
2184
  this.decSegments = 0;
1436
- this.id = `line-${counter7++}`;
2185
+ this.id = `line-${counter9++}`;
1437
2186
  this.gl = gl;
1438
2187
  this.program = getProgram7(gl);
2188
+ this.joinProgram = getJoinProgram(gl);
1439
2189
  this.width = opts.width ?? 1.5;
1440
- this.round = (opts.join ?? "round") === "round";
2190
+ this.joinStyle = opts.join ?? "round";
2191
+ this.round = this.joinStyle === "round";
2192
+ this.miterLimit = opts.miterLimit ?? 4;
1441
2193
  this.decimateOn = opts.decimate !== false;
1442
2194
  this.step = opts.step;
1443
2195
  const colorInput = opts.color ?? "#3b82f6";
@@ -1480,12 +2232,16 @@ var LineLayer = class {
1480
2232
  this.decBuf = gl.createBuffer();
1481
2233
  this.fullVao = gl.createVertexArray();
1482
2234
  this.decVao = gl.createVertexArray();
2235
+ this.joinFullVao = gl.createVertexArray();
2236
+ this.joinDecVao = gl.createVertexArray();
1483
2237
  gl.bindBuffer(gl.ARRAY_BUFFER, this.cornerBuf);
1484
2238
  gl.bufferData(gl.ARRAY_BUFFER, CORNERS2, gl.STATIC_DRAW);
1485
2239
  gl.bindBuffer(gl.ARRAY_BUFFER, this.posBuf);
1486
2240
  gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
1487
2241
  this.configureVao(this.fullVao, this.posBuf);
1488
2242
  this.configureVao(this.decVao, this.decBuf);
2243
+ this.configureJoinVao(this.joinFullVao, this.posBuf);
2244
+ this.configureJoinVao(this.joinDecVao, this.decBuf);
1489
2245
  this.uniforms = uniformLocations(gl, this.program, [
1490
2246
  ...TRANSFORM_UNIFORMS,
1491
2247
  "uColor",
@@ -1493,6 +2249,35 @@ var LineLayer = class {
1493
2249
  "uWidth",
1494
2250
  "uRound"
1495
2251
  ]);
2252
+ this.joinUniforms = uniformLocations(gl, this.joinProgram, [
2253
+ ...TRANSFORM_UNIFORMS,
2254
+ "uColor",
2255
+ "uResolution",
2256
+ "uWidth",
2257
+ "uMiter",
2258
+ "uMiterLimit"
2259
+ ]);
2260
+ this.syncGpu(data, n);
2261
+ }
2262
+ // Keep the GPU decimation texture in sync for large series; disable it (fall
2263
+ // back to CPU decimation) if the context can't support the path.
2264
+ syncGpu(data, n) {
2265
+ if (!this.decimateOn || n < GPU_DECIMATE_MIN) {
2266
+ this.disposeGpu();
2267
+ return;
2268
+ }
2269
+ if (!this.gpuDec) {
2270
+ const dec = new GpuDecimator(this.gl);
2271
+ if (!dec.supported) return;
2272
+ this.gpuDec = dec;
2273
+ }
2274
+ if (!this.gpuDec.setPoints(data, n)) this.disposeGpu();
2275
+ }
2276
+ disposeGpu() {
2277
+ if (this.gpuDec) {
2278
+ this.gpuDec.dispose();
2279
+ this.gpuDec = null;
2280
+ }
1496
2281
  }
1497
2282
  configureVao(vao, pointBuf) {
1498
2283
  const gl = this.gl;
@@ -1509,21 +2294,25 @@ var LineLayer = class {
1509
2294
  gl.vertexAttribDivisor(2, 1);
1510
2295
  gl.bindVertexArray(null);
1511
2296
  }
2297
+ // Join instances read three consecutive points (prev, p0, next) from the same
2298
+ // buffer via overlapping attribute offsets; instance i covers vertex i+1.
2299
+ configureJoinVao(vao, pointBuf) {
2300
+ const gl = this.gl;
2301
+ gl.bindVertexArray(vao);
2302
+ gl.bindBuffer(gl.ARRAY_BUFFER, pointBuf);
2303
+ for (let loc = 0; loc < 3; loc++) {
2304
+ gl.enableVertexAttribArray(loc);
2305
+ gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 8, loc * 8);
2306
+ gl.vertexAttribDivisor(loc, 1);
2307
+ }
2308
+ gl.bindVertexArray(null);
2309
+ }
1512
2310
  bounds() {
1513
2311
  if (this.count === 0) return null;
1514
2312
  return { x: this.xBounds, y: this.yBounds };
1515
2313
  }
1516
- nearestByX(x) {
1517
- if (this.count === 0) return null;
1518
- let best = 0, bestDist = Infinity;
1519
- for (let i = 0; i < this.count; i++) {
1520
- const d = Math.abs(this.xs[i] - x);
1521
- if (d < bestDist) {
1522
- bestDist = d;
1523
- best = i;
1524
- }
1525
- }
1526
- return { x: this.xs[best], y: this.ys[best], index: best };
2314
+ pick(mode, cursorPx, cursorPy, project) {
2315
+ return pickNearest(this.xs, this.ys, this.count, mode, cursorPx, cursorPy, project);
1527
2316
  }
1528
2317
  /** Replace the series data and re-upload the GPU buffer (for streaming). */
1529
2318
  setData(x, y) {
@@ -1560,6 +2349,7 @@ var LineLayer = class {
1560
2349
  this.decKey = "";
1561
2350
  this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.posBuf);
1562
2351
  this.gl.bufferData(this.gl.ARRAY_BUFFER, data, this.gl.DYNAMIC_DRAW);
2352
+ this.syncGpu(data, n);
1563
2353
  }
1564
2354
  /**
1565
2355
  * Rebuild the min/max-decimated buffer for the visible x-window if it changed.
@@ -1576,41 +2366,38 @@ var LineLayer = class {
1576
2366
  if (visN <= target * 1.5) return null;
1577
2367
  const key = `${i0}:${i1}:${target}`;
1578
2368
  if (key === this.decKey) return this.decSegments;
1579
- this.decKey = key;
1580
- const out = [];
1581
- const push = (i) => out.push(this.xs[i] - this.xRef, this.ys[i] - this.yRef);
1582
- push(i0);
1583
- for (let b = 0; b < cols; b++) {
1584
- const lo = i0 + Math.floor(visN * b / cols);
1585
- const hi = i0 + Math.floor(visN * (b + 1) / cols);
1586
- if (hi <= lo) continue;
1587
- let iMin = lo, iMax = lo;
1588
- for (let i = lo; i < hi; i++) {
1589
- if (this.ys[i] < this.ys[iMin]) iMin = i;
1590
- if (this.ys[i] > this.ys[iMax]) iMax = i;
1591
- }
1592
- if (iMin < iMax) {
1593
- push(iMin);
1594
- push(iMax);
1595
- } else {
1596
- push(iMax);
1597
- push(iMin);
2369
+ if (this.gpuDec) {
2370
+ const outCount = this.gpuDec.run(i0, i1, cols, this.decBuf);
2371
+ if (outCount != null) {
2372
+ this.decKey = key;
2373
+ this.decSegments = outCount - 1;
2374
+ return this.decSegments;
1598
2375
  }
2376
+ this.disposeGpu();
2377
+ }
2378
+ this.decKey = key;
2379
+ const indices = decimateIndices(this.ys, i0, i1, cols);
2380
+ const out = new Float32Array(indices.length * 2);
2381
+ for (let k = 0; k < indices.length; k++) {
2382
+ const i = indices[k];
2383
+ out[k * 2] = this.xs[i] - this.xRef;
2384
+ out[k * 2 + 1] = this.ys[i] - this.yRef;
1599
2385
  }
1600
- push(i1);
1601
2386
  const gl = this.gl;
1602
2387
  gl.bindBuffer(gl.ARRAY_BUFFER, this.decBuf);
1603
- gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(out), gl.DYNAMIC_DRAW);
1604
- this.decSegments = out.length / 2 - 1;
2388
+ gl.bufferData(gl.ARRAY_BUFFER, out, gl.DYNAMIC_DRAW);
2389
+ this.decSegments = indices.length - 1;
1605
2390
  return this.decSegments;
1606
2391
  }
1607
2392
  draw(state) {
1608
2393
  if (this.count < 2) return;
1609
2394
  const gl = state.gl;
1610
2395
  const decSegs = this.decimate(state.x, Math.max(1, Math.round(state.pixelWidth)));
1611
- const vao = decSegs != null ? this.decVao : this.fullVao;
1612
- const segments = decSegs != null ? decSegs : this.count - 1;
2396
+ const decimated = decSegs != null;
2397
+ const vao = decimated ? this.decVao : this.fullVao;
2398
+ const segments = decimated ? decSegs : this.count - 1;
1613
2399
  if (segments < 1) return;
2400
+ const points = segments + 1;
1614
2401
  gl.useProgram(this.program);
1615
2402
  setTransformUniforms(gl, this.uniforms, state.x, state.y, this.xRef, this.yRef);
1616
2403
  gl.uniform4f(this.uniforms.uColor, this.color[0], this.color[1], this.color[2], this.color[3]);
@@ -1620,17 +2407,262 @@ var LineLayer = class {
1620
2407
  gl.bindVertexArray(vao);
1621
2408
  gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, segments);
1622
2409
  gl.bindVertexArray(null);
2410
+ if ((this.joinStyle === "miter" || this.joinStyle === "bevel") && points >= 3) {
2411
+ gl.useProgram(this.joinProgram);
2412
+ setTransformUniforms(gl, this.joinUniforms, state.x, state.y, this.xRef, this.yRef);
2413
+ gl.uniform4f(this.joinUniforms.uColor, this.color[0], this.color[1], this.color[2], this.color[3]);
2414
+ gl.uniform2f(this.joinUniforms.uResolution, state.pixelWidth, state.pixelHeight);
2415
+ gl.uniform1f(this.joinUniforms.uWidth, this.width * state.dpr);
2416
+ gl.uniform1f(this.joinUniforms.uMiter, this.joinStyle === "miter" ? 1 : 0);
2417
+ gl.uniform1f(this.joinUniforms.uMiterLimit, this.miterLimit);
2418
+ gl.bindVertexArray(decimated ? this.joinDecVao : this.joinFullVao);
2419
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, points - 2);
2420
+ gl.bindVertexArray(null);
2421
+ }
1623
2422
  }
1624
2423
  dispose() {
1625
2424
  const gl = this.gl;
2425
+ this.disposeGpu();
1626
2426
  gl.deleteVertexArray(this.fullVao);
1627
2427
  gl.deleteVertexArray(this.decVao);
2428
+ gl.deleteVertexArray(this.joinFullVao);
2429
+ gl.deleteVertexArray(this.joinDecVao);
1628
2430
  gl.deleteBuffer(this.cornerBuf);
1629
2431
  gl.deleteBuffer(this.posBuf);
1630
2432
  gl.deleteBuffer(this.decBuf);
1631
2433
  }
1632
2434
  };
1633
2435
 
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;
2444
+ uniform vec2 uResolution;
2445
+ uniform float uWidth;
2446
+ ${TRANSFORM_GLSL}
2447
+ out vec4 vColor;
2448
+ void main() {
2449
+ vec2 s0 = (dataToClip(aArrow.xy) * 0.5 + 0.5) * uResolution;
2450
+ vec2 s1 = (dataToClip(aArrow.zw) * 0.5 + 0.5) * uResolution;
2451
+ vec2 d = s1 - s0;
2452
+ float len = length(d);
2453
+ vec2 dir = len > 1e-6 ? d / len : vec2(1.0, 0.0);
2454
+ vec2 nrm = vec2(-dir.y, dir.x);
2455
+ vec2 pos = mix(s0, s1, aCorner.x) + nrm * (aCorner.y * uWidth * 0.5);
2456
+ vColor = aColor;
2457
+ gl_Position = vec4((pos / uResolution) * 2.0 - 1.0, 0.0, 1.0);
2458
+ }`
2459
+ );
2460
+ var HEAD_VERT = (
2461
+ /* glsl */
2462
+ `#version 300 es
2463
+ precision highp float;
2464
+ layout(location = 1) in vec4 aArrow; // (bx,by,tx,ty) offset data space
2465
+ layout(location = 2) in vec4 aColor;
2466
+ uniform vec2 uResolution;
2467
+ uniform float uHeadSize;
2468
+ ${TRANSFORM_GLSL}
2469
+ out vec4 vColor;
2470
+ void main() {
2471
+ vec2 s0 = (dataToClip(aArrow.xy) * 0.5 + 0.5) * uResolution;
2472
+ vec2 s1 = (dataToClip(aArrow.zw) * 0.5 + 0.5) * uResolution;
2473
+ vec2 d = s1 - s0;
2474
+ float len = length(d);
2475
+ vec2 dir = len > 1e-6 ? d / len : vec2(1.0, 0.0);
2476
+ vec2 nrm = vec2(-dir.y, dir.x);
2477
+ float w = uHeadSize * 0.6;
2478
+ vec2 pos;
2479
+ if (gl_VertexID == 0) pos = s1; // tip
2480
+ else if (gl_VertexID == 1) pos = s1 - dir * uHeadSize + nrm * w; // back-left
2481
+ else pos = s1 - dir * uHeadSize - nrm * w; // back-right
2482
+ vColor = aColor;
2483
+ gl_Position = vec4((pos / uResolution) * 2.0 - 1.0, 0.0, 1.0);
2484
+ }`
2485
+ );
2486
+ var FRAG9 = (
2487
+ /* glsl */
2488
+ `#version 300 es
2489
+ precision highp float;
2490
+ in vec4 vColor;
2491
+ uniform vec4 uColor;
2492
+ uniform float uUseVertexColor;
2493
+ out vec4 outColor;
2494
+ void main() {
2495
+ vec4 c = uUseVertexColor > 0.5 ? vColor : uColor;
2496
+ outColor = vec4(c.rgb * c.a, c.a);
2497
+ }`
2498
+ );
2499
+ var SEG_CORNERS3 = new Float32Array([0, -1, 1, -1, 1, 1, 0, -1, 1, 1, 0, 1]);
2500
+ var cache3 = /* @__PURE__ */ new WeakMap();
2501
+ function programs3(gl) {
2502
+ let p = cache3.get(gl);
2503
+ if (!p) {
2504
+ p = { shaft: createProgram(gl, SHAFT_VERT, FRAG9), head: createProgram(gl, HEAD_VERT, FRAG9) };
2505
+ cache3.set(gl, p);
2506
+ }
2507
+ return p;
2508
+ }
2509
+ var counter10 = 0;
2510
+ var QuiverLayer = class {
2511
+ constructor(gl, opts) {
2512
+ this.buffers = [];
2513
+ this.xRef = 0;
2514
+ this.yRef = 0;
2515
+ this.xBounds = [0, 0];
2516
+ this.yBounds = [0, 0];
2517
+ this.id = `quiver-${counter10++}`;
2518
+ this.gl = gl;
2519
+ this.progs = programs3(gl);
2520
+ const colorInput = opts.color ?? "#3b82f6";
2521
+ this.color = Array.isArray(colorInput) ? colorInput : parseColor(colorInput);
2522
+ this.colorCss = typeof colorInput === "string" ? colorInput : toColorCss(this.color);
2523
+ this.name = opts.name ?? this.id;
2524
+ this.yAxis = opts.yAxis ?? "y";
2525
+ this.width = opts.width ?? 1.5;
2526
+ this.headSize = opts.headSize ?? 9;
2527
+ this.useVertexColor = opts.colorBy != null;
2528
+ const n = Math.min(opts.x.length, opts.y.length, opts.u.length, opts.v.length);
2529
+ this.count = n;
2530
+ this.xRef = n > 0 ? opts.x[0] : 0;
2531
+ this.yRef = n > 0 ? opts.y[0] : 0;
2532
+ let maxMag = 0;
2533
+ for (let i = 0; i < n; i++) maxMag = Math.max(maxMag, Math.hypot(opts.u[i], opts.v[i]));
2534
+ let scale = opts.scale;
2535
+ if (scale == null) {
2536
+ let minX2 = Infinity, maxX2 = -Infinity, minY2 = Infinity, maxY2 = -Infinity;
2537
+ for (let i = 0; i < n; i++) {
2538
+ minX2 = Math.min(minX2, opts.x[i]);
2539
+ maxX2 = Math.max(maxX2, opts.x[i]);
2540
+ minY2 = Math.min(minY2, opts.y[i]);
2541
+ maxY2 = Math.max(maxY2, opts.y[i]);
2542
+ }
2543
+ const diag = Math.hypot(maxX2 - minX2, maxY2 - minY2) || 1;
2544
+ const cell = diag / Math.max(1, Math.sqrt(n));
2545
+ scale = maxMag > 0 ? 0.9 * cell / maxMag : 1;
2546
+ }
2547
+ const arrows = new Float32Array(n * 4);
2548
+ const colors = new Float32Array(n * 4);
2549
+ const cmap = colormap(opts.colorBy?.colormap ?? "viridis");
2550
+ const vals = opts.colorBy?.values;
2551
+ let lo = opts.colorBy?.domain?.[0] ?? Infinity;
2552
+ let hi = opts.colorBy?.domain?.[1] ?? -Infinity;
2553
+ if (this.useVertexColor && !opts.colorBy?.domain) {
2554
+ for (let i = 0; i < n; i++) {
2555
+ const v = vals ? vals[i] : Math.hypot(opts.u[i], opts.v[i]);
2556
+ lo = Math.min(lo, v);
2557
+ hi = Math.max(hi, v);
2558
+ }
2559
+ }
2560
+ const span = hi - lo || 1;
2561
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
2562
+ for (let i = 0; i < n; i++) {
2563
+ const x = opts.x[i], y = opts.y[i];
2564
+ const tx = x + opts.u[i] * scale, ty = y + opts.v[i] * scale;
2565
+ arrows[i * 4] = x - this.xRef;
2566
+ arrows[i * 4 + 1] = y - this.yRef;
2567
+ arrows[i * 4 + 2] = tx - this.xRef;
2568
+ arrows[i * 4 + 3] = ty - this.yRef;
2569
+ if (this.useVertexColor) {
2570
+ const v = vals ? vals[i] : Math.hypot(opts.u[i], opts.v[i]);
2571
+ const [r, g, b] = cmap((v - lo) / span);
2572
+ colors[i * 4] = r;
2573
+ colors[i * 4 + 1] = g;
2574
+ colors[i * 4 + 2] = b;
2575
+ colors[i * 4 + 3] = 1;
2576
+ }
2577
+ minX = Math.min(minX, x, tx);
2578
+ maxX = Math.max(maxX, x, tx);
2579
+ minY = Math.min(minY, y, ty);
2580
+ maxY = Math.max(maxY, y, ty);
2581
+ }
2582
+ this.xBounds = [minX, maxX];
2583
+ this.yBounds = [minY, maxY];
2584
+ const cornerBuf = gl.createBuffer();
2585
+ gl.bindBuffer(gl.ARRAY_BUFFER, cornerBuf);
2586
+ gl.bufferData(gl.ARRAY_BUFFER, SEG_CORNERS3, gl.STATIC_DRAW);
2587
+ const arrowBuf = gl.createBuffer();
2588
+ gl.bindBuffer(gl.ARRAY_BUFFER, arrowBuf);
2589
+ gl.bufferData(gl.ARRAY_BUFFER, arrows, gl.STATIC_DRAW);
2590
+ const colorBuf = gl.createBuffer();
2591
+ gl.bindBuffer(gl.ARRAY_BUFFER, colorBuf);
2592
+ gl.bufferData(gl.ARRAY_BUFFER, colors, gl.STATIC_DRAW);
2593
+ this.buffers = [cornerBuf, arrowBuf, colorBuf];
2594
+ this.shaftVao = gl.createVertexArray();
2595
+ gl.bindVertexArray(this.shaftVao);
2596
+ gl.bindBuffer(gl.ARRAY_BUFFER, cornerBuf);
2597
+ gl.enableVertexAttribArray(0);
2598
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
2599
+ this.bindInstanceAttribs(arrowBuf, colorBuf);
2600
+ gl.bindVertexArray(null);
2601
+ this.headVao = gl.createVertexArray();
2602
+ gl.bindVertexArray(this.headVao);
2603
+ this.bindInstanceAttribs(arrowBuf, colorBuf);
2604
+ gl.bindVertexArray(null);
2605
+ this.uShaft = uniformLocations(gl, this.progs.shaft, [
2606
+ ...TRANSFORM_UNIFORMS,
2607
+ "uColor",
2608
+ "uResolution",
2609
+ "uWidth",
2610
+ "uUseVertexColor"
2611
+ ]);
2612
+ this.uHead = uniformLocations(gl, this.progs.head, [
2613
+ ...TRANSFORM_UNIFORMS,
2614
+ "uColor",
2615
+ "uResolution",
2616
+ "uHeadSize",
2617
+ "uUseVertexColor"
2618
+ ]);
2619
+ }
2620
+ bindInstanceAttribs(arrowBuf, colorBuf) {
2621
+ const gl = this.gl;
2622
+ gl.bindBuffer(gl.ARRAY_BUFFER, arrowBuf);
2623
+ gl.enableVertexAttribArray(1);
2624
+ gl.vertexAttribPointer(1, 4, gl.FLOAT, false, 0, 0);
2625
+ gl.vertexAttribDivisor(1, 1);
2626
+ gl.bindBuffer(gl.ARRAY_BUFFER, colorBuf);
2627
+ gl.enableVertexAttribArray(2);
2628
+ gl.vertexAttribPointer(2, 4, gl.FLOAT, false, 0, 0);
2629
+ gl.vertexAttribDivisor(2, 1);
2630
+ }
2631
+ bounds() {
2632
+ if (this.count === 0) return null;
2633
+ return { x: this.xBounds, y: this.yBounds };
2634
+ }
2635
+ draw(state) {
2636
+ if (this.count === 0) return;
2637
+ const gl = state.gl;
2638
+ const [r, g, b, a] = this.color;
2639
+ const uvc = this.useVertexColor ? 1 : 0;
2640
+ gl.useProgram(this.progs.shaft);
2641
+ setTransformUniforms(gl, this.uShaft, state.x, state.y, this.xRef, this.yRef);
2642
+ gl.uniform4f(this.uShaft.uColor, r, g, b, a);
2643
+ gl.uniform2f(this.uShaft.uResolution, state.pixelWidth, state.pixelHeight);
2644
+ gl.uniform1f(this.uShaft.uWidth, this.width * state.dpr);
2645
+ gl.uniform1f(this.uShaft.uUseVertexColor, uvc);
2646
+ gl.bindVertexArray(this.shaftVao);
2647
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this.count);
2648
+ gl.bindVertexArray(null);
2649
+ gl.useProgram(this.progs.head);
2650
+ setTransformUniforms(gl, this.uHead, state.x, state.y, this.xRef, this.yRef);
2651
+ gl.uniform4f(this.uHead.uColor, r, g, b, a);
2652
+ gl.uniform2f(this.uHead.uResolution, state.pixelWidth, state.pixelHeight);
2653
+ gl.uniform1f(this.uHead.uHeadSize, this.headSize * state.dpr);
2654
+ gl.uniform1f(this.uHead.uUseVertexColor, uvc);
2655
+ gl.bindVertexArray(this.headVao);
2656
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 3, this.count);
2657
+ gl.bindVertexArray(null);
2658
+ }
2659
+ dispose() {
2660
+ this.gl.deleteVertexArray(this.shaftVao);
2661
+ this.gl.deleteVertexArray(this.headVao);
2662
+ for (const b of this.buffers) this.gl.deleteBuffer(b);
2663
+ }
2664
+ };
2665
+
1634
2666
  // src/layers/scatter.ts
1635
2667
  var VERT8 = (
1636
2668
  /* glsl */
@@ -1652,7 +2684,7 @@ void main() {
1652
2684
  gl_Position = vec4((pos / uResolution) * 2.0 - 1.0, 0.0, 1.0);
1653
2685
  }`
1654
2686
  );
1655
- var FRAG8 = (
2687
+ var FRAG10 = (
1656
2688
  /* glsl */
1657
2689
  `#version 300 es
1658
2690
  precision highp float;
@@ -1670,16 +2702,16 @@ void main() {
1670
2702
  }`
1671
2703
  );
1672
2704
  var CORNERS3 = new Float32Array([-1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1]);
1673
- var programCache8 = /* @__PURE__ */ new WeakMap();
2705
+ var programCache9 = /* @__PURE__ */ new WeakMap();
1674
2706
  function getProgram8(gl) {
1675
- let p = programCache8.get(gl);
2707
+ let p = programCache9.get(gl);
1676
2708
  if (!p) {
1677
- p = createProgram(gl, VERT8, FRAG8);
1678
- programCache8.set(gl, p);
2709
+ p = createProgram(gl, VERT8, FRAG10);
2710
+ programCache9.set(gl, p);
1679
2711
  }
1680
2712
  return p;
1681
2713
  }
1682
- var counter8 = 0;
2714
+ var counter11 = 0;
1683
2715
  var ScatterLayer = class {
1684
2716
  constructor(gl, opts) {
1685
2717
  this.buffers = [];
@@ -1687,7 +2719,7 @@ var ScatterLayer = class {
1687
2719
  this.yRef = 0;
1688
2720
  this.xBounds = [0, 0];
1689
2721
  this.yBounds = [0, 0];
1690
- this.id = `scatter-${counter8++}`;
2722
+ this.id = `scatter-${counter11++}`;
1691
2723
  this.gl = gl;
1692
2724
  this.program = getProgram8(gl);
1693
2725
  this.size = opts.size ?? 5;
@@ -1697,6 +2729,7 @@ var ScatterLayer = class {
1697
2729
  this.name = opts.name ?? this.id;
1698
2730
  this.yAxis = opts.yAxis ?? "y";
1699
2731
  this.useVertexColor = opts.colorBy != null;
2732
+ this.labels = opts.labels;
1700
2733
  const n = Math.min(opts.x.length, opts.y.length);
1701
2734
  this.count = n;
1702
2735
  this.xs = new Float64Array(n);
@@ -1774,17 +2807,14 @@ var ScatterLayer = class {
1774
2807
  if (this.count === 0) return null;
1775
2808
  return { x: this.xBounds, y: this.yBounds };
1776
2809
  }
1777
- nearestByX(x) {
1778
- if (this.count === 0) return null;
1779
- let best = 0, bestDist = Infinity;
1780
- for (let i = 0; i < this.count; i++) {
1781
- const d = Math.abs(this.xs[i] - x);
1782
- if (d < bestDist) {
1783
- bestDist = d;
1784
- best = i;
1785
- }
1786
- }
1787
- return { x: this.xs[best], y: this.ys[best], index: best };
2810
+ pick(mode, cursorPx, cursorPy, project) {
2811
+ const gatePx = this.size / 2 + 4;
2812
+ return pickNearest(this.xs, this.ys, this.count, mode, cursorPx, cursorPy, project, gatePx);
2813
+ }
2814
+ /** User-supplied text for a point, shown when it is clicked. */
2815
+ infoAt(index) {
2816
+ const label = this.labels?.[index];
2817
+ return label != null ? [label] : null;
1788
2818
  }
1789
2819
  /** Replace point positions and re-upload (for streaming). Keeps uniform color. */
1790
2820
  setData(x, y) {
@@ -1816,18 +2846,207 @@ var ScatterLayer = class {
1816
2846
  draw(state) {
1817
2847
  if (this.count === 0) return;
1818
2848
  const gl = state.gl;
1819
- gl.useProgram(this.program);
1820
- setTransformUniforms(gl, this.uniforms, state.x, state.y, this.xRef, this.yRef);
1821
- gl.uniform4f(this.uniforms.uColor, this.color[0], this.color[1], this.color[2], this.color[3]);
1822
- gl.uniform2f(this.uniforms.uResolution, state.pixelWidth, state.pixelHeight);
1823
- gl.uniform1f(this.uniforms.uSize, this.size / 2 * state.dpr);
1824
- gl.uniform1f(this.uniforms.uUseVertexColor, this.useVertexColor ? 1 : 0);
1825
- gl.bindVertexArray(this.vao);
2849
+ gl.useProgram(this.program);
2850
+ setTransformUniforms(gl, this.uniforms, state.x, state.y, this.xRef, this.yRef);
2851
+ gl.uniform4f(this.uniforms.uColor, this.color[0], this.color[1], this.color[2], this.color[3]);
2852
+ gl.uniform2f(this.uniforms.uResolution, state.pixelWidth, state.pixelHeight);
2853
+ gl.uniform1f(this.uniforms.uSize, this.size / 2 * state.dpr);
2854
+ gl.uniform1f(this.uniforms.uUseVertexColor, this.useVertexColor ? 1 : 0);
2855
+ gl.bindVertexArray(this.vao);
2856
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this.count);
2857
+ gl.bindVertexArray(null);
2858
+ }
2859
+ dispose() {
2860
+ this.gl.deleteVertexArray(this.vao);
2861
+ for (const b of this.buffers) this.gl.deleteBuffer(b);
2862
+ }
2863
+ };
2864
+
2865
+ // src/layers/stem.ts
2866
+ var STEM_VERT = (
2867
+ /* glsl */
2868
+ `#version 300 es
2869
+ precision highp float;
2870
+ layout(location = 0) in vec2 aCorner; // (along 0..1, side -1..1)
2871
+ layout(location = 1) in vec4 aSeg; // (x0,y0,x1,y1) offset data space
2872
+ uniform vec2 uResolution;
2873
+ uniform float uWidth;
2874
+ ${TRANSFORM_GLSL}
2875
+ void main() {
2876
+ vec2 s0 = (dataToClip(aSeg.xy) * 0.5 + 0.5) * uResolution;
2877
+ vec2 s1 = (dataToClip(aSeg.zw) * 0.5 + 0.5) * uResolution;
2878
+ vec2 d = s1 - s0;
2879
+ float len = length(d);
2880
+ vec2 dir = len > 1e-6 ? d / len : vec2(1.0, 0.0);
2881
+ vec2 nrm = vec2(-dir.y, dir.x);
2882
+ vec2 pos = mix(s0, s1, aCorner.x) + nrm * (aCorner.y * uWidth * 0.5);
2883
+ gl_Position = vec4((pos / uResolution) * 2.0 - 1.0, 0.0, 1.0);
2884
+ }`
2885
+ );
2886
+ var MARKER_VERT = (
2887
+ /* glsl */
2888
+ `#version 300 es
2889
+ precision highp float;
2890
+ layout(location = 0) in vec2 aCorner; // unit quad [-1,1]^2
2891
+ layout(location = 1) in vec2 aPos; // tip point (offset data space)
2892
+ uniform vec2 uResolution;
2893
+ uniform float uSize; // radius in device px
2894
+ ${TRANSFORM_GLSL}
2895
+ out vec2 vLocal;
2896
+ void main() {
2897
+ vec2 c = (dataToClip(aPos) * 0.5 + 0.5) * uResolution;
2898
+ vLocal = aCorner;
2899
+ vec2 pos = c + aCorner * uSize;
2900
+ gl_Position = vec4((pos / uResolution) * 2.0 - 1.0, 0.0, 1.0);
2901
+ }`
2902
+ );
2903
+ var SOLID_FRAG2 = (
2904
+ /* glsl */
2905
+ `#version 300 es
2906
+ precision highp float;
2907
+ uniform vec4 uColor;
2908
+ out vec4 outColor;
2909
+ void main() { outColor = vec4(uColor.rgb * uColor.a, uColor.a); }`
2910
+ );
2911
+ var DISC_FRAG = (
2912
+ /* glsl */
2913
+ `#version 300 es
2914
+ precision highp float;
2915
+ in vec2 vLocal;
2916
+ uniform vec4 uColor;
2917
+ out vec4 outColor;
2918
+ void main() {
2919
+ float r = length(vLocal);
2920
+ if (r > 1.0) discard;
2921
+ float alpha = smoothstep(1.0, 1.0 - 0.15, r);
2922
+ outColor = vec4(uColor.rgb * uColor.a * alpha, uColor.a * alpha);
2923
+ }`
2924
+ );
2925
+ var SEG_CORNERS4 = new Float32Array([0, -1, 1, -1, 1, 1, 0, -1, 1, 1, 0, 1]);
2926
+ var QUAD_CORNERS2 = new Float32Array([-1, -1, 1, -1, 1, 1, -1, -1, 1, 1, -1, 1]);
2927
+ var cache4 = /* @__PURE__ */ new WeakMap();
2928
+ function programs4(gl) {
2929
+ let p = cache4.get(gl);
2930
+ if (!p) {
2931
+ p = { stem: createProgram(gl, STEM_VERT, SOLID_FRAG2), marker: createProgram(gl, MARKER_VERT, DISC_FRAG) };
2932
+ cache4.set(gl, p);
2933
+ }
2934
+ return p;
2935
+ }
2936
+ var counter12 = 0;
2937
+ var StemLayer = class {
2938
+ constructor(gl, opts) {
2939
+ this.buffers = [];
2940
+ this.xRef = 0;
2941
+ this.yRef = 0;
2942
+ this.xBounds = [0, 0];
2943
+ this.yBounds = [0, 0];
2944
+ this.id = `stem-${counter12++}`;
2945
+ this.gl = gl;
2946
+ this.progs = programs4(gl);
2947
+ const colorInput = opts.color ?? "#3b82f6";
2948
+ this.color = Array.isArray(colorInput) ? colorInput : parseColor(colorInput);
2949
+ this.colorCss = typeof colorInput === "string" ? colorInput : toColorCss(this.color);
2950
+ this.name = opts.name ?? this.id;
2951
+ this.yAxis = opts.yAxis ?? "y";
2952
+ this.width = opts.width ?? 1.5;
2953
+ this.markerSize = opts.markerSize ?? 6;
2954
+ this.baseline = opts.baseline ?? 0;
2955
+ const n = Math.min(opts.x.length, opts.y.length);
2956
+ this.count = n;
2957
+ this.xs = new Float64Array(n);
2958
+ this.ys = new Float64Array(n);
2959
+ this.xRef = n > 0 ? opts.x[0] : 0;
2960
+ this.yRef = n > 0 ? opts.y[0] : 0;
2961
+ const segs = new Float32Array(n * 4);
2962
+ const tips = new Float32Array(n * 2);
2963
+ let minX = Infinity, maxX = -Infinity;
2964
+ let minY = Math.min(this.baseline, Infinity), maxY = Math.max(this.baseline, -Infinity);
2965
+ for (let i = 0; i < n; i++) {
2966
+ const x = opts.x[i], y = opts.y[i];
2967
+ this.xs[i] = x;
2968
+ this.ys[i] = y;
2969
+ segs[i * 4] = x - this.xRef;
2970
+ segs[i * 4 + 1] = this.baseline - this.yRef;
2971
+ segs[i * 4 + 2] = x - this.xRef;
2972
+ segs[i * 4 + 3] = y - this.yRef;
2973
+ tips[i * 2] = x - this.xRef;
2974
+ tips[i * 2 + 1] = y - this.yRef;
2975
+ if (x < minX) minX = x;
2976
+ if (x > maxX) maxX = x;
2977
+ if (y < minY) minY = y;
2978
+ if (y > maxY) maxY = y;
2979
+ }
2980
+ this.xBounds = [minX, maxX];
2981
+ this.yBounds = [minY, maxY];
2982
+ const cornerSeg = gl.createBuffer();
2983
+ gl.bindBuffer(gl.ARRAY_BUFFER, cornerSeg);
2984
+ gl.bufferData(gl.ARRAY_BUFFER, SEG_CORNERS4, gl.STATIC_DRAW);
2985
+ const cornerQuad = gl.createBuffer();
2986
+ gl.bindBuffer(gl.ARRAY_BUFFER, cornerQuad);
2987
+ gl.bufferData(gl.ARRAY_BUFFER, QUAD_CORNERS2, gl.STATIC_DRAW);
2988
+ const segBuf = gl.createBuffer();
2989
+ gl.bindBuffer(gl.ARRAY_BUFFER, segBuf);
2990
+ gl.bufferData(gl.ARRAY_BUFFER, segs, gl.STATIC_DRAW);
2991
+ const tipBuf = gl.createBuffer();
2992
+ gl.bindBuffer(gl.ARRAY_BUFFER, tipBuf);
2993
+ gl.bufferData(gl.ARRAY_BUFFER, tips, gl.STATIC_DRAW);
2994
+ this.buffers = [cornerSeg, cornerQuad, segBuf, tipBuf];
2995
+ this.stemVao = gl.createVertexArray();
2996
+ gl.bindVertexArray(this.stemVao);
2997
+ gl.bindBuffer(gl.ARRAY_BUFFER, cornerSeg);
2998
+ gl.enableVertexAttribArray(0);
2999
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
3000
+ gl.bindBuffer(gl.ARRAY_BUFFER, segBuf);
3001
+ gl.enableVertexAttribArray(1);
3002
+ gl.vertexAttribPointer(1, 4, gl.FLOAT, false, 0, 0);
3003
+ gl.vertexAttribDivisor(1, 1);
3004
+ this.markerVao = gl.createVertexArray();
3005
+ gl.bindVertexArray(this.markerVao);
3006
+ gl.bindBuffer(gl.ARRAY_BUFFER, cornerQuad);
3007
+ gl.enableVertexAttribArray(0);
3008
+ gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
3009
+ gl.bindBuffer(gl.ARRAY_BUFFER, tipBuf);
3010
+ gl.enableVertexAttribArray(1);
3011
+ gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 0, 0);
3012
+ gl.vertexAttribDivisor(1, 1);
3013
+ gl.bindVertexArray(null);
3014
+ this.uStem = uniformLocations(gl, this.progs.stem, [...TRANSFORM_UNIFORMS, "uColor", "uResolution", "uWidth"]);
3015
+ this.uMarker = uniformLocations(gl, this.progs.marker, [...TRANSFORM_UNIFORMS, "uColor", "uResolution", "uSize"]);
3016
+ }
3017
+ bounds() {
3018
+ if (this.count === 0) return null;
3019
+ return { x: this.xBounds, y: this.yBounds };
3020
+ }
3021
+ pick(mode, cursorPx, cursorPy, project) {
3022
+ return pickNearest(this.xs, this.ys, this.count, mode, cursorPx, cursorPy, project);
3023
+ }
3024
+ draw(state) {
3025
+ if (this.count === 0) return;
3026
+ const gl = state.gl;
3027
+ const [r, g, b, a] = this.color;
3028
+ gl.useProgram(this.progs.stem);
3029
+ setTransformUniforms(gl, this.uStem, state.x, state.y, this.xRef, this.yRef);
3030
+ gl.uniform4f(this.uStem.uColor, r, g, b, a);
3031
+ gl.uniform2f(this.uStem.uResolution, state.pixelWidth, state.pixelHeight);
3032
+ gl.uniform1f(this.uStem.uWidth, this.width * state.dpr);
3033
+ gl.bindVertexArray(this.stemVao);
1826
3034
  gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this.count);
1827
3035
  gl.bindVertexArray(null);
3036
+ if (this.markerSize > 0) {
3037
+ gl.useProgram(this.progs.marker);
3038
+ setTransformUniforms(gl, this.uMarker, state.x, state.y, this.xRef, this.yRef);
3039
+ gl.uniform4f(this.uMarker.uColor, r, g, b, a);
3040
+ gl.uniform2f(this.uMarker.uResolution, state.pixelWidth, state.pixelHeight);
3041
+ gl.uniform1f(this.uMarker.uSize, this.markerSize / 2 * state.dpr);
3042
+ gl.bindVertexArray(this.markerVao);
3043
+ gl.drawArraysInstanced(gl.TRIANGLES, 0, 6, this.count);
3044
+ gl.bindVertexArray(null);
3045
+ }
1828
3046
  }
1829
3047
  dispose() {
1830
- this.gl.deleteVertexArray(this.vao);
3048
+ this.gl.deleteVertexArray(this.stemVao);
3049
+ this.gl.deleteVertexArray(this.markerVao);
1831
3050
  for (const b of this.buffers) this.gl.deleteBuffer(b);
1832
3051
  }
1833
3052
  };
@@ -1962,6 +3181,27 @@ function drawCrosshair(ctx, region, px, theme) {
1962
3181
  ctx.stroke();
1963
3182
  ctx.restore();
1964
3183
  }
3184
+ function drawCrosshairXY(ctx, region, px, py, theme) {
3185
+ const left = region.left;
3186
+ const right = region.left + region.width;
3187
+ const top = region.top;
3188
+ const bottom = region.top + region.height;
3189
+ if (px < left || px > right || py < top || py > bottom) return;
3190
+ ctx.save();
3191
+ ctx.strokeStyle = theme.text;
3192
+ ctx.globalAlpha = 0.4;
3193
+ ctx.setLineDash([3, 3]);
3194
+ ctx.lineWidth = 1;
3195
+ const x = Math.round(px) + 0.5;
3196
+ const y = Math.round(py) + 0.5;
3197
+ ctx.beginPath();
3198
+ ctx.moveTo(x, top);
3199
+ ctx.lineTo(x, bottom);
3200
+ ctx.moveTo(left, y);
3201
+ ctx.lineTo(right, y);
3202
+ ctx.stroke();
3203
+ ctx.restore();
3204
+ }
1965
3205
  function drawMarker(ctx, px, py, color) {
1966
3206
  ctx.save();
1967
3207
  ctx.fillStyle = color;
@@ -2221,7 +3461,7 @@ function createToolbar(container, host, dark) {
2221
3461
  var DEFAULT_MARGIN = { top: 16, right: 16, bottom: 40, left: 56 };
2222
3462
  var Y_AXIS_GAP = 52;
2223
3463
  function isPickable(layer) {
2224
- return typeof layer.nearestByX === "function";
3464
+ return typeof layer.pick === "function";
2225
3465
  }
2226
3466
  function padDomain(min, max, log, frac) {
2227
3467
  if (log) {
@@ -2233,6 +3473,19 @@ function padDomain(min, max, log, frac) {
2233
3473
  const pad = (max - min) * frac || 1;
2234
3474
  return [min - pad, max + pad];
2235
3475
  }
3476
+ function clampAxis(domain, bounds) {
3477
+ const [lo, hi] = domain;
3478
+ const span = hi - lo;
3479
+ const [dlo, dhi] = bounds;
3480
+ if (span >= dhi - dlo) {
3481
+ const loMin = dhi - span;
3482
+ const clampedLo2 = Math.min(Math.max(lo, loMin), dlo);
3483
+ return [clampedLo2, clampedLo2 + span];
3484
+ }
3485
+ let clampedLo = Math.max(lo, dlo);
3486
+ if (clampedLo + span > dhi) clampedLo = dhi - span;
3487
+ return [clampedLo, clampedLo + span];
3488
+ }
2236
3489
  var Plot = class {
2237
3490
  constructor(container, options = {}) {
2238
3491
  /** Named y axes, insertion-ordered. `"y"` is always the primary. */
@@ -2243,6 +3496,10 @@ var Plot = class {
2243
3496
  this.modeChangeCbs = [];
2244
3497
  this.toolbarHandle = null;
2245
3498
  this.hoverPx = null;
3499
+ /** Cursor position while the pointer is pressed, when `crosshair`. */
3500
+ this.pressPx = null;
3501
+ /** A point clicked to pin its details, until another click clears it. */
3502
+ this.selected = null;
2246
3503
  this.container = container;
2247
3504
  if (getComputedStyle(container).position === "static") {
2248
3505
  container.style.position = "relative";
@@ -2273,8 +3530,14 @@ var Plot = class {
2273
3530
  this.isDark = options.theme === "dark";
2274
3531
  this.theme = options.theme === "dark" ? darkTheme : options.theme === "light" || options.theme == null ? lightTheme : options.theme;
2275
3532
  this.baseMargin = { ...DEFAULT_MARGIN, ...options.margin };
2276
- this.mode = options.mode ?? "box";
3533
+ this.mode = options.mode ?? "pan";
2277
3534
  this.hoverEnabled = options.hover !== false;
3535
+ this.pickMode = options.pick ?? "x";
3536
+ this.pointInfo = options.pointInfo ?? "click";
3537
+ this.crosshair = options.crosshair ?? true;
3538
+ this.equalAspect = options.equalAspect ?? false;
3539
+ this.boundedPan = options.boundedPan ?? false;
3540
+ this.hoverReadout = options.hoverReadout;
2278
3541
  this.selectionDiv = document.createElement("div");
2279
3542
  Object.assign(this.selectionDiv.style, {
2280
3543
  position: "absolute",
@@ -2303,11 +3566,28 @@ var Plot = class {
2303
3566
  boxShadow: "0 4px 12px rgba(0,0,0,0.18)"
2304
3567
  });
2305
3568
  container.appendChild(this.tooltip);
3569
+ this.infoBox = document.createElement("div");
3570
+ Object.assign(this.infoBox.style, {
3571
+ position: "absolute",
3572
+ display: "none",
3573
+ zIndex: "6",
3574
+ pointerEvents: "none",
3575
+ padding: "6px 8px",
3576
+ borderRadius: "6px",
3577
+ font: "12px system-ui, -apple-system, sans-serif",
3578
+ lineHeight: "1.4",
3579
+ whiteSpace: "nowrap",
3580
+ background: this.isDark ? "rgba(15,23,42,0.96)" : "rgba(255,255,255,0.98)",
3581
+ color: this.isDark ? "#e2e8f0" : "#1e293b",
3582
+ border: `1px solid ${this.isDark ? "rgba(148,163,184,0.4)" : "rgba(100,116,139,0.4)"}`,
3583
+ boxShadow: "0 6px 18px rgba(0,0,0,0.25)"
3584
+ });
3585
+ container.appendChild(this.infoBox);
2306
3586
  this.resizeObserver = new ResizeObserver(() => this.resize());
2307
3587
  this.resizeObserver.observe(container);
2308
3588
  this.resize();
2309
3589
  if (options.interactive !== false) this.attachInteraction();
2310
- if (options.toolbar !== false) {
3590
+ if (options.showToolbar !== false) {
2311
3591
  this.toolbarHandle = createToolbar(
2312
3592
  container,
2313
3593
  {
@@ -2385,6 +3665,34 @@ var Plot = class {
2385
3665
  this.requestRender();
2386
3666
  return layer;
2387
3667
  }
3668
+ /**
3669
+ * Register a layer built outside core (e.g. `@photonviz/map`). Use with
3670
+ * {@link context} to construct the layer against this plot's WebGL2 context.
3671
+ */
3672
+ add(layer) {
3673
+ return this.register(layer);
3674
+ }
3675
+ /** The shared WebGL2 context, for constructing custom layers. */
3676
+ get context() {
3677
+ return this.gl;
3678
+ }
3679
+ /**
3680
+ * Convert a client (screen) coordinate to data space — the shared x and the
3681
+ * primary y. Returns null if the point is outside the plot region. Useful for
3682
+ * custom hit-testing (e.g. map feature picking on click).
3683
+ */
3684
+ dataAt(clientX, clientY) {
3685
+ const rect = this.axisCanvas.getBoundingClientRect();
3686
+ const region = plotRegion(this.layout());
3687
+ const px = clientX - rect.left;
3688
+ const py = clientY - rect.top;
3689
+ if (px < region.left || px > region.left + region.width || py < region.top || py > region.top + region.height) {
3690
+ return null;
3691
+ }
3692
+ const nx = (px - region.left) / region.width;
3693
+ const ny = 1 - (py - region.top) / region.height;
3694
+ return { x: this.scaleX.invert(nx), y: this.primaryY().scale.invert(ny) };
3695
+ }
2388
3696
  addLine(opts) {
2389
3697
  return this.register(new LineLayer(this.gl, opts));
2390
3698
  }
@@ -2409,6 +3717,18 @@ var Plot = class {
2409
3717
  addContour(opts) {
2410
3718
  return this.register(new ContourLayer(this.gl, opts));
2411
3719
  }
3720
+ addErrorBar(opts) {
3721
+ return this.register(new ErrorBarLayer(this.gl, opts));
3722
+ }
3723
+ addStem(opts) {
3724
+ return this.register(new StemLayer(this.gl, opts));
3725
+ }
3726
+ addQuiver(opts) {
3727
+ return this.register(new QuiverLayer(this.gl, opts));
3728
+ }
3729
+ addCandlestick(opts) {
3730
+ return this.register(new CandlestickLayer(this.gl, opts));
3731
+ }
2412
3732
  /** Compute an STFT of `signal` and render it as a heatmap (time × frequency). */
2413
3733
  addHeatmapSpectrogram(signal, opts = {}) {
2414
3734
  const s = spectrogram(signal, opts);
@@ -2554,6 +3874,7 @@ var Plot = class {
2554
3874
  this.toolbarHandle?.destroy();
2555
3875
  this.selectionDiv.remove();
2556
3876
  this.tooltip.remove();
3877
+ this.infoBox.remove();
2557
3878
  for (const l of this.layers) l.dispose();
2558
3879
  this.container.removeChild(this.gridCanvas);
2559
3880
  this.container.removeChild(this.dataCanvas);
@@ -2588,6 +3909,8 @@ var Plot = class {
2588
3909
  render() {
2589
3910
  const layout = this.layout();
2590
3911
  const region = plotRegion(layout);
3912
+ if (this.equalAspect) this.applyAspect(region);
3913
+ if (this.boundedPan) this.clampView();
2591
3914
  const primary = this.primaryY();
2592
3915
  const ticksX = this.axisX.resolve(this.scaleX);
2593
3916
  const ticksYPrimary = primary.axis.resolve(primary.scale);
@@ -2643,8 +3966,16 @@ var Plot = class {
2643
3966
  titleX: pos.titleX
2644
3967
  });
2645
3968
  }
2646
- if (this.hoverEnabled && this.hoverPx) this.renderHover(region);
2647
- else this.tooltip.style.display = "none";
3969
+ if (this.crosshair && this.pressPx) {
3970
+ drawCrosshairXY(this.axisCtx, region, this.pressPx.x, this.pressPx.y, this.theme);
3971
+ }
3972
+ if (this.hoverEnabled && this.hoverPx) {
3973
+ this.renderHover(region);
3974
+ } else {
3975
+ this.tooltip.style.display = "none";
3976
+ if (this.pointInfo === "hover") this.selected = null;
3977
+ }
3978
+ this.updateInfoBox(region);
2648
3979
  }
2649
3980
  renderHover(region) {
2650
3981
  const cursor = this.hoverPx;
@@ -2654,33 +3985,49 @@ var Plot = class {
2654
3985
  }
2655
3986
  const nx = (cursor.x - region.left) / region.width;
2656
3987
  const dataX = this.scaleX.invert(nx);
2657
- drawCrosshair(this.axisCtx, region, cursor.x, this.theme);
3988
+ const ny = 1 - (cursor.y - region.top) / region.height;
3989
+ const dataY = this.primaryY().scale.invert(ny);
3990
+ if (this.crosshair) {
3991
+ drawCrosshairXY(this.axisCtx, region, cursor.x, cursor.y, this.theme);
3992
+ } else {
3993
+ drawCrosshair(this.axisCtx, region, cursor.x, this.theme);
3994
+ }
2658
3995
  const rows = [];
2659
3996
  for (const layer of this.layers) {
2660
3997
  if (!isPickable(layer)) continue;
2661
- const p = layer.nearestByX(dataX);
2662
- if (!p) continue;
2663
3998
  const ya = this.yAxes.get(layer.yAxis);
2664
- const px = pxX(region, this.scaleX.norm(p.x));
2665
- const py = pxY(region, ya.scale.norm(p.y));
3999
+ const project = (x, y) => [
4000
+ pxX(region, this.scaleX.norm(x)),
4001
+ pxY(region, ya.scale.norm(y))
4002
+ ];
4003
+ const p = layer.pick(this.pickMode, cursor.x, cursor.y, project);
4004
+ if (!p) continue;
4005
+ const [px, py] = project(p.x, p.y);
2666
4006
  drawMarker(this.axisCtx, px, py, layer.colorCss);
2667
4007
  rows.push({ layer, x: p.x, y: p.y });
2668
4008
  }
2669
- if (rows.length === 0) {
2670
- this.tooltip.style.display = "none";
2671
- return;
2672
- }
2673
- this.updateTooltip(rows, cursor, dataX);
4009
+ this.updateTooltip(rows, cursor, dataX, dataY);
4010
+ if (this.pointInfo === "hover") this.selected = this.pickPoint(cursor.x, cursor.y);
2674
4011
  }
2675
- updateTooltip(rows, cursor, dataX) {
4012
+ updateTooltip(rows, cursor, dataX, dataY) {
2676
4013
  const tip = this.tooltip;
2677
4014
  tip.replaceChildren();
2678
- const xfmt = this.axisX.config.format ?? defaultFormat;
2679
- const header = document.createElement("div");
2680
- header.style.opacity = "0.7";
2681
- header.style.marginBottom = "3px";
2682
- header.textContent = `x = ${xfmt(dataX)}`;
2683
- tip.appendChild(header);
4015
+ if (this.hoverReadout) {
4016
+ for (const line of this.hoverReadout(dataX, dataY)) {
4017
+ const el = document.createElement("div");
4018
+ el.style.opacity = "0.7";
4019
+ el.style.marginBottom = "3px";
4020
+ el.textContent = `${line.label} ${line.value}`;
4021
+ tip.appendChild(el);
4022
+ }
4023
+ } else {
4024
+ const xfmt = this.axisX.config.format ?? defaultFormat;
4025
+ const header = document.createElement("div");
4026
+ header.style.opacity = "0.7";
4027
+ header.style.marginBottom = "3px";
4028
+ header.textContent = `x = ${xfmt(dataX)}`;
4029
+ tip.appendChild(header);
4030
+ }
2684
4031
  for (const r of rows) {
2685
4032
  const yfmt = this.yAxes.get(r.layer.yAxis).axis.config.format ?? defaultFormat;
2686
4033
  const row = document.createElement("div");
@@ -2746,18 +4093,19 @@ var Plot = class {
2746
4093
  }
2747
4094
  return { type: "plot" };
2748
4095
  }
4096
+ // Pan/zoom shift the domain in the scale's *transformed* space (via invert),
4097
+ // not raw data space — so a log axis stays positive instead of crossing zero
4098
+ // into NaN. For linear/time scales this is identical to the old arithmetic.
2749
4099
  panX(dxPx, region) {
2750
- const dxData = dxPx / region.width * (this.scaleX.domain[1] - this.scaleX.domain[0]);
2751
- this.scaleX.domain = [this.scaleX.domain[0] - dxData, this.scaleX.domain[1] - dxData];
4100
+ const f = dxPx / region.width;
4101
+ this.scaleX.domain = [this.scaleX.invert(-f), this.scaleX.invert(1 - f)];
2752
4102
  this.autoX = false;
2753
4103
  }
2754
4104
  panY(id, dyPx, region) {
2755
- const dyFrac = dyPx / region.height;
4105
+ const f = dyPx / region.height;
2756
4106
  for (const ya of this.yAxes.values()) {
2757
4107
  if (id && ya.id !== id) continue;
2758
- const span = ya.scale.domain[1] - ya.scale.domain[0];
2759
- const d = dyFrac * span;
2760
- ya.scale.domain = [ya.scale.domain[0] + d, ya.scale.domain[1] + d];
4108
+ ya.scale.domain = [ya.scale.invert(f), ya.scale.invert(1 + f)];
2761
4109
  ya.auto = false;
2762
4110
  }
2763
4111
  }
@@ -2784,12 +4132,16 @@ var Plot = class {
2784
4132
  let lastY = 0;
2785
4133
  let startX = 0;
2786
4134
  let startY = 0;
4135
+ let downX = 0;
4136
+ let downY = 0;
2787
4137
  el.addEventListener("pointerdown", (e) => {
2788
4138
  el.setPointerCapture(e.pointerId);
2789
4139
  this.hoverPx = null;
2790
4140
  const rect = el.getBoundingClientRect();
2791
4141
  const px = e.clientX - rect.left;
2792
4142
  const py = e.clientY - rect.top;
4143
+ downX = px;
4144
+ downY = py;
2793
4145
  const zone = this.zoneAt(px, py);
2794
4146
  if (zone.type === "x") {
2795
4147
  axisDrag = "x";
@@ -2802,15 +4154,18 @@ var Plot = class {
2802
4154
  lastX = e.clientX;
2803
4155
  lastY = e.clientY;
2804
4156
  el.style.cursor = "grabbing";
4157
+ if (this.crosshair) this.setPress(px, py);
2805
4158
  } else {
2806
4159
  selecting = true;
2807
4160
  startX = px;
2808
4161
  startY = py;
4162
+ if (this.crosshair) this.setPress(px, py);
2809
4163
  }
2810
4164
  });
2811
4165
  el.addEventListener("pointermove", (e) => {
2812
4166
  const rect = el.getBoundingClientRect();
2813
4167
  const region = plotRegion(this.layout());
4168
+ if (this.pressPx) this.setPress(e.clientX - rect.left, e.clientY - rect.top);
2814
4169
  if (axisDrag === "x") {
2815
4170
  this.panX(e.clientX - lastX, region);
2816
4171
  lastX = e.clientX;
@@ -2846,6 +4201,14 @@ var Plot = class {
2846
4201
  el.addEventListener("pointerleave", () => this.setHover(null));
2847
4202
  const end = (e) => {
2848
4203
  if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);
4204
+ if (this.pressPx) {
4205
+ this.pressPx = null;
4206
+ this.requestRender();
4207
+ }
4208
+ const rect = el.getBoundingClientRect();
4209
+ const upPx = e.clientX - rect.left;
4210
+ const upPy = e.clientY - rect.top;
4211
+ const isClick = e.type === "pointerup" && !axisDrag && Math.hypot(upPx - downX, upPy - downY) < 4;
2849
4212
  if (axisDrag) {
2850
4213
  axisDrag = null;
2851
4214
  } else if (panning) {
@@ -2853,10 +4216,10 @@ var Plot = class {
2853
4216
  this.updateCursor();
2854
4217
  } else if (selecting) {
2855
4218
  selecting = false;
2856
- const rect = el.getBoundingClientRect();
2857
- this.applySelection(startX, startY, e.clientX - rect.left, e.clientY - rect.top);
4219
+ if (!isClick) this.applySelection(startX, startY, upPx, upPy);
2858
4220
  this.selectionDiv.style.display = "none";
2859
4221
  }
4222
+ if (isClick) this.handleClick(upPx, upPy);
2860
4223
  };
2861
4224
  el.addEventListener("pointerup", end);
2862
4225
  el.addEventListener("pointercancel", end);
@@ -2867,6 +4230,160 @@ var Plot = class {
2867
4230
  this.hoverPx = px;
2868
4231
  this.requestRender();
2869
4232
  }
4233
+ /** Update the press-crosshair position and redraw. */
4234
+ setPress(x, y) {
4235
+ this.pressPx = { x, y };
4236
+ this.requestRender();
4237
+ }
4238
+ /** Nearest point (2D, within a small radius) under the cursor, or null. */
4239
+ pickPoint(cursorPx, cursorPy) {
4240
+ const region = plotRegion(this.layout());
4241
+ let hit = null;
4242
+ let hitDist = Infinity;
4243
+ for (const layer of this.layers) {
4244
+ if (!isPickable(layer)) continue;
4245
+ const ya = this.yAxes.get(layer.yAxis);
4246
+ const project = (x, y) => [
4247
+ pxX(region, this.scaleX.norm(x)),
4248
+ pxY(region, ya.scale.norm(y))
4249
+ ];
4250
+ const p = layer.pick("xy", cursorPx, cursorPy, project);
4251
+ if (!p) continue;
4252
+ const [ppx, ppy] = project(p.x, p.y);
4253
+ const d = Math.hypot(ppx - cursorPx, ppy - cursorPy);
4254
+ if (d < hitDist) {
4255
+ hitDist = d;
4256
+ hit = { layer, x: p.x, y: p.y, index: p.index };
4257
+ }
4258
+ }
4259
+ return hit && hitDist <= 14 ? hit : null;
4260
+ }
4261
+ /** Click handler: in `pointInfo:"click"` mode, pin the point under the cursor. */
4262
+ handleClick(cursorPx, cursorPy) {
4263
+ if (this.pointInfo !== "click") return;
4264
+ this.selected = this.pickPoint(cursorPx, cursorPy);
4265
+ this.requestRender();
4266
+ }
4267
+ /** Draw the pinned point's marker and position its info box (or hide it). */
4268
+ updateInfoBox(region) {
4269
+ const box = this.infoBox;
4270
+ if (!this.selected) {
4271
+ box.style.display = "none";
4272
+ return;
4273
+ }
4274
+ const { layer, x, y, index } = this.selected;
4275
+ const ya = this.yAxes.get(layer.yAxis);
4276
+ const px = pxX(region, this.scaleX.norm(x));
4277
+ const py = pxY(region, ya.scale.norm(y));
4278
+ if (px < region.left || px > region.left + region.width || py < region.top || py > region.top + region.height) {
4279
+ box.style.display = "none";
4280
+ return;
4281
+ }
4282
+ drawMarker(this.axisCtx, px, py, layer.colorCss);
4283
+ box.replaceChildren();
4284
+ const info = layer.infoAt ? layer.infoAt(index) : null;
4285
+ const title = document.createElement("div");
4286
+ title.style.fontWeight = "600";
4287
+ title.style.marginBottom = "2px";
4288
+ title.textContent = info && info.length ? info[0] : layer.name;
4289
+ box.appendChild(title);
4290
+ if (info && info.length > 1) {
4291
+ for (let i = 1; i < info.length; i++) {
4292
+ const row = document.createElement("div");
4293
+ row.textContent = info[i];
4294
+ box.appendChild(row);
4295
+ }
4296
+ } else {
4297
+ const lines = this.hoverReadout ? this.hoverReadout(x, y) : [
4298
+ { label: "x", value: (this.axisX.config.format ?? defaultFormat)(x) },
4299
+ { label: "y", value: (ya.axis.config.format ?? defaultFormat)(y) }
4300
+ ];
4301
+ for (const ln of lines) {
4302
+ const row = document.createElement("div");
4303
+ row.textContent = `${ln.label} ${ln.value}`;
4304
+ box.appendChild(row);
4305
+ }
4306
+ }
4307
+ box.style.display = "block";
4308
+ const cw = this.container.clientWidth;
4309
+ const tw = box.offsetWidth;
4310
+ const th = box.offsetHeight;
4311
+ let left = px + 12;
4312
+ if (left + tw > cw) left = px - tw - 12;
4313
+ let top = py + 12;
4314
+ if (top + th > this.container.clientHeight) top = py - th - 12;
4315
+ box.style.left = `${Math.max(0, left)}px`;
4316
+ box.style.top = `${Math.max(0, top)}px`;
4317
+ }
4318
+ /** Data-space x extent across all layers, or null if empty. */
4319
+ layerBoundsX() {
4320
+ let lo = Infinity;
4321
+ let hi = -Infinity;
4322
+ let any = false;
4323
+ for (const l of this.layers) {
4324
+ const b = l.bounds();
4325
+ if (!b) continue;
4326
+ any = true;
4327
+ lo = Math.min(lo, b.x[0]);
4328
+ hi = Math.max(hi, b.x[1]);
4329
+ }
4330
+ return any ? [lo, hi] : null;
4331
+ }
4332
+ /** Data-space y extent across the layers bound to axis `id`, or null. */
4333
+ layerBoundsY(id) {
4334
+ let lo = Infinity;
4335
+ let hi = -Infinity;
4336
+ let any = false;
4337
+ for (const l of this.layers) {
4338
+ if (l.yAxis !== id) continue;
4339
+ const b = l.bounds();
4340
+ if (!b) continue;
4341
+ any = true;
4342
+ lo = Math.min(lo, b.y[0]);
4343
+ hi = Math.max(hi, b.y[1]);
4344
+ }
4345
+ return any ? [lo, hi] : null;
4346
+ }
4347
+ /** Keep the view inside the data bounds (used when `boundedPan`). */
4348
+ clampView() {
4349
+ if (!this.scaleX.log) {
4350
+ const bx = this.layerBoundsX();
4351
+ if (bx) this.scaleX.domain = clampAxis(this.scaleX.domain, bx);
4352
+ }
4353
+ for (const ya of this.yAxes.values()) {
4354
+ if (ya.scale.log) continue;
4355
+ const by = this.layerBoundsY(ya.id);
4356
+ if (by) ya.scale.domain = clampAxis(ya.scale.domain, by);
4357
+ }
4358
+ }
4359
+ /**
4360
+ * Expand the looser axis so both axes share the same data-units-per-pixel,
4361
+ * preventing distortion (maps set `equalAspect`). Linear axes only; balances
4362
+ * the primary y against x, and is idempotent once balanced.
4363
+ */
4364
+ applyAspect(region) {
4365
+ if (this.scaleX.log) return;
4366
+ const primary = this.primaryY();
4367
+ if (primary.scale.log) return;
4368
+ const w = region.width;
4369
+ const h = region.height;
4370
+ if (w <= 0 || h <= 0) return;
4371
+ const [xlo, xhi] = this.scaleX.domain;
4372
+ const [ylo, yhi] = primary.scale.domain;
4373
+ const uppX = (xhi - xlo) / w;
4374
+ const uppY = (yhi - ylo) / h;
4375
+ if (uppX <= 0 || uppY <= 0) return;
4376
+ if (Math.abs(uppX - uppY) <= 1e-9 * Math.max(uppX, uppY)) return;
4377
+ if (uppX > uppY) {
4378
+ const target = uppX * h;
4379
+ const cy = (ylo + yhi) / 2;
4380
+ primary.scale.domain = [cy - target / 2, cy + target / 2];
4381
+ } else {
4382
+ const target = uppY * w;
4383
+ const cx = (xlo + xhi) / 2;
4384
+ this.scaleX.domain = [cx - target / 2, cx + target / 2];
4385
+ }
4386
+ }
2870
4387
  drawSelection(x0, y0, x1, y1) {
2871
4388
  const region = plotRegion(this.layout());
2872
4389
  const lock = this.axisLock();
@@ -2931,16 +4448,14 @@ var Plot = class {
2931
4448
  zoomAround(nx, ny, factor) {
2932
4449
  const lock = this.axisLock();
2933
4450
  if (lock.x) {
2934
- const [x0, x1] = this.scaleX.domain;
2935
- const cx = x0 + nx * (x1 - x0);
2936
- this.scaleX.domain = [cx + (x0 - cx) * factor, cx + (x1 - cx) * factor];
4451
+ const t = nx * (1 - factor);
4452
+ this.scaleX.domain = [this.scaleX.invert(t), this.scaleX.invert(t + factor)];
2937
4453
  this.autoX = false;
2938
4454
  }
2939
4455
  if (lock.y) {
4456
+ const t = ny * (1 - factor);
2940
4457
  for (const ya of this.yAxes.values()) {
2941
- const [y0, y1] = ya.scale.domain;
2942
- const cy = y0 + ny * (y1 - y0);
2943
- ya.scale.domain = [cy + (y0 - cy) * factor, cy + (y1 - cy) * factor];
4458
+ ya.scale.domain = [ya.scale.invert(t), ya.scale.invert(t + factor)];
2944
4459
  ya.auto = false;
2945
4460
  }
2946
4461
  }
@@ -2953,29 +4468,80 @@ var PolarPlot = class {
2953
4468
  constructor(container, options = {}) {
2954
4469
  this.entries = [];
2955
4470
  this.R = 1;
4471
+ this.baseR = 1;
2956
4472
  this.dpr = 1;
2957
4473
  this.frameRequested = false;
4474
+ /** Multiplier on the fitted radius: <1 zooms in (smaller r-domain), >1 out. */
4475
+ this.zoom = 1;
4476
+ /** Angular offset (radians, CCW) applied to all series + gridlines. */
4477
+ this.rotation = 0;
4478
+ this.hoverPx = null;
4479
+ this.homeButton = null;
4480
+ /** A point clicked to pin its details, until another click clears it. */
4481
+ this.selected = null;
2958
4482
  this.container = container;
2959
4483
  if (getComputedStyle(container).position === "static") container.style.position = "relative";
2960
- this.gridCanvas = this.makeCanvas(0);
2961
- this.dataCanvas = this.makeCanvas(1);
4484
+ this.gridCanvas = this.makeCanvas(0, false);
4485
+ this.dataCanvas = this.makeCanvas(1, false);
4486
+ this.overlayCanvas = this.makeCanvas(2, true);
2962
4487
  this.gridCtx = this.gridCanvas.getContext("2d");
2963
4488
  this.dataCtx = this.dataCanvas.getContext("2d");
4489
+ this.overlayCtx = this.overlayCanvas.getContext("2d");
2964
4490
  const s = getSharedGL();
2965
4491
  this.gl = s.gl;
2966
4492
  this.sharedCanvas = s.canvas;
4493
+ this.isDark = options.theme === "dark";
2967
4494
  this.theme = options.theme === "dark" ? darkTheme : options.theme === "light" || options.theme == null ? lightTheme : options.theme;
2968
4495
  this.toRad = options.angleUnit === "deg" ? Math.PI / 180 : 1;
2969
4496
  this.fixedR = options.maxRadius;
2970
4497
  this.margin = options.margin ?? 28;
4498
+ this.interactive = options.interactive !== false;
4499
+ this.hoverEnabled = options.hover !== false;
4500
+ this.pointInfo = options.pointInfo ?? "click";
4501
+ this.tooltip = document.createElement("div");
4502
+ Object.assign(this.tooltip.style, {
4503
+ position: "absolute",
4504
+ display: "none",
4505
+ zIndex: "4",
4506
+ pointerEvents: "none",
4507
+ padding: "6px 8px",
4508
+ borderRadius: "6px",
4509
+ font: "12px system-ui, -apple-system, sans-serif",
4510
+ lineHeight: "1.4",
4511
+ whiteSpace: "nowrap",
4512
+ background: this.isDark ? "rgba(15,23,42,0.92)" : "rgba(255,255,255,0.96)",
4513
+ color: this.isDark ? "#e2e8f0" : "#1e293b",
4514
+ border: `1px solid ${this.isDark ? "rgba(148,163,184,0.25)" : "rgba(100,116,139,0.25)"}`,
4515
+ boxShadow: "0 4px 12px rgba(0,0,0,0.18)"
4516
+ });
4517
+ container.appendChild(this.tooltip);
4518
+ this.infoBox = document.createElement("div");
4519
+ Object.assign(this.infoBox.style, {
4520
+ position: "absolute",
4521
+ display: "none",
4522
+ zIndex: "6",
4523
+ pointerEvents: "none",
4524
+ padding: "6px 8px",
4525
+ borderRadius: "6px",
4526
+ font: "12px system-ui, -apple-system, sans-serif",
4527
+ lineHeight: "1.4",
4528
+ whiteSpace: "nowrap",
4529
+ background: this.isDark ? "rgba(15,23,42,0.96)" : "rgba(255,255,255,0.98)",
4530
+ color: this.isDark ? "#e2e8f0" : "#1e293b",
4531
+ border: `1px solid ${this.isDark ? "rgba(148,163,184,0.4)" : "rgba(100,116,139,0.4)"}`,
4532
+ boxShadow: "0 6px 18px rgba(0,0,0,0.25)"
4533
+ });
4534
+ container.appendChild(this.infoBox);
2971
4535
  this.resizeObserver = new ResizeObserver(() => this.resize());
2972
4536
  this.resizeObserver.observe(container);
2973
4537
  this.resize();
4538
+ if (this.interactive) this.attachInteraction();
4539
+ if (options.showToolbar !== false) this.homeButton = this.makeHomeButton();
2974
4540
  }
2975
- makeCanvas(z) {
4541
+ makeCanvas(z, interactive) {
2976
4542
  const c = document.createElement("canvas");
2977
4543
  Object.assign(c.style, { position: "absolute", inset: "0", width: "100%", height: "100%", zIndex: String(z) });
2978
- c.style.pointerEvents = "none";
4544
+ if (!interactive) c.style.pointerEvents = "none";
2979
4545
  this.container.appendChild(c);
2980
4546
  return c;
2981
4547
  }
@@ -2983,9 +4549,10 @@ var PolarPlot = class {
2983
4549
  const n = Math.min(theta.length, r.length);
2984
4550
  const m = closed && n > 0 ? n + 1 : n;
2985
4551
  const x = new Float64Array(m), y = new Float64Array(m);
4552
+ const rot = this.rotation;
2986
4553
  let maxR = 0;
2987
4554
  for (let i = 0; i < n; i++) {
2988
- const a = theta[i] * this.toRad, rr = r[i];
4555
+ const a = theta[i] * this.toRad + rot, rr = r[i];
2989
4556
  x[i] = rr * Math.cos(a);
2990
4557
  y[i] = rr * Math.sin(a);
2991
4558
  if (Math.abs(rr) > maxR) maxR = Math.abs(rr);
@@ -3000,7 +4567,7 @@ var PolarPlot = class {
3000
4567
  const closed = opts.closed ?? false;
3001
4568
  const { x, y, maxR } = this.toXY(opts.theta, opts.r, closed);
3002
4569
  const layer = new LineLayer(this.gl, { x, y, color: opts.color, width: opts.width ?? 2, decimate: false });
3003
- const entry = { layer, closed, maxR };
4570
+ const entry = { layer, closed, maxR, theta: opts.theta, r: opts.r };
3004
4571
  this.entries.push(entry);
3005
4572
  this.refit();
3006
4573
  return this.handle(entry);
@@ -3008,7 +4575,7 @@ var PolarPlot = class {
3008
4575
  addScatter(opts) {
3009
4576
  const { x, y, maxR } = this.toXY(opts.theta, opts.r, false);
3010
4577
  const layer = new ScatterLayer(this.gl, { x, y, color: opts.color, size: opts.size ?? 5 });
3011
- const entry = { layer, closed: false, maxR };
4578
+ const entry = { layer, closed: false, maxR, theta: opts.theta, r: opts.r, labels: opts.labels };
3012
4579
  this.entries.push(entry);
3013
4580
  this.refit();
3014
4581
  return this.handle(entry);
@@ -3016,6 +4583,8 @@ var PolarPlot = class {
3016
4583
  handle(entry) {
3017
4584
  return {
3018
4585
  setData: (theta, r) => {
4586
+ entry.theta = theta;
4587
+ entry.r = r;
3019
4588
  const { x, y, maxR } = this.toXY(theta, r, entry.closed);
3020
4589
  entry.layer.setData(x, y);
3021
4590
  entry.maxR = maxR;
@@ -3025,29 +4594,63 @@ var PolarPlot = class {
3025
4594
  };
3026
4595
  }
3027
4596
  refit() {
4597
+ let base;
3028
4598
  if (this.fixedR != null) {
3029
- this.R = this.fixedR;
3030
- return;
4599
+ base = this.fixedR;
4600
+ } else {
4601
+ let m = 0;
4602
+ for (const e of this.entries) m = Math.max(m, e.maxR);
4603
+ base = m || 1;
4604
+ }
4605
+ this.baseR = base;
4606
+ this.R = base * this.zoom;
4607
+ }
4608
+ /** Re-project every series with the current rotation (called when rotation changes). */
4609
+ retransform() {
4610
+ for (const e of this.entries) {
4611
+ const { x, y } = this.toXY(e.theta, e.r, e.closed);
4612
+ e.layer.setData(x, y);
3031
4613
  }
3032
- let m = 0;
3033
- for (const e of this.entries) m = Math.max(m, e.maxR);
3034
- this.R = m || 1;
4614
+ }
4615
+ // ---- Interaction control --------------------------------------------------
4616
+ /** Reset zoom (r-domain) and rotation back to the initial view. */
4617
+ home() {
4618
+ this.zoom = 1;
4619
+ this.rotation = 0;
4620
+ this.refit();
4621
+ this.retransform();
4622
+ this.requestRender();
4623
+ }
4624
+ /** Current rotation offset in radians (CCW). */
4625
+ getRotation() {
4626
+ return this.rotation;
4627
+ }
4628
+ /** Set the rotation offset (radians, CCW) and redraw. */
4629
+ setRotation(rad) {
4630
+ this.rotation = rad;
4631
+ this.retransform();
4632
+ this.requestRender();
3035
4633
  }
3036
4634
  destroy() {
3037
4635
  this.resizeObserver.disconnect();
3038
4636
  for (const e of this.entries) e.layer.dispose();
4637
+ this.tooltip.remove();
4638
+ this.infoBox.remove();
4639
+ this.homeButton?.remove();
3039
4640
  this.container.removeChild(this.gridCanvas);
3040
4641
  this.container.removeChild(this.dataCanvas);
4642
+ this.container.removeChild(this.overlayCanvas);
3041
4643
  }
3042
4644
  resize() {
3043
4645
  this.dpr = window.devicePixelRatio || 1;
3044
4646
  const w = this.container.clientWidth, h = this.container.clientHeight;
3045
- for (const c of [this.gridCanvas, this.dataCanvas]) {
4647
+ for (const c of [this.gridCanvas, this.dataCanvas, this.overlayCanvas]) {
3046
4648
  c.width = Math.max(1, Math.round(w * this.dpr));
3047
4649
  c.height = Math.max(1, Math.round(h * this.dpr));
3048
4650
  }
3049
4651
  this.gridCtx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
3050
4652
  this.dataCtx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
4653
+ this.overlayCtx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
3051
4654
  this.render();
3052
4655
  }
3053
4656
  requestRender() {
@@ -3089,6 +4692,14 @@ var PolarPlot = class {
3089
4692
  this.dataCtx.clearRect(0, 0, w, h);
3090
4693
  this.dataCtx.drawImage(this.sharedCanvas, 0, 0, w, h);
3091
4694
  this.drawGrid(sq);
4695
+ this.overlayCtx.clearRect(0, 0, w, h);
4696
+ if (this.hoverEnabled && this.hoverPx) {
4697
+ this.renderHover(sq);
4698
+ } else {
4699
+ this.tooltip.style.display = "none";
4700
+ if (this.pointInfo === "hover") this.selected = null;
4701
+ }
4702
+ this.updateInfoBox(sq);
3092
4703
  }
3093
4704
  drawGrid(sq) {
3094
4705
  const ctx = this.gridCtx;
@@ -3102,7 +4713,7 @@ var PolarPlot = class {
3102
4713
  ctx.strokeStyle = this.theme.grid;
3103
4714
  ctx.lineWidth = 1;
3104
4715
  for (let deg = 0; deg < 360; deg += 30) {
3105
- const a = deg * Math.PI / 180;
4716
+ const a = deg * Math.PI / 180 + this.rotation;
3106
4717
  const ex = cx + Math.cos(a) * pr, ey = cy - Math.sin(a) * pr;
3107
4718
  ctx.beginPath();
3108
4719
  ctx.moveTo(cx, cy);
@@ -3123,7 +4734,270 @@ var PolarPlot = class {
3123
4734
  }
3124
4735
  ctx.restore();
3125
4736
  }
4737
+ // ---- Hover ----------------------------------------------------------------
4738
+ setHover(px) {
4739
+ const had = this.hoverPx !== null;
4740
+ if (!px && !had) return;
4741
+ this.hoverPx = px;
4742
+ this.requestRender();
4743
+ }
4744
+ /** Nearest data point (across all series) to a cursor position, in CSS px. */
4745
+ pickNearest(sq, cursorX, cursorY) {
4746
+ const pr = sq.side / 2;
4747
+ const { cx, cy } = sq;
4748
+ const rot = this.rotation;
4749
+ let best = null;
4750
+ for (const e of this.entries) {
4751
+ const n = Math.min(e.theta.length, e.r.length);
4752
+ for (let i = 0; i < n; i++) {
4753
+ const a = e.theta[i] * this.toRad + rot, rr = e.r[i];
4754
+ const px = cx + rr * Math.cos(a) / this.R * pr;
4755
+ const py = cy - rr * Math.sin(a) / this.R * pr;
4756
+ const dx = px - cursorX, dy = py - cursorY;
4757
+ const d2 = dx * dx + dy * dy;
4758
+ if (!best || d2 < best.d2) best = { entry: e, index: i, px, py, theta: e.theta[i], r: rr, d2 };
4759
+ }
4760
+ }
4761
+ return best;
4762
+ }
4763
+ /** Draw a filled, white-ringed marker on the overlay at a screen position. */
4764
+ drawPointMarker(px, py, color) {
4765
+ const ctx = this.overlayCtx;
4766
+ ctx.save();
4767
+ ctx.fillStyle = color;
4768
+ ctx.strokeStyle = "#ffffff";
4769
+ ctx.lineWidth = 1.5;
4770
+ ctx.beginPath();
4771
+ ctx.arc(px, py, 4, 0, Math.PI * 2);
4772
+ ctx.fill();
4773
+ ctx.stroke();
4774
+ ctx.restore();
4775
+ }
4776
+ renderHover(sq) {
4777
+ const cursor = this.hoverPx;
4778
+ const best = this.pickNearest(sq, cursor.x, cursor.y);
4779
+ const within = best != null && best.d2 <= 24 * 24;
4780
+ if (this.pointInfo === "hover") {
4781
+ this.selected = within ? { entry: best.entry, index: best.index } : null;
4782
+ }
4783
+ if (!within) {
4784
+ this.tooltip.style.display = "none";
4785
+ return;
4786
+ }
4787
+ this.drawPointMarker(best.px, best.py, best.entry.layer.colorCss);
4788
+ this.updateTooltip(best.entry, best.theta, best.r, cursor);
4789
+ }
4790
+ /** Click handler: pin the nearest point's details, or clear on empty space. */
4791
+ handleClick(sq, cursorX, cursorY) {
4792
+ if (this.pointInfo !== "click") return;
4793
+ const hit = this.pickNearest(sq, cursorX, cursorY);
4794
+ this.selected = hit && hit.d2 <= 14 * 14 ? { entry: hit.entry, index: hit.index } : null;
4795
+ this.requestRender();
4796
+ }
4797
+ /** Draw the pinned point's marker and position its info box (or hide it). */
4798
+ updateInfoBox(sq) {
4799
+ const box = this.infoBox;
4800
+ if (!this.selected) {
4801
+ box.style.display = "none";
4802
+ return;
4803
+ }
4804
+ const { entry, index } = this.selected;
4805
+ if (index >= Math.min(entry.theta.length, entry.r.length)) {
4806
+ this.selected = null;
4807
+ box.style.display = "none";
4808
+ return;
4809
+ }
4810
+ const pr = sq.side / 2;
4811
+ const a = entry.theta[index] * this.toRad + this.rotation, rr = entry.r[index];
4812
+ const px = sq.cx + rr * Math.cos(a) / this.R * pr;
4813
+ const py = sq.cy - rr * Math.sin(a) / this.R * pr;
4814
+ if (px < sq.left || px > sq.left + sq.side || py < sq.top || py > sq.top + sq.side) {
4815
+ box.style.display = "none";
4816
+ return;
4817
+ }
4818
+ this.drawPointMarker(px, py, entry.layer.colorCss);
4819
+ box.replaceChildren();
4820
+ const label = entry.labels && index < entry.labels.length ? entry.labels[index] : void 0;
4821
+ const title = document.createElement("div");
4822
+ title.style.fontWeight = "600";
4823
+ title.style.marginBottom = "2px";
4824
+ title.textContent = label != null && label !== "" ? label : entry.layer.name;
4825
+ box.appendChild(title);
4826
+ const unit = this.toRad === 1 ? "" : "\xB0";
4827
+ const rRow = document.createElement("div");
4828
+ rRow.textContent = `r = ${fmt(rr)}`;
4829
+ const tRow = document.createElement("div");
4830
+ tRow.textContent = `\u03B8 = ${fmt(entry.theta[index])}${unit}`;
4831
+ box.appendChild(rRow);
4832
+ box.appendChild(tRow);
4833
+ box.style.display = "block";
4834
+ const cw = this.container.clientWidth, ch = this.container.clientHeight;
4835
+ const tw = box.offsetWidth, th = box.offsetHeight;
4836
+ let left = px + 12;
4837
+ if (left + tw > cw) left = px - tw - 12;
4838
+ let top = py + 12;
4839
+ if (top + th > ch) top = py - th - 12;
4840
+ box.style.left = `${Math.max(0, left)}px`;
4841
+ box.style.top = `${Math.max(0, top)}px`;
4842
+ }
4843
+ updateTooltip(entry, theta, r, cursor) {
4844
+ const tip = this.tooltip;
4845
+ tip.replaceChildren();
4846
+ const head = document.createElement("div");
4847
+ head.style.display = "flex";
4848
+ head.style.alignItems = "center";
4849
+ head.style.gap = "6px";
4850
+ head.style.marginBottom = "3px";
4851
+ const dot = document.createElement("span");
4852
+ Object.assign(dot.style, {
4853
+ width: "8px",
4854
+ height: "8px",
4855
+ borderRadius: "50%",
4856
+ background: entry.layer.colorCss,
4857
+ flex: "0 0 auto"
4858
+ });
4859
+ const nameEl = document.createElement("span");
4860
+ nameEl.textContent = entry.layer.name;
4861
+ head.appendChild(dot);
4862
+ head.appendChild(nameEl);
4863
+ tip.appendChild(head);
4864
+ const unit = this.toRad === 1 ? "" : "\xB0";
4865
+ const rRow = document.createElement("div");
4866
+ rRow.textContent = `r = ${fmt(r)}`;
4867
+ const tRow = document.createElement("div");
4868
+ tRow.textContent = `\u03B8 = ${fmt(theta)}${unit}`;
4869
+ tip.appendChild(rRow);
4870
+ tip.appendChild(tRow);
4871
+ tip.style.display = "block";
4872
+ const cw = this.container.clientWidth;
4873
+ const tw = tip.offsetWidth, th = tip.offsetHeight;
4874
+ let left = cursor.x + 14;
4875
+ if (left + tw > cw) left = cursor.x - tw - 14;
4876
+ let top = cursor.y + 14;
4877
+ if (top + th > this.container.clientHeight) top = cursor.y - th - 14;
4878
+ tip.style.left = `${Math.max(0, left)}px`;
4879
+ tip.style.top = `${Math.max(0, top)}px`;
4880
+ }
4881
+ // ---- Interaction ----------------------------------------------------------
4882
+ attachInteraction() {
4883
+ const el = this.overlayCanvas;
4884
+ el.style.touchAction = "none";
4885
+ el.style.cursor = "grab";
4886
+ el.addEventListener(
4887
+ "wheel",
4888
+ (e) => {
4889
+ e.preventDefault();
4890
+ const factor = Math.exp(e.deltaY * 1e-3);
4891
+ this.zoom = Math.max(1e-3, Math.min(1e3, this.zoom * factor));
4892
+ this.R = this.baseR * this.zoom;
4893
+ this.requestRender();
4894
+ },
4895
+ { passive: false }
4896
+ );
4897
+ let rotating = false;
4898
+ let lastAngle = 0;
4899
+ let downX = 0, downY = 0;
4900
+ const cursorAngle = (px, py) => {
4901
+ const sq = this.square();
4902
+ return Math.atan2(sq.cy - py, px - sq.cx);
4903
+ };
4904
+ el.addEventListener("pointerdown", (e) => {
4905
+ el.setPointerCapture(e.pointerId);
4906
+ const rect = el.getBoundingClientRect();
4907
+ downX = e.clientX - rect.left;
4908
+ downY = e.clientY - rect.top;
4909
+ rotating = true;
4910
+ lastAngle = cursorAngle(downX, downY);
4911
+ this.hoverPx = null;
4912
+ el.style.cursor = "grabbing";
4913
+ });
4914
+ el.addEventListener("pointermove", (e) => {
4915
+ const rect = el.getBoundingClientRect();
4916
+ const px = e.clientX - rect.left;
4917
+ const py = e.clientY - rect.top;
4918
+ if (rotating) {
4919
+ const ang = cursorAngle(px, py);
4920
+ let d = ang - lastAngle;
4921
+ if (d > Math.PI) d -= 2 * Math.PI;
4922
+ else if (d < -Math.PI) d += 2 * Math.PI;
4923
+ this.rotation += d;
4924
+ lastAngle = ang;
4925
+ this.retransform();
4926
+ this.requestRender();
4927
+ } else if (this.hoverEnabled) {
4928
+ this.setHover({ x: px, y: py });
4929
+ }
4930
+ });
4931
+ el.addEventListener("pointerleave", () => this.setHover(null));
4932
+ const end = (e, click) => {
4933
+ if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId);
4934
+ if (rotating) {
4935
+ rotating = false;
4936
+ el.style.cursor = "grab";
4937
+ }
4938
+ if (!click) return;
4939
+ const rect = el.getBoundingClientRect();
4940
+ const px = e.clientX - rect.left, py = e.clientY - rect.top;
4941
+ if (Math.hypot(px - downX, py - downY) < 4) this.handleClick(this.square(), px, py);
4942
+ };
4943
+ el.addEventListener("pointerup", (e) => end(e, true));
4944
+ el.addEventListener("pointercancel", (e) => end(e, false));
4945
+ }
4946
+ makeHomeButton() {
4947
+ const dark = this.isDark;
4948
+ const btn = document.createElement("button");
4949
+ btn.type = "button";
4950
+ btn.title = "Reset view (Home)";
4951
+ btn.setAttribute("aria-label", "Reset view (Home)");
4952
+ Object.assign(btn.style, {
4953
+ position: "absolute",
4954
+ top: "8px",
4955
+ right: "8px",
4956
+ zIndex: "5",
4957
+ display: "inline-flex",
4958
+ alignItems: "center",
4959
+ justifyContent: "center",
4960
+ width: "26px",
4961
+ height: "26px",
4962
+ padding: "0",
4963
+ borderRadius: "6px",
4964
+ background: dark ? "rgba(15,23,42,0.85)" : "rgba(255,255,255,0.9)",
4965
+ color: dark ? "#cbd5e1" : "#475569",
4966
+ border: `1px solid ${dark ? "rgba(148,163,184,0.25)" : "rgba(100,116,139,0.25)"}`,
4967
+ backdropFilter: "blur(6px)",
4968
+ boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
4969
+ cursor: "pointer"
4970
+ });
4971
+ const svgNS = "http://www.w3.org/2000/svg";
4972
+ const svg = document.createElementNS(svgNS, "svg");
4973
+ svg.setAttribute("width", "16");
4974
+ svg.setAttribute("height", "16");
4975
+ svg.setAttribute("viewBox", "0 0 16 16");
4976
+ svg.setAttribute("aria-hidden", "true");
4977
+ const mkPath = (d) => {
4978
+ const p = document.createElementNS(svgNS, "path");
4979
+ p.setAttribute("d", d);
4980
+ p.setAttribute("fill", "none");
4981
+ p.setAttribute("stroke", "currentColor");
4982
+ p.setAttribute("stroke-width", "1.5");
4983
+ p.setAttribute("stroke-linecap", "round");
4984
+ p.setAttribute("stroke-linejoin", "round");
4985
+ return p;
4986
+ };
4987
+ svg.appendChild(mkPath("M2 7.5 8 2l6 5.5"));
4988
+ svg.appendChild(mkPath("M4 7v6.5h3.2V10h1.6v3.5H12V7"));
4989
+ btn.appendChild(svg);
4990
+ btn.addEventListener("click", () => this.home());
4991
+ this.container.appendChild(btn);
4992
+ return btn;
4993
+ }
3126
4994
  };
4995
+ function fmt(v) {
4996
+ if (!Number.isFinite(v)) return String(v);
4997
+ const a = Math.abs(v);
4998
+ if (a !== 0 && (a < 1e-3 || a >= 1e5)) return v.toExponential(2);
4999
+ return String(Math.round(v * 1e3) / 1e3);
5000
+ }
3127
5001
 
3128
5002
  // src/plot3d/mat4.ts
3129
5003
  function identity() {
@@ -3217,7 +5091,7 @@ void main() {
3217
5091
  gl_PointSize = uSize;
3218
5092
  }`
3219
5093
  );
3220
- var FRAG9 = (
5094
+ var FRAG11 = (
3221
5095
  /* glsl */
3222
5096
  `#version 300 es
3223
5097
  precision highp float;
@@ -3229,19 +5103,19 @@ void main() {
3229
5103
  outColor = vec4(vColor, 1.0);
3230
5104
  }`
3231
5105
  );
3232
- var programCache9 = /* @__PURE__ */ new WeakMap();
5106
+ var programCache10 = /* @__PURE__ */ new WeakMap();
3233
5107
  function getProgram9(gl) {
3234
- let p = programCache9.get(gl);
5108
+ let p = programCache10.get(gl);
3235
5109
  if (!p) {
3236
- p = createProgram(gl, VERT9, FRAG9);
3237
- programCache9.set(gl, p);
5110
+ p = createProgram(gl, VERT9, FRAG11);
5111
+ programCache10.set(gl, p);
3238
5112
  }
3239
5113
  return p;
3240
5114
  }
3241
- var counter9 = 0;
5115
+ var counter13 = 0;
3242
5116
  var PointCloudLayer = class {
3243
5117
  constructor(gl, opts) {
3244
- this.id = `points3d-${counter9++}`;
5118
+ this.id = `points3d-${counter13++}`;
3245
5119
  this.gl = gl;
3246
5120
  this.program = getProgram9(gl);
3247
5121
  this.size = opts.size ?? 4;
@@ -3326,7 +5200,7 @@ out vec3 vColor;
3326
5200
  out vec3 vN;
3327
5201
  void main() { vColor = aColor; vN = aNormal; gl_Position = uMVP * vec4(aPos, 1.0); }`
3328
5202
  );
3329
- var FRAG10 = (
5203
+ var FRAG12 = (
3330
5204
  /* glsl */
3331
5205
  `#version 300 es
3332
5206
  precision highp float;
@@ -3341,21 +5215,21 @@ void main() {
3341
5215
  outColor = vec4(vColor * shade, 1.0);
3342
5216
  }`
3343
5217
  );
3344
- var programCache10 = /* @__PURE__ */ new WeakMap();
5218
+ var programCache11 = /* @__PURE__ */ new WeakMap();
3345
5219
  function getProgram10(gl) {
3346
- let p = programCache10.get(gl);
5220
+ let p = programCache11.get(gl);
3347
5221
  if (!p) {
3348
- p = createProgram(gl, VERT10, FRAG10);
3349
- programCache10.set(gl, p);
5222
+ p = createProgram(gl, VERT10, FRAG12);
5223
+ programCache11.set(gl, p);
3350
5224
  }
3351
5225
  return p;
3352
5226
  }
3353
- var counter10 = 0;
5227
+ var counter14 = 0;
3354
5228
  var SurfaceLayer = class {
3355
5229
  constructor(gl, opts) {
3356
5230
  this.lightDir = [0.5, 1, 0.35];
3357
5231
  this.ambient = 0.35;
3358
- this.id = `surface-${counter10++}`;
5232
+ this.id = `surface-${counter14++}`;
3359
5233
  this.gl = gl;
3360
5234
  this.program = getProgram10(gl);
3361
5235
  const { cols, rows, values } = opts;
@@ -3825,7 +5699,9 @@ export {
3825
5699
  Axis,
3826
5700
  BarLayer,
3827
5701
  BoxLayer,
5702
+ CandlestickLayer,
3828
5703
  ContourLayer,
5704
+ ErrorBarLayer,
3829
5705
  HeatmapLayer,
3830
5706
  HexbinLayer,
3831
5707
  LineLayer,
@@ -3835,12 +5711,17 @@ export {
3835
5711
  Plot3D,
3836
5712
  PointCloudLayer,
3837
5713
  PolarPlot,
5714
+ QuiverLayer,
3838
5715
  ScatterLayer,
5716
+ StemLayer,
3839
5717
  SurfaceLayer,
5718
+ TRANSFORM_GLSL,
5719
+ TRANSFORM_UNIFORMS,
3840
5720
  TimeScale,
3841
5721
  autoTicks,
3842
5722
  boxStats,
3843
5723
  colormap,
5724
+ createProgram,
3844
5725
  createToolbar,
3845
5726
  darkTheme,
3846
5727
  defaultFormat,
@@ -3852,8 +5733,10 @@ export {
3852
5733
  parseColor,
3853
5734
  quantileSorted,
3854
5735
  resolveTicks,
5736
+ setTransformUniforms,
3855
5737
  spectrogram,
3856
5738
  toColorCss,
5739
+ uniformLocations,
3857
5740
  withMinorTicks
3858
5741
  };
3859
5742
  //# sourceMappingURL=index.js.map