resolvcharts 0.3.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/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.moveTo(pts[0], pts[1]);
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
- const curve = r.opts.curve ?? "linear";
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
- if (n === 0) return path;
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 * INNER;
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;
@@ -1423,85 +1872,6 @@ var Scheduler = class {
1423
1872
  const sharedScheduler = new Scheduler();
1424
1873
  const easeOutCubic = (t) => 1 - (1 - t) ** 3;
1425
1874
  //#endregion
1426
- //#region src/core/theme.ts
1427
- const FONT = "Inter, system-ui, -apple-system, \"Segoe UI\", sans-serif";
1428
- /** Built-in light theme.
1429
- * @category Theming
1430
- */
1431
- const lightTheme = {
1432
- bg: "#ffffff",
1433
- text: "#1f1f1f",
1434
- textMuted: "#8a8a8a",
1435
- grid: "rgba(0,0,0,0.07)",
1436
- axisBaseline: "rgba(0,0,0,0.15)",
1437
- crosshair: "rgba(0,0,0,0.25)",
1438
- series: ["#0070f3"],
1439
- contextGray: "#b0b0b0",
1440
- up: "#0e9f6e",
1441
- down: "#e5484d",
1442
- tooltipBg: "#ffffff",
1443
- tooltipText: "#1f1f1f",
1444
- tooltipMuted: "#8a8a8a",
1445
- tooltipBorder: "rgba(0,0,0,0.08)",
1446
- tooltipShadow: "0 4px 14px rgba(0,0,0,0.08)",
1447
- fontFamily: FONT
1448
- };
1449
- /** Built-in dark theme.
1450
- * @category Theming
1451
- */
1452
- const darkTheme = {
1453
- bg: "#0a0a0a",
1454
- text: "#ededed",
1455
- textMuted: "#8a8a8a",
1456
- grid: "rgba(255,255,255,0.07)",
1457
- axisBaseline: "rgba(255,255,255,0.18)",
1458
- crosshair: "rgba(255,255,255,0.3)",
1459
- series: ["#3b9dff"],
1460
- contextGray: "#5c5c5c",
1461
- up: "#34c98e",
1462
- down: "#f2555a",
1463
- tooltipBg: "#161616",
1464
- tooltipText: "#ededed",
1465
- tooltipMuted: "#8a8a8a",
1466
- tooltipBorder: "rgba(255,255,255,0.1)",
1467
- tooltipShadow: "0 4px 14px rgba(0,0,0,0.4)",
1468
- fontFamily: FONT
1469
- };
1470
- const categoricalLight = [
1471
- "#0070f3",
1472
- "#0e9f6e",
1473
- "#8e4ec6",
1474
- "#f5a623"
1475
- ];
1476
- const categoricalDark = [
1477
- "#3b9dff",
1478
- "#34c98e",
1479
- "#a97ef0",
1480
- "#f5b84d"
1481
- ];
1482
- /** Merge a theme option (`"light" | "dark" | partial tokens`) into full {@link ThemeTokens}.
1483
- * @category Theming
1484
- */
1485
- function resolveTheme(opt, accent) {
1486
- const base = opt === "dark" ? {
1487
- ...darkTheme,
1488
- series: [...categoricalDark]
1489
- } : opt === "light" || opt === void 0 ? {
1490
- ...lightTheme,
1491
- series: [...categoricalLight]
1492
- } : {
1493
- ...lightTheme,
1494
- series: [...categoricalLight],
1495
- ...opt
1496
- };
1497
- if (accent) base.series[0] = accent;
1498
- return base;
1499
- }
1500
- function seriesColor(theme, index, explicit) {
1501
- if (explicit) return explicit;
1502
- return theme.series[index % theme.series.length];
1503
- }
1504
- //#endregion
1505
1875
  //#region src/core/ticks.ts
1506
1876
  const STEPS = [
1507
1877
  1,
@@ -1641,6 +2011,36 @@ const MODULES = {
1641
2011
  axes: true,
1642
2012
  defaultTooltip: true
1643
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
+ },
1644
2044
  bar: {
1645
2045
  render: renderBar,
1646
2046
  hit: hitBar,
@@ -1904,7 +2304,7 @@ var Chart = class {
1904
2304
  bars
1905
2305
  };
1906
2306
  }
1907
- if (t === "donut") {
2307
+ if (t === "donut" || t === "pie") {
1908
2308
  const slices = data.slices.map((s, i) => ({
1909
2309
  label: s.label,
1910
2310
  value: s.value,
@@ -1916,7 +2316,7 @@ var Chart = class {
1916
2316
  total: slices.reduce((a, s) => a + Math.max(0, s.value), 0)
1917
2317
  };
1918
2318
  }
1919
- if (t === "bar" || t === "combo") {
2319
+ if (t === "bar" || t === "combo" || t === "radar" || t === "funnel") {
1920
2320
  const d = data;
1921
2321
  const prevSeries = prev?.kind === "category" ? prev.series : [];
1922
2322
  return {
@@ -1948,15 +2348,22 @@ var Chart = class {
1948
2348
  kind: "xy",
1949
2349
  sortedX,
1950
2350
  series: d.series.map((s, i) => {
1951
- const y = new Float64Array(n);
1952
- const src = s.y;
1953
- if (idx) for (let k = 0; k < n; k++) y[k] = src[idx[k]];
1954
- else for (let k = 0; k < n; k++) y[k] = src[k];
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);
1955
2359
  return {
1956
2360
  label: s.label ?? `Series ${i + 1}`,
1957
2361
  color: seriesColor(this.theme, i, s.color),
1958
2362
  y,
1959
2363
  prevY: prevSeries[i]?.y ?? null,
2364
+ r: chan(s.r),
2365
+ y2: chan(s.y2),
2366
+ prevY2: prevSeries[i]?.y2 ?? null,
1960
2367
  pathCache: null
1961
2368
  };
1962
2369
  })
@@ -2020,6 +2427,12 @@ var Chart = class {
2020
2427
  if (v < min) min = v;
2021
2428
  if (v > max) max = v;
2022
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
+ }
2023
2436
  }
2024
2437
  if (min === Infinity) {
2025
2438
  min = 0;
@@ -2247,6 +2660,10 @@ var Chart = class {
2247
2660
  const o = this.backend.overlay;
2248
2661
  const plot = this.backend.plot;
2249
2662
  this.backend.beginOverlay();
2663
+ if (info.kind === "point" && this.options.type === "radar") {
2664
+ drawRadarHover(this.renderContext(1), info);
2665
+ return;
2666
+ }
2250
2667
  if (info.kind === "point" || info.kind === "candle") {
2251
2668
  o.strokeStyle = this.theme.crosshair;
2252
2669
  o.lineWidth = 1;
@@ -2277,7 +2694,7 @@ var Chart = class {
2277
2694
  const cx = plot.x + plot.w / 2;
2278
2695
  const cy = plot.y + plot.h / 2;
2279
2696
  const rOuter = Math.min(plot.w, plot.h) / 2;
2280
- const rInner = rOuter * .65;
2697
+ const rInner = this.options.type === "pie" ? 0 : rOuter * .65;
2281
2698
  let a = -Math.PI / 2;
2282
2699
  for (let i = 0; i < d.slices.length; i++) {
2283
2700
  const s = d.slices[i];
@@ -2386,7 +2803,7 @@ var Chart = class {
2386
2803
  */
2387
2804
  append(x, y) {
2388
2805
  const d = this.data;
2389
- 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)");
2390
2807
  const nowT = typeof performance === "undefined" ? Date.now() : performance.now();
2391
2808
  if (this.lastAppendAt > 0) {
2392
2809
  const dt = nowT - this.lastAppendAt;
@@ -2428,11 +2845,30 @@ var Chart = class {
2428
2845
  prevY.set(s.y.subarray(drop));
2429
2846
  prevY[m - 1] = m > 1 ? yNew[m - 2] : yNew[m - 1];
2430
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
+ }
2431
2864
  return {
2432
2865
  label: s.label,
2433
2866
  color: s.color,
2434
2867
  y: yNew,
2435
2868
  prevY,
2869
+ r: rNew,
2870
+ y2: y2New,
2871
+ prevY2,
2436
2872
  pathCache: null
2437
2873
  };
2438
2874
  });
@@ -2494,13 +2930,22 @@ function resortXY(d) {
2494
2930
  for (const s of d.series) {
2495
2931
  const ny = new Float64Array(n);
2496
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;
2497
2936
  for (let i = 0; i < n; i++) {
2498
2937
  const k = idx[i];
2499
2938
  ny[i] = s.y[k];
2500
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];
2501
2943
  }
2502
2944
  s.y = ny;
2503
2945
  s.prevY = np;
2946
+ s.r = nr;
2947
+ s.y2 = ny2;
2948
+ s.prevY2 = np2;
2504
2949
  }
2505
2950
  }
2506
2951
  /**