resolvcharts 0.2.0 → 0.4.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/README.md +53 -3
- package/dist/index.cjs +643 -155
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +50 -10
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +50 -10
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +643 -156
- package/dist/index.mjs.map +1 -1
- package/dist/resolvcharts.global.min.js +192 -22
- package/dist/resolvcharts.global.min.js.map +1 -1
- package/dist/series-PmAjztEh.cjs +1191 -0
- package/dist/series-PmAjztEh.cjs.map +1 -0
- package/dist/series-wyqYpkXm.mjs +1191 -0
- package/dist/series-wyqYpkXm.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/series-BdprL87H.mjs +0 -479
- package/dist/series-BdprL87H.mjs.map +0 -1
- package/dist/series-J_5Z6dah.cjs +0 -479
- package/dist/series-J_5Z6dah.cjs.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -463,12 +463,12 @@ let smScratch = /* @__PURE__ */ new Float64Array(0);
|
|
|
463
463
|
* Monotone cubic path (Fritsch–Carlson tangents) — never overshoots data.
|
|
464
464
|
* `pts` is interleaved [x0, y0, x1, y1, ...] with `n` points.
|
|
465
465
|
*/
|
|
466
|
-
function smoothPath(ctx, pts, n) {
|
|
466
|
+
function smoothPath(ctx, pts, n, connect = false) {
|
|
467
467
|
if (n < 3) {
|
|
468
468
|
for (let i = 0; i < n; i++) {
|
|
469
469
|
const x = pts[2 * i];
|
|
470
470
|
const y = pts[2 * i + 1];
|
|
471
|
-
if (i === 0) ctx.moveTo(x, y);
|
|
471
|
+
if (i === 0 && !connect) ctx.moveTo(x, y);
|
|
472
472
|
else ctx.lineTo(x, y);
|
|
473
473
|
}
|
|
474
474
|
return;
|
|
@@ -499,7 +499,8 @@ function smoothPath(ctx, pts, n) {
|
|
|
499
499
|
if (Math.abs(ti1) > lim) tang[i + 1] = Math.sign(ti1) * lim;
|
|
500
500
|
}
|
|
501
501
|
}
|
|
502
|
-
ctx.
|
|
502
|
+
if (connect) ctx.lineTo(pts[0], pts[1]);
|
|
503
|
+
else ctx.moveTo(pts[0], pts[1]);
|
|
503
504
|
for (let i = 0; i < segs; i++) {
|
|
504
505
|
const x0 = pts[2 * i];
|
|
505
506
|
const x1 = pts[2 * (i + 1)];
|
|
@@ -509,6 +510,27 @@ function smoothPath(ctx, pts, n) {
|
|
|
509
510
|
ctx.bezierCurveTo(x0 + h, y0 + h * tang[i], x1 - h, y1 - h * tang[i + 1], x1, y1);
|
|
510
511
|
}
|
|
511
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* Trace `n` interleaved [x, y] pixel points into `path` honoring `curve`
|
|
515
|
+
* (linear / step / smooth). `connect` continues the current subpath with a
|
|
516
|
+
* lineTo instead of starting a new one — used for closed band shapes.
|
|
517
|
+
*/
|
|
518
|
+
function traceCurve(path, pts, n, curve, connect = false) {
|
|
519
|
+
if (n === 0) return;
|
|
520
|
+
const join = (x, y) => connect ? path.lineTo(x, y) : path.moveTo(x, y);
|
|
521
|
+
if (curve === "step") {
|
|
522
|
+
join(pts[0], pts[1]);
|
|
523
|
+
for (let i = 1; i < n; i++) {
|
|
524
|
+
const x = pts[2 * i];
|
|
525
|
+
path.lineTo(x, pts[2 * i - 1]);
|
|
526
|
+
path.lineTo(x, pts[2 * i + 1]);
|
|
527
|
+
}
|
|
528
|
+
} else if (curve === "smooth") smoothPath(path, pts, n, connect);
|
|
529
|
+
else {
|
|
530
|
+
join(pts[0], pts[1]);
|
|
531
|
+
for (let i = 1; i < n; i++) path.lineTo(pts[2 * i], pts[2 * i + 1]);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
512
534
|
//#endregion
|
|
513
535
|
//#region src/charts/line.ts
|
|
514
536
|
/** fraction of the tween the trailing edge lags behind the leading point */
|
|
@@ -537,19 +559,7 @@ function buildPath(r, s, yFallback) {
|
|
|
537
559
|
lastX: 0,
|
|
538
560
|
lastY: 0
|
|
539
561
|
};
|
|
540
|
-
|
|
541
|
-
if (curve === "step") {
|
|
542
|
-
path.moveTo(pts[0], pts[1]);
|
|
543
|
-
for (let i = 1; i < dn; i++) {
|
|
544
|
-
const x = pts[2 * i];
|
|
545
|
-
path.lineTo(x, pts[2 * i - 1]);
|
|
546
|
-
path.lineTo(x, pts[2 * i + 1]);
|
|
547
|
-
}
|
|
548
|
-
} else if (curve === "smooth") smoothPath(path, pts, dn);
|
|
549
|
-
else {
|
|
550
|
-
path.moveTo(pts[0], pts[1]);
|
|
551
|
-
for (let i = 1; i < dn; i++) path.lineTo(pts[2 * i], pts[2 * i + 1]);
|
|
552
|
-
}
|
|
562
|
+
traceCurve(path, pts, dn, r.opts.curve ?? "linear");
|
|
553
563
|
return {
|
|
554
564
|
path,
|
|
555
565
|
firstX: pts[0],
|
|
@@ -676,6 +686,72 @@ function renderBaseline(r) {
|
|
|
676
686
|
ctx.setLineDash([]);
|
|
677
687
|
}
|
|
678
688
|
//#endregion
|
|
689
|
+
//#region src/charts/bubble.ts
|
|
690
|
+
const MIN_R = 3;
|
|
691
|
+
const MAX_R = 16;
|
|
692
|
+
/**
|
|
693
|
+
* Bubble chart: scatter points sized by each series' `r` channel, mapped
|
|
694
|
+
* linearly to [MIN_R, MAX_R] px across the dataset's radius extent.
|
|
695
|
+
*/
|
|
696
|
+
function renderBubble(r) {
|
|
697
|
+
if (!isXY(r.data)) return;
|
|
698
|
+
const d = r.data;
|
|
699
|
+
const ctx = r.ctx;
|
|
700
|
+
const t = r.t;
|
|
701
|
+
const lerp = t < 1;
|
|
702
|
+
const fallback = r.yScale.domain[0];
|
|
703
|
+
let lo = Infinity;
|
|
704
|
+
let hi = -Infinity;
|
|
705
|
+
for (const s of d.series) {
|
|
706
|
+
const rr = s.r;
|
|
707
|
+
if (!rr) continue;
|
|
708
|
+
for (let i = 0; i < rr.length; i++) {
|
|
709
|
+
const v = rr[i];
|
|
710
|
+
if (v < lo) lo = v;
|
|
711
|
+
if (v > hi) hi = v;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (lo === Infinity) {
|
|
715
|
+
lo = 0;
|
|
716
|
+
hi = 1;
|
|
717
|
+
}
|
|
718
|
+
const span = hi - lo || 1;
|
|
719
|
+
const grow = lerp ? t : 1;
|
|
720
|
+
const rad = (v) => (MIN_R + (v - lo) / span * 13) * grow;
|
|
721
|
+
const xMin = r.plot.x - MAX_R;
|
|
722
|
+
const xMax = r.plot.x + r.plot.w + MAX_R;
|
|
723
|
+
ctx.save();
|
|
724
|
+
ctx.beginPath();
|
|
725
|
+
ctx.rect(r.plot.x, r.plot.y, r.plot.w, r.plot.h);
|
|
726
|
+
ctx.clip();
|
|
727
|
+
for (const s of d.series) {
|
|
728
|
+
const y = s.y;
|
|
729
|
+
const prev = s.prevY;
|
|
730
|
+
const rr = s.r;
|
|
731
|
+
ctx.fillStyle = withAlpha(s.color, .45);
|
|
732
|
+
ctx.strokeStyle = s.color;
|
|
733
|
+
ctx.lineWidth = 1.5;
|
|
734
|
+
ctx.beginPath();
|
|
735
|
+
for (let i = 0; i < d.sortedX.length; i++) {
|
|
736
|
+
const x = r.xScale(d.sortedX[i]);
|
|
737
|
+
if (x < xMin || x > xMax) continue;
|
|
738
|
+
let yv = y[i];
|
|
739
|
+
if (lerp) {
|
|
740
|
+
const p = prev ? prev[i] : fallback;
|
|
741
|
+
yv = p + (yv - p) * t;
|
|
742
|
+
}
|
|
743
|
+
const yp = r.yScale(yv);
|
|
744
|
+
const pr = rad(rr ? rr[i] : lo + span / 2);
|
|
745
|
+
if (pr <= 0) continue;
|
|
746
|
+
ctx.moveTo(x + pr, yp);
|
|
747
|
+
ctx.arc(x, yp, pr, 0, Math.PI * 2);
|
|
748
|
+
}
|
|
749
|
+
ctx.fill();
|
|
750
|
+
ctx.stroke();
|
|
751
|
+
}
|
|
752
|
+
ctx.restore();
|
|
753
|
+
}
|
|
754
|
+
//#endregion
|
|
679
755
|
//#region src/charts/candle.ts
|
|
680
756
|
function renderCandles(r, ohlcStyle) {
|
|
681
757
|
if (r.data.kind !== "ohlc") return;
|
|
@@ -850,20 +926,7 @@ function hitCandle(r, px) {
|
|
|
850
926
|
let ovScratch = /* @__PURE__ */ new Float64Array(0);
|
|
851
927
|
function overlayPath(r, pts, n) {
|
|
852
928
|
const path = new Path2D();
|
|
853
|
-
|
|
854
|
-
const curve = r.opts.curve ?? "linear";
|
|
855
|
-
if (curve === "step") {
|
|
856
|
-
path.moveTo(pts[0], pts[1]);
|
|
857
|
-
for (let i = 1; i < n; i++) {
|
|
858
|
-
const x = pts[2 * i];
|
|
859
|
-
path.lineTo(x, pts[2 * i - 1]);
|
|
860
|
-
path.lineTo(x, pts[2 * i + 1]);
|
|
861
|
-
}
|
|
862
|
-
} else if (curve === "smooth") smoothPath(path, pts, n);
|
|
863
|
-
else {
|
|
864
|
-
path.moveTo(pts[0], pts[1]);
|
|
865
|
-
for (let i = 1; i < n; i++) path.lineTo(pts[2 * i], pts[2 * i + 1]);
|
|
866
|
-
}
|
|
929
|
+
traceCurve(path, pts, n, r.opts.curve ?? "linear");
|
|
867
930
|
return path;
|
|
868
931
|
}
|
|
869
932
|
function renderCombo(r) {
|
|
@@ -1016,14 +1079,16 @@ function formatSI(v) {
|
|
|
1016
1079
|
//#region src/charts/donut.ts
|
|
1017
1080
|
const INNER = .65;
|
|
1018
1081
|
const GAP = .015;
|
|
1019
|
-
const START = -Math.PI / 2;
|
|
1082
|
+
const START$1 = -Math.PI / 2;
|
|
1083
|
+
/** donut keeps a center hole; pie fills to the middle */
|
|
1084
|
+
const innerRatio = (r) => r.opts.type === "pie" ? 0 : INNER;
|
|
1020
1085
|
function donutGeometry(r, d, t) {
|
|
1021
1086
|
const cx = r.plot.x + r.plot.w / 2;
|
|
1022
1087
|
const cy = r.plot.y + r.plot.h / 2;
|
|
1023
1088
|
const rOuter = Math.max(1, Math.min(r.plot.w, r.plot.h) / 2);
|
|
1024
|
-
const rInner = rOuter *
|
|
1089
|
+
const rInner = rOuter * innerRatio(r);
|
|
1025
1090
|
const angles = [];
|
|
1026
|
-
let a = START;
|
|
1091
|
+
let a = START$1;
|
|
1027
1092
|
const sweep = Math.PI * 2 * Math.min(1, Math.max(0, t));
|
|
1028
1093
|
for (const s of d.slices) {
|
|
1029
1094
|
const frac = d.total > 0 ? s.value / d.total : 0;
|
|
@@ -1056,12 +1121,13 @@ function renderDonut(r) {
|
|
|
1056
1121
|
if (a1 <= a0) continue;
|
|
1057
1122
|
ctx.beginPath();
|
|
1058
1123
|
ctx.arc(geo.cx, geo.cy, geo.rOuter, a0, a1);
|
|
1059
|
-
ctx.arc(geo.cx, geo.cy, geo.rInner, a1, a0, true);
|
|
1124
|
+
if (geo.rInner > 0) ctx.arc(geo.cx, geo.cy, geo.rInner, a1, a0, true);
|
|
1125
|
+
else ctx.lineTo(geo.cx, geo.cy);
|
|
1060
1126
|
ctx.closePath();
|
|
1061
1127
|
ctx.fillStyle = s.color;
|
|
1062
1128
|
ctx.fill();
|
|
1063
1129
|
}
|
|
1064
|
-
if (d.slices.length > 0 && r.t > .5) {
|
|
1130
|
+
if (r.opts.type !== "pie" && d.slices.length > 0 && r.t > .5) {
|
|
1065
1131
|
ctx.fillStyle = r.theme.text;
|
|
1066
1132
|
ctx.font = `600 20px ${r.theme.fontFamily}`;
|
|
1067
1133
|
ctx.textAlign = "center";
|
|
@@ -1078,8 +1144,8 @@ function hitDonut(r, px, py) {
|
|
|
1078
1144
|
const dist = Math.hypot(dx, dy);
|
|
1079
1145
|
if (dist < geo.rInner || dist > geo.rOuter) return null;
|
|
1080
1146
|
let angle = Math.atan2(dy, dx);
|
|
1081
|
-
while (angle < START) angle += Math.PI * 2;
|
|
1082
|
-
while (angle >= START + Math.PI * 2) angle -= Math.PI * 2;
|
|
1147
|
+
while (angle < START$1) angle += Math.PI * 2;
|
|
1148
|
+
while (angle >= START$1 + Math.PI * 2) angle -= Math.PI * 2;
|
|
1083
1149
|
for (let i = 0; i < geo.angles.length; i++) {
|
|
1084
1150
|
const an = geo.angles[i];
|
|
1085
1151
|
const s = d.slices[i];
|
|
@@ -1098,6 +1164,389 @@ function hitDonut(r, px, py) {
|
|
|
1098
1164
|
return null;
|
|
1099
1165
|
}
|
|
1100
1166
|
//#endregion
|
|
1167
|
+
//#region src/core/theme.ts
|
|
1168
|
+
const FONT = "Inter, system-ui, -apple-system, \"Segoe UI\", sans-serif";
|
|
1169
|
+
/** Built-in light theme.
|
|
1170
|
+
* @category Theming
|
|
1171
|
+
*/
|
|
1172
|
+
const lightTheme = {
|
|
1173
|
+
bg: "#ffffff",
|
|
1174
|
+
text: "#1f1f1f",
|
|
1175
|
+
textMuted: "#8a8a8a",
|
|
1176
|
+
grid: "rgba(0,0,0,0.07)",
|
|
1177
|
+
axisBaseline: "rgba(0,0,0,0.15)",
|
|
1178
|
+
crosshair: "rgba(0,0,0,0.25)",
|
|
1179
|
+
series: ["#0070f3"],
|
|
1180
|
+
contextGray: "#b0b0b0",
|
|
1181
|
+
up: "#0e9f6e",
|
|
1182
|
+
down: "#e5484d",
|
|
1183
|
+
tooltipBg: "#ffffff",
|
|
1184
|
+
tooltipText: "#1f1f1f",
|
|
1185
|
+
tooltipMuted: "#8a8a8a",
|
|
1186
|
+
tooltipBorder: "rgba(0,0,0,0.08)",
|
|
1187
|
+
tooltipShadow: "0 4px 14px rgba(0,0,0,0.08)",
|
|
1188
|
+
fontFamily: FONT
|
|
1189
|
+
};
|
|
1190
|
+
/** Built-in dark theme.
|
|
1191
|
+
* @category Theming
|
|
1192
|
+
*/
|
|
1193
|
+
const darkTheme = {
|
|
1194
|
+
bg: "#0a0a0a",
|
|
1195
|
+
text: "#ededed",
|
|
1196
|
+
textMuted: "#8a8a8a",
|
|
1197
|
+
grid: "rgba(255,255,255,0.07)",
|
|
1198
|
+
axisBaseline: "rgba(255,255,255,0.18)",
|
|
1199
|
+
crosshair: "rgba(255,255,255,0.3)",
|
|
1200
|
+
series: ["#3b9dff"],
|
|
1201
|
+
contextGray: "#5c5c5c",
|
|
1202
|
+
up: "#34c98e",
|
|
1203
|
+
down: "#f2555a",
|
|
1204
|
+
tooltipBg: "#161616",
|
|
1205
|
+
tooltipText: "#ededed",
|
|
1206
|
+
tooltipMuted: "#8a8a8a",
|
|
1207
|
+
tooltipBorder: "rgba(255,255,255,0.1)",
|
|
1208
|
+
tooltipShadow: "0 4px 14px rgba(0,0,0,0.4)",
|
|
1209
|
+
fontFamily: FONT
|
|
1210
|
+
};
|
|
1211
|
+
const categoricalLight = [
|
|
1212
|
+
"#0070f3",
|
|
1213
|
+
"#0e9f6e",
|
|
1214
|
+
"#8e4ec6",
|
|
1215
|
+
"#f5a623"
|
|
1216
|
+
];
|
|
1217
|
+
const categoricalDark = [
|
|
1218
|
+
"#3b9dff",
|
|
1219
|
+
"#34c98e",
|
|
1220
|
+
"#a97ef0",
|
|
1221
|
+
"#f5b84d"
|
|
1222
|
+
];
|
|
1223
|
+
/** Merge a theme option (`"light" | "dark" | partial tokens`) into full {@link ThemeTokens}.
|
|
1224
|
+
* @category Theming
|
|
1225
|
+
*/
|
|
1226
|
+
function resolveTheme(opt, accent) {
|
|
1227
|
+
const base = opt === "dark" ? {
|
|
1228
|
+
...darkTheme,
|
|
1229
|
+
series: [...categoricalDark]
|
|
1230
|
+
} : opt === "light" || opt === void 0 ? {
|
|
1231
|
+
...lightTheme,
|
|
1232
|
+
series: [...categoricalLight]
|
|
1233
|
+
} : {
|
|
1234
|
+
...lightTheme,
|
|
1235
|
+
series: [...categoricalLight],
|
|
1236
|
+
...opt
|
|
1237
|
+
};
|
|
1238
|
+
if (accent) base.series[0] = accent;
|
|
1239
|
+
return base;
|
|
1240
|
+
}
|
|
1241
|
+
function seriesColor(theme, index, explicit) {
|
|
1242
|
+
if (explicit) return explicit;
|
|
1243
|
+
return theme.series[index % theme.series.length];
|
|
1244
|
+
}
|
|
1245
|
+
//#endregion
|
|
1246
|
+
//#region src/charts/funnel.ts
|
|
1247
|
+
const ROW_GAP = 2;
|
|
1248
|
+
const LABEL_MIN_W = 72;
|
|
1249
|
+
/**
|
|
1250
|
+
* Funnel: one stage per `categories` entry using the first series' values.
|
|
1251
|
+
* Each stage is a trapezoid whose bottom edge matches the next stage's top
|
|
1252
|
+
* width — the classic funnel silhouette.
|
|
1253
|
+
*/
|
|
1254
|
+
function stages(r, d, t) {
|
|
1255
|
+
const s = d.series[0];
|
|
1256
|
+
if (!s) return [];
|
|
1257
|
+
const n = d.categories.length;
|
|
1258
|
+
let max = 0;
|
|
1259
|
+
for (let i = 0; i < s.values.length; i++) {
|
|
1260
|
+
const v = s.values[i];
|
|
1261
|
+
if (v > max) max = v;
|
|
1262
|
+
}
|
|
1263
|
+
max ||= 1;
|
|
1264
|
+
const rowH = r.plot.h / Math.max(1, n);
|
|
1265
|
+
const out = [];
|
|
1266
|
+
for (let i = 0; i < n; i++) {
|
|
1267
|
+
const cur = s.values[i] ?? 0;
|
|
1268
|
+
const prev = s.prevValues?.[i] ?? 0;
|
|
1269
|
+
const v = t < 1 ? prev + (cur - prev) * t : cur;
|
|
1270
|
+
const cur1 = s.values[i + 1] ?? cur;
|
|
1271
|
+
const prev1 = s.prevValues?.[i + 1] ?? 0;
|
|
1272
|
+
const v1 = i + 1 < n ? t < 1 ? prev1 + (cur1 - prev1) * t : cur1 : v;
|
|
1273
|
+
out.push({
|
|
1274
|
+
cat: d.categories[i] ?? "",
|
|
1275
|
+
value: cur,
|
|
1276
|
+
top: r.plot.y + rowH * i,
|
|
1277
|
+
bot: r.plot.y + rowH * (i + 1) - ROW_GAP,
|
|
1278
|
+
w0: Math.max(0, v) / max * r.plot.w,
|
|
1279
|
+
w1: Math.max(0, v1) / max * r.plot.w,
|
|
1280
|
+
color: seriesColor(r.theme, i, void 0)
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
return out;
|
|
1284
|
+
}
|
|
1285
|
+
function stageRect(r, st) {
|
|
1286
|
+
const cx = r.plot.x + r.plot.w / 2;
|
|
1287
|
+
const w = Math.max(st.w0, st.w1);
|
|
1288
|
+
return {
|
|
1289
|
+
x: cx - w / 2,
|
|
1290
|
+
y: st.top,
|
|
1291
|
+
w,
|
|
1292
|
+
h: st.bot - st.top
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
function renderFunnel(r) {
|
|
1296
|
+
if (r.data.kind !== "category") return;
|
|
1297
|
+
const d = r.data;
|
|
1298
|
+
const ctx = r.ctx;
|
|
1299
|
+
const cx = r.plot.x + r.plot.w / 2;
|
|
1300
|
+
for (const st of stages(r, d, r.t)) {
|
|
1301
|
+
ctx.beginPath();
|
|
1302
|
+
ctx.moveTo(cx - st.w0 / 2, st.top);
|
|
1303
|
+
ctx.lineTo(cx + st.w0 / 2, st.top);
|
|
1304
|
+
ctx.lineTo(cx + st.w1 / 2, st.bot);
|
|
1305
|
+
ctx.lineTo(cx - st.w1 / 2, st.bot);
|
|
1306
|
+
ctx.closePath();
|
|
1307
|
+
ctx.fillStyle = st.color;
|
|
1308
|
+
ctx.fill();
|
|
1309
|
+
const label = `${st.cat} · ${formatSI(st.value)}`;
|
|
1310
|
+
ctx.font = `11px ${r.theme.fontFamily}`;
|
|
1311
|
+
ctx.textBaseline = "middle";
|
|
1312
|
+
if (Math.min(st.w0, st.w1) >= LABEL_MIN_W) {
|
|
1313
|
+
ctx.fillStyle = "#ffffff";
|
|
1314
|
+
ctx.textAlign = "center";
|
|
1315
|
+
ctx.fillText(label, cx, (st.top + st.bot) / 2);
|
|
1316
|
+
} else {
|
|
1317
|
+
ctx.fillStyle = r.theme.textMuted;
|
|
1318
|
+
ctx.textAlign = "left";
|
|
1319
|
+
ctx.fillText(label, cx + Math.max(st.w0, st.w1) / 2 + 8, (st.top + st.bot) / 2);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
function hitFunnel(r, px, py) {
|
|
1324
|
+
if (r.data.kind !== "category") return null;
|
|
1325
|
+
const d = r.data;
|
|
1326
|
+
const s = d.series[0];
|
|
1327
|
+
if (!s) return null;
|
|
1328
|
+
const cx = r.plot.x + r.plot.w / 2;
|
|
1329
|
+
for (const st of stages(r, d, 1)) {
|
|
1330
|
+
if (py < st.top || py > st.bot) continue;
|
|
1331
|
+
const frac = st.bot - st.top > 0 ? (py - st.top) / (st.bot - st.top) : 0;
|
|
1332
|
+
const w = st.w0 + (st.w1 - st.w0) * frac;
|
|
1333
|
+
if (Math.abs(px - cx) > w / 2 + 4) continue;
|
|
1334
|
+
return {
|
|
1335
|
+
kind: "bar",
|
|
1336
|
+
category: st.cat,
|
|
1337
|
+
seriesIndex: 0,
|
|
1338
|
+
label: s.label,
|
|
1339
|
+
color: st.color,
|
|
1340
|
+
value: st.value,
|
|
1341
|
+
px: cx,
|
|
1342
|
+
py,
|
|
1343
|
+
rect: stageRect(r, st)
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1348
|
+
//#endregion
|
|
1349
|
+
//#region src/charts/radar.ts
|
|
1350
|
+
const RINGS = 4;
|
|
1351
|
+
const LABEL_PAD = 16;
|
|
1352
|
+
const MIN_AXES = 3;
|
|
1353
|
+
const START = -Math.PI / 2;
|
|
1354
|
+
function radarGeo(r, d) {
|
|
1355
|
+
const cx = r.plot.x + r.plot.w / 2;
|
|
1356
|
+
const cy = r.plot.y + r.plot.h / 2;
|
|
1357
|
+
const radius = Math.max(1, Math.min(r.plot.w, r.plot.h) / 2 - LABEL_PAD);
|
|
1358
|
+
let max = 0;
|
|
1359
|
+
for (const s of d.series) for (let i = 0; i < s.values.length; i++) {
|
|
1360
|
+
const v = s.values[i];
|
|
1361
|
+
if (v > max) max = v;
|
|
1362
|
+
}
|
|
1363
|
+
return {
|
|
1364
|
+
cx,
|
|
1365
|
+
cy,
|
|
1366
|
+
radius,
|
|
1367
|
+
max: max || 1
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
const angleOf = (i, n) => START + i / n * Math.PI * 2;
|
|
1371
|
+
function renderRadar(r) {
|
|
1372
|
+
if (r.data.kind !== "category") return;
|
|
1373
|
+
const d = r.data;
|
|
1374
|
+
const n = d.categories.length;
|
|
1375
|
+
if (n < MIN_AXES) return;
|
|
1376
|
+
const ctx = r.ctx;
|
|
1377
|
+
const { cx, cy, radius, max } = radarGeo(r, d);
|
|
1378
|
+
const animate = r.t < 1;
|
|
1379
|
+
ctx.strokeStyle = r.theme.grid;
|
|
1380
|
+
ctx.lineWidth = 1;
|
|
1381
|
+
for (let ring = 1; ring <= RINGS; ring++) {
|
|
1382
|
+
const rr = radius * ring / RINGS;
|
|
1383
|
+
ctx.beginPath();
|
|
1384
|
+
for (let i = 0; i <= n; i++) {
|
|
1385
|
+
const a = angleOf(i % n, n);
|
|
1386
|
+
const x = cx + Math.cos(a) * rr;
|
|
1387
|
+
const y = cy + Math.sin(a) * rr;
|
|
1388
|
+
if (i === 0) ctx.moveTo(x, y);
|
|
1389
|
+
else ctx.lineTo(x, y);
|
|
1390
|
+
}
|
|
1391
|
+
ctx.stroke();
|
|
1392
|
+
}
|
|
1393
|
+
ctx.beginPath();
|
|
1394
|
+
for (let i = 0; i < n; i++) {
|
|
1395
|
+
const a = angleOf(i, n);
|
|
1396
|
+
ctx.moveTo(cx, cy);
|
|
1397
|
+
ctx.lineTo(cx + Math.cos(a) * radius, cy + Math.sin(a) * radius);
|
|
1398
|
+
}
|
|
1399
|
+
ctx.stroke();
|
|
1400
|
+
for (const s of d.series) {
|
|
1401
|
+
ctx.beginPath();
|
|
1402
|
+
for (let i = 0; i < n; i++) {
|
|
1403
|
+
const cur = s.values[i] ?? 0;
|
|
1404
|
+
const prev = s.prevValues?.[i] ?? 0;
|
|
1405
|
+
const v = animate ? prev + (cur - prev) * r.t : cur;
|
|
1406
|
+
const rr = Math.max(0, v) / max * radius;
|
|
1407
|
+
const a = angleOf(i, n);
|
|
1408
|
+
const x = cx + Math.cos(a) * rr;
|
|
1409
|
+
const y = cy + Math.sin(a) * rr;
|
|
1410
|
+
if (i === 0) ctx.moveTo(x, y);
|
|
1411
|
+
else ctx.lineTo(x, y);
|
|
1412
|
+
}
|
|
1413
|
+
ctx.closePath();
|
|
1414
|
+
ctx.fillStyle = withAlpha(s.color, .16);
|
|
1415
|
+
ctx.fill();
|
|
1416
|
+
ctx.strokeStyle = s.color;
|
|
1417
|
+
ctx.lineWidth = 2;
|
|
1418
|
+
ctx.lineJoin = "round";
|
|
1419
|
+
ctx.stroke();
|
|
1420
|
+
ctx.fillStyle = s.color;
|
|
1421
|
+
ctx.beginPath();
|
|
1422
|
+
for (let i = 0; i < n; i++) {
|
|
1423
|
+
const cur = s.values[i] ?? 0;
|
|
1424
|
+
const prev = s.prevValues?.[i] ?? 0;
|
|
1425
|
+
const v = animate ? prev + (cur - prev) * r.t : cur;
|
|
1426
|
+
const rr = Math.max(0, v) / max * radius;
|
|
1427
|
+
const a = angleOf(i, n);
|
|
1428
|
+
const x = cx + Math.cos(a) * rr;
|
|
1429
|
+
const y = cy + Math.sin(a) * rr;
|
|
1430
|
+
ctx.moveTo(x + 2.5, y);
|
|
1431
|
+
ctx.arc(x, y, 2.5, 0, Math.PI * 2);
|
|
1432
|
+
}
|
|
1433
|
+
ctx.fill();
|
|
1434
|
+
}
|
|
1435
|
+
ctx.fillStyle = r.theme.textMuted;
|
|
1436
|
+
ctx.font = `11px ${r.theme.fontFamily}`;
|
|
1437
|
+
for (let i = 0; i < n; i++) {
|
|
1438
|
+
const a = angleOf(i, n);
|
|
1439
|
+
const lx = cx + Math.cos(a) * (radius + LABEL_PAD - 4);
|
|
1440
|
+
const ly = cy + Math.sin(a) * (radius + LABEL_PAD - 4);
|
|
1441
|
+
ctx.textAlign = Math.cos(a) > .3 ? "left" : Math.cos(a) < -.3 ? "right" : "center";
|
|
1442
|
+
ctx.textBaseline = Math.sin(a) > .3 ? "top" : Math.sin(a) < -.3 ? "bottom" : "middle";
|
|
1443
|
+
ctx.fillText(d.categories[i] ?? "", lx, ly);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
function hitRadar(r, px, py) {
|
|
1447
|
+
if (r.data.kind !== "category") return null;
|
|
1448
|
+
const d = r.data;
|
|
1449
|
+
const n = d.categories.length;
|
|
1450
|
+
if (n < MIN_AXES) return null;
|
|
1451
|
+
const { cx, cy, radius } = radarGeo(r, d);
|
|
1452
|
+
const dx = px - cx;
|
|
1453
|
+
const dy = py - cy;
|
|
1454
|
+
if (Math.hypot(dx, dy) > radius + LABEL_PAD) return null;
|
|
1455
|
+
let rel = Math.atan2(dy, dx) - START;
|
|
1456
|
+
while (rel < 0) rel += Math.PI * 2;
|
|
1457
|
+
while (rel >= Math.PI * 2) rel -= Math.PI * 2;
|
|
1458
|
+
const i = Math.round(rel / (Math.PI * 2) * n) % n;
|
|
1459
|
+
return {
|
|
1460
|
+
kind: "point",
|
|
1461
|
+
index: i,
|
|
1462
|
+
x: i,
|
|
1463
|
+
px,
|
|
1464
|
+
py,
|
|
1465
|
+
values: d.series.map((s) => ({
|
|
1466
|
+
label: s.label,
|
|
1467
|
+
color: s.color,
|
|
1468
|
+
y: s.values[i] ?? 0
|
|
1469
|
+
})),
|
|
1470
|
+
title: d.categories[i] ?? ""
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
/** Hover accent: highlighted spoke + vertex dots (replaces the xy crosshair). */
|
|
1474
|
+
function drawRadarHover(r, info) {
|
|
1475
|
+
if (r.data.kind !== "category" || info.kind !== "point") return;
|
|
1476
|
+
const d = r.data;
|
|
1477
|
+
const n = d.categories.length;
|
|
1478
|
+
if (info.index < 0 || info.index >= n) return;
|
|
1479
|
+
const o = r.backend.overlay;
|
|
1480
|
+
const { cx, cy, radius, max } = radarGeo(r, d);
|
|
1481
|
+
const a = angleOf(info.index, n);
|
|
1482
|
+
o.strokeStyle = r.theme.crosshair;
|
|
1483
|
+
o.lineWidth = 1;
|
|
1484
|
+
o.beginPath();
|
|
1485
|
+
o.moveTo(cx, cy);
|
|
1486
|
+
o.lineTo(cx + Math.cos(a) * radius, cy + Math.sin(a) * radius);
|
|
1487
|
+
o.stroke();
|
|
1488
|
+
for (const s of d.series) {
|
|
1489
|
+
const v = s.values[info.index] ?? 0;
|
|
1490
|
+
const rr = Math.max(0, v) / max * radius;
|
|
1491
|
+
o.fillStyle = s.color;
|
|
1492
|
+
o.strokeStyle = r.theme.bg;
|
|
1493
|
+
o.lineWidth = 2;
|
|
1494
|
+
o.beginPath();
|
|
1495
|
+
o.arc(cx + Math.cos(a) * rr, cy + Math.sin(a) * rr, 4, 0, Math.PI * 2);
|
|
1496
|
+
o.fill();
|
|
1497
|
+
o.stroke();
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
//#endregion
|
|
1501
|
+
//#region src/charts/rangearea.ts
|
|
1502
|
+
/** pooled scratch for the reversed lower edge — consumed within the call */
|
|
1503
|
+
let revScratch = /* @__PURE__ */ new Float64Array(0);
|
|
1504
|
+
/**
|
|
1505
|
+
* Range-area chart: a filled band between each series' `y` (lower edge) and
|
|
1506
|
+
* `y2` (upper edge) — confidence intervals, min/max envelopes. Series
|
|
1507
|
+
* without `y2` are skipped.
|
|
1508
|
+
*/
|
|
1509
|
+
function renderRangeArea(r) {
|
|
1510
|
+
if (!isXY(r.data)) return;
|
|
1511
|
+
const d = r.data;
|
|
1512
|
+
const ctx = r.ctx;
|
|
1513
|
+
const curve = r.opts.curve ?? "linear";
|
|
1514
|
+
ctx.save();
|
|
1515
|
+
ctx.beginPath();
|
|
1516
|
+
ctx.rect(r.plot.x, r.plot.y, r.plot.w, r.plot.h);
|
|
1517
|
+
ctx.clip();
|
|
1518
|
+
for (const s of d.series) {
|
|
1519
|
+
if (!s.y2) continue;
|
|
1520
|
+
const hi = seriesPoints(r, {
|
|
1521
|
+
y: s.y2,
|
|
1522
|
+
prevY: s.prevY2
|
|
1523
|
+
});
|
|
1524
|
+
const lo = seriesPoints(r, s);
|
|
1525
|
+
const n = Math.min(hi.length, lo.length) / 2;
|
|
1526
|
+
if (n === 0) continue;
|
|
1527
|
+
if (revScratch.length < n * 2) revScratch = new Float64Array(n * 2);
|
|
1528
|
+
for (let i = 0; i < n; i++) {
|
|
1529
|
+
revScratch[2 * i] = lo[2 * (n - 1 - i)];
|
|
1530
|
+
revScratch[2 * i + 1] = lo[2 * (n - 1 - i) + 1];
|
|
1531
|
+
}
|
|
1532
|
+
const band = new Path2D();
|
|
1533
|
+
traceCurve(band, hi, n, curve);
|
|
1534
|
+
traceCurve(band, revScratch, n, curve, true);
|
|
1535
|
+
band.closePath();
|
|
1536
|
+
ctx.fillStyle = withAlpha(s.color, .2);
|
|
1537
|
+
ctx.fill(band);
|
|
1538
|
+
ctx.strokeStyle = s.color;
|
|
1539
|
+
ctx.lineWidth = 1.5;
|
|
1540
|
+
ctx.lineJoin = "round";
|
|
1541
|
+
ctx.lineCap = "round";
|
|
1542
|
+
const edges = new Path2D();
|
|
1543
|
+
traceCurve(edges, hi, n, curve);
|
|
1544
|
+
traceCurve(edges, lo, n, curve);
|
|
1545
|
+
ctx.stroke(edges);
|
|
1546
|
+
}
|
|
1547
|
+
ctx.restore();
|
|
1548
|
+
}
|
|
1549
|
+
//#endregion
|
|
1101
1550
|
//#region src/charts/scatter.ts
|
|
1102
1551
|
function renderScatter(r) {
|
|
1103
1552
|
if (!isXY(r.data)) return;
|
|
@@ -1381,6 +1830,11 @@ function padDomain(min, max, includeZero = false) {
|
|
|
1381
1830
|
}
|
|
1382
1831
|
return [lo, hi];
|
|
1383
1832
|
}
|
|
1833
|
+
/** @category Utilities
|
|
1834
|
+
* Pin domain ends — each defined bound overrides the fitted value. */
|
|
1835
|
+
function boundDomain(dom, min, max) {
|
|
1836
|
+
return [min ?? dom[0], max ?? dom[1]];
|
|
1837
|
+
}
|
|
1384
1838
|
//#endregion
|
|
1385
1839
|
//#region src/core/scheduler.ts
|
|
1386
1840
|
const raf = typeof requestAnimationFrame === "function" ? (cb) => requestAnimationFrame(cb) : (cb) => setTimeout(() => cb(Date.now()), 16);
|
|
@@ -1418,85 +1872,6 @@ var Scheduler = class {
|
|
|
1418
1872
|
const sharedScheduler = new Scheduler();
|
|
1419
1873
|
const easeOutCubic = (t) => 1 - (1 - t) ** 3;
|
|
1420
1874
|
//#endregion
|
|
1421
|
-
//#region src/core/theme.ts
|
|
1422
|
-
const FONT = "Inter, system-ui, -apple-system, \"Segoe UI\", sans-serif";
|
|
1423
|
-
/** Built-in light theme.
|
|
1424
|
-
* @category Theming
|
|
1425
|
-
*/
|
|
1426
|
-
const lightTheme = {
|
|
1427
|
-
bg: "#ffffff",
|
|
1428
|
-
text: "#1f1f1f",
|
|
1429
|
-
textMuted: "#8a8a8a",
|
|
1430
|
-
grid: "rgba(0,0,0,0.07)",
|
|
1431
|
-
axisBaseline: "rgba(0,0,0,0.15)",
|
|
1432
|
-
crosshair: "rgba(0,0,0,0.25)",
|
|
1433
|
-
series: ["#0070f3"],
|
|
1434
|
-
contextGray: "#b0b0b0",
|
|
1435
|
-
up: "#0e9f6e",
|
|
1436
|
-
down: "#e5484d",
|
|
1437
|
-
tooltipBg: "#ffffff",
|
|
1438
|
-
tooltipText: "#1f1f1f",
|
|
1439
|
-
tooltipMuted: "#8a8a8a",
|
|
1440
|
-
tooltipBorder: "rgba(0,0,0,0.08)",
|
|
1441
|
-
tooltipShadow: "0 4px 14px rgba(0,0,0,0.08)",
|
|
1442
|
-
fontFamily: FONT
|
|
1443
|
-
};
|
|
1444
|
-
/** Built-in dark theme.
|
|
1445
|
-
* @category Theming
|
|
1446
|
-
*/
|
|
1447
|
-
const darkTheme = {
|
|
1448
|
-
bg: "#0a0a0a",
|
|
1449
|
-
text: "#ededed",
|
|
1450
|
-
textMuted: "#8a8a8a",
|
|
1451
|
-
grid: "rgba(255,255,255,0.07)",
|
|
1452
|
-
axisBaseline: "rgba(255,255,255,0.18)",
|
|
1453
|
-
crosshair: "rgba(255,255,255,0.3)",
|
|
1454
|
-
series: ["#3b9dff"],
|
|
1455
|
-
contextGray: "#5c5c5c",
|
|
1456
|
-
up: "#34c98e",
|
|
1457
|
-
down: "#f2555a",
|
|
1458
|
-
tooltipBg: "#161616",
|
|
1459
|
-
tooltipText: "#ededed",
|
|
1460
|
-
tooltipMuted: "#8a8a8a",
|
|
1461
|
-
tooltipBorder: "rgba(255,255,255,0.1)",
|
|
1462
|
-
tooltipShadow: "0 4px 14px rgba(0,0,0,0.4)",
|
|
1463
|
-
fontFamily: FONT
|
|
1464
|
-
};
|
|
1465
|
-
const categoricalLight = [
|
|
1466
|
-
"#0070f3",
|
|
1467
|
-
"#0e9f6e",
|
|
1468
|
-
"#8e4ec6",
|
|
1469
|
-
"#f5a623"
|
|
1470
|
-
];
|
|
1471
|
-
const categoricalDark = [
|
|
1472
|
-
"#3b9dff",
|
|
1473
|
-
"#34c98e",
|
|
1474
|
-
"#a97ef0",
|
|
1475
|
-
"#f5b84d"
|
|
1476
|
-
];
|
|
1477
|
-
/** Merge a theme option (`"light" | "dark" | partial tokens`) into full {@link ThemeTokens}.
|
|
1478
|
-
* @category Theming
|
|
1479
|
-
*/
|
|
1480
|
-
function resolveTheme(opt, accent) {
|
|
1481
|
-
const base = opt === "dark" ? {
|
|
1482
|
-
...darkTheme,
|
|
1483
|
-
series: [...categoricalDark]
|
|
1484
|
-
} : opt === "light" || opt === void 0 ? {
|
|
1485
|
-
...lightTheme,
|
|
1486
|
-
series: [...categoricalLight]
|
|
1487
|
-
} : {
|
|
1488
|
-
...lightTheme,
|
|
1489
|
-
series: [...categoricalLight],
|
|
1490
|
-
...opt
|
|
1491
|
-
};
|
|
1492
|
-
if (accent) base.series[0] = accent;
|
|
1493
|
-
return base;
|
|
1494
|
-
}
|
|
1495
|
-
function seriesColor(theme, index, explicit) {
|
|
1496
|
-
if (explicit) return explicit;
|
|
1497
|
-
return theme.series[index % theme.series.length];
|
|
1498
|
-
}
|
|
1499
|
-
//#endregion
|
|
1500
1875
|
//#region src/core/ticks.ts
|
|
1501
1876
|
const STEPS = [
|
|
1502
1877
|
1,
|
|
@@ -1636,6 +2011,36 @@ const MODULES = {
|
|
|
1636
2011
|
axes: true,
|
|
1637
2012
|
defaultTooltip: true
|
|
1638
2013
|
},
|
|
2014
|
+
bubble: {
|
|
2015
|
+
render: renderBubble,
|
|
2016
|
+
hit: (r, px) => hitXY(r, px),
|
|
2017
|
+
axes: true,
|
|
2018
|
+
defaultTooltip: true
|
|
2019
|
+
},
|
|
2020
|
+
rangearea: {
|
|
2021
|
+
render: renderRangeArea,
|
|
2022
|
+
hit: (r, px) => hitXY(r, px),
|
|
2023
|
+
axes: true,
|
|
2024
|
+
defaultTooltip: true
|
|
2025
|
+
},
|
|
2026
|
+
pie: {
|
|
2027
|
+
render: renderDonut,
|
|
2028
|
+
hit: hitDonut,
|
|
2029
|
+
axes: false,
|
|
2030
|
+
defaultTooltip: true
|
|
2031
|
+
},
|
|
2032
|
+
radar: {
|
|
2033
|
+
render: renderRadar,
|
|
2034
|
+
hit: hitRadar,
|
|
2035
|
+
axes: false,
|
|
2036
|
+
defaultTooltip: true
|
|
2037
|
+
},
|
|
2038
|
+
funnel: {
|
|
2039
|
+
render: renderFunnel,
|
|
2040
|
+
hit: hitFunnel,
|
|
2041
|
+
axes: false,
|
|
2042
|
+
defaultTooltip: true
|
|
2043
|
+
},
|
|
1639
2044
|
bar: {
|
|
1640
2045
|
render: renderBar,
|
|
1641
2046
|
hit: hitBar,
|
|
@@ -1696,6 +2101,8 @@ const GPU_KIND = {
|
|
|
1696
2101
|
};
|
|
1697
2102
|
/** points retained beyond the window's left edge so they slide off-screen instead of vanishing mid-plot */
|
|
1698
2103
|
const EDGE_KEEP = 3;
|
|
2104
|
+
/** default point count at which `renderer: "auto"` engages the GPU path */
|
|
2105
|
+
const GPU_THRESHOLD = 1e5;
|
|
1699
2106
|
/** Chart instance. Create via {@link createChart}.
|
|
1700
2107
|
* @category Charts
|
|
1701
2108
|
*/
|
|
@@ -1721,6 +2128,8 @@ var Chart = class {
|
|
|
1721
2128
|
this.destroyed = false;
|
|
1722
2129
|
this.pendingView = false;
|
|
1723
2130
|
this.gpu = null;
|
|
2131
|
+
this.gpuInit = false;
|
|
2132
|
+
this.dataRev = 0;
|
|
1724
2133
|
this.lastAppendAt = 0;
|
|
1725
2134
|
this.estInterval = 400;
|
|
1726
2135
|
this.onScroll = () => {
|
|
@@ -1783,7 +2192,35 @@ var Chart = class {
|
|
|
1783
2192
|
this.reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
1784
2193
|
if (options.tooltip ?? this.mod.defaultTooltip) this.tooltip = new Tooltip(el, this.theme);
|
|
1785
2194
|
this.data = this.normalize(options.data, null);
|
|
1786
|
-
|
|
2195
|
+
this.maybeInitGpu();
|
|
2196
|
+
this.bindEvents();
|
|
2197
|
+
const dom = this.layout();
|
|
2198
|
+
this.view = {
|
|
2199
|
+
x: [...dom.x],
|
|
2200
|
+
y: [...dom.y]
|
|
2201
|
+
};
|
|
2202
|
+
this.viewTo = {
|
|
2203
|
+
x: [...dom.x],
|
|
2204
|
+
y: [...dom.y]
|
|
2205
|
+
};
|
|
2206
|
+
this.startAnimation(dom);
|
|
2207
|
+
}
|
|
2208
|
+
/**
|
|
2209
|
+
* Lazily acquire the WebGPU backend. `"webgpu"` attaches unconditionally;
|
|
2210
|
+
* `"auto"` waits until the xy point count crosses `gpuThreshold` — small
|
|
2211
|
+
* charts never pay adapter/device acquisition.
|
|
2212
|
+
*/
|
|
2213
|
+
maybeInitGpu() {
|
|
2214
|
+
if (this.gpu || this.gpuInit || this.destroyed) return;
|
|
2215
|
+
if (!GPU_KIND[this.options.type]) return;
|
|
2216
|
+
const r = this.options.renderer ?? "auto";
|
|
2217
|
+
if (r !== "auto" && r !== "webgpu") return;
|
|
2218
|
+
if (r === "auto") {
|
|
2219
|
+
const d = this.data;
|
|
2220
|
+
if ((d.kind === "xy" ? d.sortedX.length * Math.max(1, d.series.length) : 0) < (this.options.gpuThreshold ?? GPU_THRESHOLD)) return;
|
|
2221
|
+
}
|
|
2222
|
+
this.gpuInit = true;
|
|
2223
|
+
Promise.resolve().then(() => require("./series-PmAjztEh.cjs")).then(({ GpuSeries }) => GpuSeries.create(this.options.gpu).then((g) => {
|
|
1787
2224
|
if (!g || this.destroyed) {
|
|
1788
2225
|
g?.destroy();
|
|
1789
2226
|
return;
|
|
@@ -1796,17 +2233,6 @@ var Chart = class {
|
|
|
1796
2233
|
return false;
|
|
1797
2234
|
});
|
|
1798
2235
|
}));
|
|
1799
|
-
this.bindEvents();
|
|
1800
|
-
const dom = this.layout();
|
|
1801
|
-
this.view = {
|
|
1802
|
-
x: [...dom.x],
|
|
1803
|
-
y: [...dom.y]
|
|
1804
|
-
};
|
|
1805
|
-
this.viewTo = {
|
|
1806
|
-
x: [...dom.x],
|
|
1807
|
-
y: [...dom.y]
|
|
1808
|
-
};
|
|
1809
|
-
this.startAnimation(dom);
|
|
1810
2236
|
}
|
|
1811
2237
|
normalize(data, prev) {
|
|
1812
2238
|
const t = this.options.type;
|
|
@@ -1878,7 +2304,7 @@ var Chart = class {
|
|
|
1878
2304
|
bars
|
|
1879
2305
|
};
|
|
1880
2306
|
}
|
|
1881
|
-
if (t === "donut") {
|
|
2307
|
+
if (t === "donut" || t === "pie") {
|
|
1882
2308
|
const slices = data.slices.map((s, i) => ({
|
|
1883
2309
|
label: s.label,
|
|
1884
2310
|
value: s.value,
|
|
@@ -1890,7 +2316,7 @@ var Chart = class {
|
|
|
1890
2316
|
total: slices.reduce((a, s) => a + Math.max(0, s.value), 0)
|
|
1891
2317
|
};
|
|
1892
2318
|
}
|
|
1893
|
-
if (t === "bar" || t === "combo") {
|
|
2319
|
+
if (t === "bar" || t === "combo" || t === "radar" || t === "funnel") {
|
|
1894
2320
|
const d = data;
|
|
1895
2321
|
const prevSeries = prev?.kind === "category" ? prev.series : [];
|
|
1896
2322
|
return {
|
|
@@ -1922,15 +2348,22 @@ var Chart = class {
|
|
|
1922
2348
|
kind: "xy",
|
|
1923
2349
|
sortedX,
|
|
1924
2350
|
series: d.series.map((s, i) => {
|
|
1925
|
-
const
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
2351
|
+
const chan = (src) => {
|
|
2352
|
+
if (!src) return null;
|
|
2353
|
+
const out = new Float64Array(n);
|
|
2354
|
+
if (idx) for (let k = 0; k < n; k++) out[k] = src[idx[k]];
|
|
2355
|
+
else for (let k = 0; k < n; k++) out[k] = src[k];
|
|
2356
|
+
return out;
|
|
2357
|
+
};
|
|
2358
|
+
const y = chan(s.y) ?? new Float64Array(n);
|
|
1929
2359
|
return {
|
|
1930
2360
|
label: s.label ?? `Series ${i + 1}`,
|
|
1931
2361
|
color: seriesColor(this.theme, i, s.color),
|
|
1932
2362
|
y,
|
|
1933
2363
|
prevY: prevSeries[i]?.y ?? null,
|
|
2364
|
+
r: chan(s.r),
|
|
2365
|
+
y2: chan(s.y2),
|
|
2366
|
+
prevY2: prevSeries[i]?.y2 ?? null,
|
|
1934
2367
|
pathCache: null
|
|
1935
2368
|
};
|
|
1936
2369
|
})
|
|
@@ -1994,6 +2427,12 @@ var Chart = class {
|
|
|
1994
2427
|
if (v < min) min = v;
|
|
1995
2428
|
if (v > max) max = v;
|
|
1996
2429
|
}
|
|
2430
|
+
const y2 = s.y2;
|
|
2431
|
+
if (y2) for (let i = Math.min(x0, y2.length); i < y2.length; i++) {
|
|
2432
|
+
const v = y2[i];
|
|
2433
|
+
if (v < min) min = v;
|
|
2434
|
+
if (v > max) max = v;
|
|
2435
|
+
}
|
|
1997
2436
|
}
|
|
1998
2437
|
if (min === Infinity) {
|
|
1999
2438
|
min = 0;
|
|
@@ -2002,7 +2441,7 @@ var Chart = class {
|
|
|
2002
2441
|
const includeZero = this.options.type === "area" || this.options.type === "sparkline";
|
|
2003
2442
|
return {
|
|
2004
2443
|
x,
|
|
2005
|
-
y: padDomain(min, max, includeZero)
|
|
2444
|
+
y: this.yDomain(padDomain(min, max, includeZero))
|
|
2006
2445
|
};
|
|
2007
2446
|
}
|
|
2008
2447
|
if (d.kind === "category") {
|
|
@@ -2038,7 +2477,7 @@ var Chart = class {
|
|
|
2038
2477
|
}
|
|
2039
2478
|
return {
|
|
2040
2479
|
x: [0, d.categories.length],
|
|
2041
|
-
y: padDomain(min, max, true)
|
|
2480
|
+
y: this.yDomain(padDomain(min, max, true))
|
|
2042
2481
|
};
|
|
2043
2482
|
}
|
|
2044
2483
|
if (d.kind === "ohlc") {
|
|
@@ -2057,7 +2496,7 @@ var Chart = class {
|
|
|
2057
2496
|
}
|
|
2058
2497
|
return {
|
|
2059
2498
|
x,
|
|
2060
|
-
y: padDomain(min, max)
|
|
2499
|
+
y: this.yDomain(padDomain(min, max))
|
|
2061
2500
|
};
|
|
2062
2501
|
}
|
|
2063
2502
|
if (d.kind === "waterfall") {
|
|
@@ -2073,14 +2512,17 @@ var Chart = class {
|
|
|
2073
2512
|
}
|
|
2074
2513
|
return {
|
|
2075
2514
|
x: [0, d.bars.length],
|
|
2076
|
-
y: padDomain(min, max, true)
|
|
2515
|
+
y: this.yDomain(padDomain(min, max, true))
|
|
2077
2516
|
};
|
|
2078
2517
|
}
|
|
2079
2518
|
return {
|
|
2080
2519
|
x: [0, 1],
|
|
2081
|
-
y: [0, 1]
|
|
2520
|
+
y: this.yDomain([0, 1])
|
|
2082
2521
|
};
|
|
2083
2522
|
}
|
|
2523
|
+
yDomain(y) {
|
|
2524
|
+
return boundDomain(y, this.options.yMin, this.options.yMax);
|
|
2525
|
+
}
|
|
2084
2526
|
lerpView(eased) {
|
|
2085
2527
|
const f = this.viewFrom;
|
|
2086
2528
|
const to = this.viewTo;
|
|
@@ -2182,30 +2624,35 @@ var Chart = class {
|
|
|
2182
2624
|
else this.mod.render(this.renderContext(t, scales));
|
|
2183
2625
|
if (this.hover) this.drawHover(this.hover, scales.yScale);
|
|
2184
2626
|
}
|
|
2185
|
-
/**
|
|
2627
|
+
/**
|
|
2628
|
+
* Series rasterization on the GPU; grid/labels/hover stay on Canvas2D.
|
|
2629
|
+
* Raw data lives in GPU buffers — the per-frame cost is uniform writes +
|
|
2630
|
+
* instanced draws, with compute decimation above the density threshold.
|
|
2631
|
+
*/
|
|
2186
2632
|
gpuRender(t, scales) {
|
|
2187
2633
|
const g = this.gpu;
|
|
2188
2634
|
const d = this.data;
|
|
2189
2635
|
const kind = GPU_KIND[this.options.type];
|
|
2190
|
-
if (!g || !kind || d.kind !== "xy") {
|
|
2636
|
+
if (!g || g.unavailable || !kind || d.kind !== "xy") {
|
|
2191
2637
|
this.mod.render(this.renderContext(t, scales));
|
|
2192
2638
|
return;
|
|
2193
2639
|
}
|
|
2194
|
-
|
|
2640
|
+
g.syncData(d, this.dataRev);
|
|
2641
|
+
const view = {
|
|
2642
|
+
x: scales.xScale.domain,
|
|
2643
|
+
y: scales.yScale.domain
|
|
2644
|
+
};
|
|
2195
2645
|
const flow = this.options.flow ? FLOW_RIPPLE : 0;
|
|
2196
|
-
g.begin(this.backend.plot);
|
|
2197
|
-
for (
|
|
2646
|
+
g.begin(this.backend.plot, t, flow);
|
|
2647
|
+
for (let i = 0; i < d.series.length; i++) {
|
|
2648
|
+
const s = d.series[i];
|
|
2649
|
+
if (!s) continue;
|
|
2198
2650
|
if (kind === "points") {
|
|
2199
|
-
|
|
2200
|
-
g.points(pts, s.color, 2.5, .65);
|
|
2651
|
+
g.points(i, s.color, 2.5, .65, view);
|
|
2201
2652
|
continue;
|
|
2202
2653
|
}
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
const base = scales.yScale(Math.max(0, scales.yScale.domain[0]));
|
|
2206
|
-
g.area(pts, s.color, base, this.backend.plot);
|
|
2207
|
-
}
|
|
2208
|
-
g.line(pts, s.color, 2);
|
|
2654
|
+
if (kind === "area") g.area(i, s.color, Math.max(0, scales.yScale.domain[0]), view);
|
|
2655
|
+
g.line(i, s.color, 2, view);
|
|
2209
2656
|
}
|
|
2210
2657
|
g.end();
|
|
2211
2658
|
}
|
|
@@ -2213,6 +2660,10 @@ var Chart = class {
|
|
|
2213
2660
|
const o = this.backend.overlay;
|
|
2214
2661
|
const plot = this.backend.plot;
|
|
2215
2662
|
this.backend.beginOverlay();
|
|
2663
|
+
if (info.kind === "point" && this.options.type === "radar") {
|
|
2664
|
+
drawRadarHover(this.renderContext(1), info);
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2216
2667
|
if (info.kind === "point" || info.kind === "candle") {
|
|
2217
2668
|
o.strokeStyle = this.theme.crosshair;
|
|
2218
2669
|
o.lineWidth = 1;
|
|
@@ -2243,7 +2694,7 @@ var Chart = class {
|
|
|
2243
2694
|
const cx = plot.x + plot.w / 2;
|
|
2244
2695
|
const cy = plot.y + plot.h / 2;
|
|
2245
2696
|
const rOuter = Math.min(plot.w, plot.h) / 2;
|
|
2246
|
-
const rInner = rOuter * .65;
|
|
2697
|
+
const rInner = this.options.type === "pie" ? 0 : rOuter * .65;
|
|
2247
2698
|
let a = -Math.PI / 2;
|
|
2248
2699
|
for (let i = 0; i < d.slices.length; i++) {
|
|
2249
2700
|
const s = d.slices[i];
|
|
@@ -2340,6 +2791,8 @@ var Chart = class {
|
|
|
2340
2791
|
setData(data) {
|
|
2341
2792
|
const prev = this.data;
|
|
2342
2793
|
this.data = this.normalize(data, prev);
|
|
2794
|
+
this.dataRev++;
|
|
2795
|
+
this.maybeInitGpu();
|
|
2343
2796
|
const dom = this.layout();
|
|
2344
2797
|
this.startAnimation(dom);
|
|
2345
2798
|
}
|
|
@@ -2350,7 +2803,7 @@ var Chart = class {
|
|
|
2350
2803
|
*/
|
|
2351
2804
|
append(x, y) {
|
|
2352
2805
|
const d = this.data;
|
|
2353
|
-
if (d.kind !== "xy") throw new Error("[resolvcharts] append() requires xy data (line/area/scatter/sparkline)");
|
|
2806
|
+
if (d.kind !== "xy") throw new Error("[resolvcharts] append() requires xy data (line/area/scatter/bubble/rangearea/sparkline)");
|
|
2354
2807
|
const nowT = typeof performance === "undefined" ? Date.now() : performance.now();
|
|
2355
2808
|
if (this.lastAppendAt > 0) {
|
|
2356
2809
|
const dt = nowT - this.lastAppendAt;
|
|
@@ -2392,11 +2845,30 @@ var Chart = class {
|
|
|
2392
2845
|
prevY.set(s.y.subarray(drop));
|
|
2393
2846
|
prevY[m - 1] = m > 1 ? yNew[m - 2] : yNew[m - 1];
|
|
2394
2847
|
}
|
|
2848
|
+
let rNew = null;
|
|
2849
|
+
if (s.r) {
|
|
2850
|
+
rNew = new Float64Array(m);
|
|
2851
|
+
rNew.set(s.r.subarray(drop));
|
|
2852
|
+
rNew[m - 1] = s.r[n - 1];
|
|
2853
|
+
}
|
|
2854
|
+
let y2New = null;
|
|
2855
|
+
let prevY2 = null;
|
|
2856
|
+
if (s.y2) {
|
|
2857
|
+
y2New = new Float64Array(m);
|
|
2858
|
+
y2New.set(s.y2.subarray(drop));
|
|
2859
|
+
y2New[m - 1] = s.y2[n - 1];
|
|
2860
|
+
prevY2 = new Float64Array(m);
|
|
2861
|
+
prevY2.set(s.y2.subarray(drop));
|
|
2862
|
+
prevY2[m - 1] = m > 1 ? y2New[m - 2] : y2New[m - 1];
|
|
2863
|
+
}
|
|
2395
2864
|
return {
|
|
2396
2865
|
label: s.label,
|
|
2397
2866
|
color: s.color,
|
|
2398
2867
|
y: yNew,
|
|
2399
2868
|
prevY,
|
|
2869
|
+
r: rNew,
|
|
2870
|
+
y2: y2New,
|
|
2871
|
+
prevY2,
|
|
2400
2872
|
pathCache: null
|
|
2401
2873
|
};
|
|
2402
2874
|
});
|
|
@@ -2405,8 +2877,14 @@ var Chart = class {
|
|
|
2405
2877
|
sortedX,
|
|
2406
2878
|
series
|
|
2407
2879
|
};
|
|
2408
|
-
|
|
2880
|
+
const resort = m > 1 && x < sortedX[m - 2];
|
|
2881
|
+
if (resort) resortXY(nd);
|
|
2409
2882
|
this.data = nd;
|
|
2883
|
+
if (resort) this.dataRev++;
|
|
2884
|
+
else if (this.gpu && !this.gpu.unavailable) {
|
|
2885
|
+
if (!this.gpu.appended(x, d.series.map((s, i) => ys[i] ?? s.y[n - 1]), d.series.map((s) => s.y[n - 1]), drop)) this.dataRev++;
|
|
2886
|
+
}
|
|
2887
|
+
this.maybeInitGpu();
|
|
2410
2888
|
this.pendingView = true;
|
|
2411
2889
|
sharedScheduler.add(this.frame);
|
|
2412
2890
|
}
|
|
@@ -2452,13 +2930,22 @@ function resortXY(d) {
|
|
|
2452
2930
|
for (const s of d.series) {
|
|
2453
2931
|
const ny = new Float64Array(n);
|
|
2454
2932
|
const np = s.prevY ? new Float64Array(n) : null;
|
|
2933
|
+
const nr = s.r ? new Float64Array(n) : null;
|
|
2934
|
+
const ny2 = s.y2 ? new Float64Array(n) : null;
|
|
2935
|
+
const np2 = s.prevY2 ? new Float64Array(n) : null;
|
|
2455
2936
|
for (let i = 0; i < n; i++) {
|
|
2456
2937
|
const k = idx[i];
|
|
2457
2938
|
ny[i] = s.y[k];
|
|
2458
2939
|
if (np && s.prevY) np[i] = s.prevY[k];
|
|
2940
|
+
if (nr && s.r) nr[i] = s.r[k];
|
|
2941
|
+
if (ny2 && s.y2) ny2[i] = s.y2[k];
|
|
2942
|
+
if (np2 && s.prevY2) np2[i] = s.prevY2[k];
|
|
2459
2943
|
}
|
|
2460
2944
|
s.y = ny;
|
|
2461
2945
|
s.prevY = np;
|
|
2946
|
+
s.r = nr;
|
|
2947
|
+
s.y2 = ny2;
|
|
2948
|
+
s.prevY2 = np2;
|
|
2462
2949
|
}
|
|
2463
2950
|
}
|
|
2464
2951
|
/**
|
|
@@ -2482,6 +2969,7 @@ function createChart(el, options) {
|
|
|
2482
2969
|
}
|
|
2483
2970
|
//#endregion
|
|
2484
2971
|
exports.Chart = Chart;
|
|
2972
|
+
exports.boundDomain = boundDomain;
|
|
2485
2973
|
exports.computeTicks = computeTicks;
|
|
2486
2974
|
exports.createChart = createChart;
|
|
2487
2975
|
exports.darkTheme = darkTheme;
|