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