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