@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.d.ts +434 -15
- package/dist/index.js +2049 -166
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -82,10 +82,10 @@ var Axis = class {
|
|
|
82
82
|
}
|
|
83
83
|
ticks = major;
|
|
84
84
|
}
|
|
85
|
-
const
|
|
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 ??
|
|
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
|
|
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,
|
|
1140
|
+
p = createProgram(gl, VERT4, FRAG5);
|
|
951
1141
|
programCache4.set(gl, p);
|
|
952
1142
|
}
|
|
953
1143
|
return p;
|
|
954
1144
|
}
|
|
955
|
-
var
|
|
1145
|
+
var counter5 = 0;
|
|
956
1146
|
var ContourLayer = class {
|
|
957
1147
|
constructor(gl, opts) {
|
|
958
|
-
this.id = `contour-${
|
|
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
|
|
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,
|
|
1483
|
+
p = createProgram(gl, VERT5, FRAG6);
|
|
1070
1484
|
programCache5.set(gl, p);
|
|
1071
1485
|
}
|
|
1072
1486
|
return p;
|
|
1073
1487
|
}
|
|
1074
|
-
var
|
|
1488
|
+
var counter7 = 0;
|
|
1075
1489
|
var HeatmapLayer = class {
|
|
1076
1490
|
constructor(gl, opts) {
|
|
1077
|
-
this.id = `heatmap-${
|
|
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
|
|
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,
|
|
1631
|
+
p = createProgram(gl, VERT6, FRAG7);
|
|
1218
1632
|
programCache6.set(gl, p);
|
|
1219
1633
|
}
|
|
1220
1634
|
return p;
|
|
1221
1635
|
}
|
|
1222
|
-
var
|
|
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-${
|
|
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
|
-
|
|
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
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
uniform
|
|
1337
|
-
uniform
|
|
1338
|
-
uniform
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
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
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
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
|
|
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
|
|
2128
|
+
var programCache8 = /* @__PURE__ */ new WeakMap();
|
|
1390
2129
|
function getProgram7(gl) {
|
|
1391
|
-
let p =
|
|
2130
|
+
let p = programCache8.get(gl);
|
|
1392
2131
|
if (!p) {
|
|
1393
|
-
p = createProgram(gl, VERT7,
|
|
1394
|
-
|
|
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
|
|
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-${
|
|
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.
|
|
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
|
-
|
|
1517
|
-
|
|
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.
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
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,
|
|
1604
|
-
this.decSegments =
|
|
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
|
|
1612
|
-
const
|
|
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
|
|
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
|
|
2705
|
+
var programCache9 = /* @__PURE__ */ new WeakMap();
|
|
1674
2706
|
function getProgram8(gl) {
|
|
1675
|
-
let p =
|
|
2707
|
+
let p = programCache9.get(gl);
|
|
1676
2708
|
if (!p) {
|
|
1677
|
-
p = createProgram(gl, VERT8,
|
|
1678
|
-
|
|
2709
|
+
p = createProgram(gl, VERT8, FRAG10);
|
|
2710
|
+
programCache9.set(gl, p);
|
|
1679
2711
|
}
|
|
1680
2712
|
return p;
|
|
1681
2713
|
}
|
|
1682
|
-
var
|
|
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-${
|
|
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
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
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.
|
|
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.
|
|
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 ?? "
|
|
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.
|
|
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.
|
|
2647
|
-
|
|
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
|
-
|
|
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
|
|
2665
|
-
|
|
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
|
-
|
|
2670
|
-
|
|
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
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
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
|
|
2751
|
-
this.scaleX.domain = [this.scaleX.
|
|
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
|
|
4105
|
+
const f = dyPx / region.height;
|
|
2756
4106
|
for (const ya of this.yAxes.values()) {
|
|
2757
4107
|
if (id && ya.id !== id) continue;
|
|
2758
|
-
|
|
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
|
-
|
|
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
|
|
2935
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3030
|
-
|
|
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
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
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
|
|
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
|
|
5106
|
+
var programCache10 = /* @__PURE__ */ new WeakMap();
|
|
3233
5107
|
function getProgram9(gl) {
|
|
3234
|
-
let p =
|
|
5108
|
+
let p = programCache10.get(gl);
|
|
3235
5109
|
if (!p) {
|
|
3236
|
-
p = createProgram(gl, VERT9,
|
|
3237
|
-
|
|
5110
|
+
p = createProgram(gl, VERT9, FRAG11);
|
|
5111
|
+
programCache10.set(gl, p);
|
|
3238
5112
|
}
|
|
3239
5113
|
return p;
|
|
3240
5114
|
}
|
|
3241
|
-
var
|
|
5115
|
+
var counter13 = 0;
|
|
3242
5116
|
var PointCloudLayer = class {
|
|
3243
5117
|
constructor(gl, opts) {
|
|
3244
|
-
this.id = `points3d-${
|
|
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
|
|
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
|
|
5218
|
+
var programCache11 = /* @__PURE__ */ new WeakMap();
|
|
3345
5219
|
function getProgram10(gl) {
|
|
3346
|
-
let p =
|
|
5220
|
+
let p = programCache11.get(gl);
|
|
3347
5221
|
if (!p) {
|
|
3348
|
-
p = createProgram(gl, VERT10,
|
|
3349
|
-
|
|
5222
|
+
p = createProgram(gl, VERT10, FRAG12);
|
|
5223
|
+
programCache11.set(gl, p);
|
|
3350
5224
|
}
|
|
3351
5225
|
return p;
|
|
3352
5226
|
}
|
|
3353
|
-
var
|
|
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-${
|
|
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
|