resolvcharts 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,11 +1,8 @@
1
- //#region src/charts/bar.ts
2
- const MAX_BAR_W$1 = 40;
3
- const RADIUS$1 = 3;
4
1
  function barGeometry(r, d, catIdx, seriesIdx, value) {
5
2
  const cats = d.categories.length;
6
3
  if (cats === 0) return null;
7
4
  const slot = r.plot.w / cats;
8
- const groupW = Math.min(slot * .8, MAX_BAR_W$1 * d.series.length);
5
+ const groupW = Math.min(slot * .8, 40 * d.series.length);
9
6
  const barW = groupW / d.series.length;
10
7
  const x = r.plot.x + slot * catIdx + (slot - groupW) / 2 + barW * seriesIdx;
11
8
  const base = r.yScale(0);
@@ -45,7 +42,7 @@ function stackedGeometry(r, d, catIdx, v0, v1) {
45
42
  const cats = d.categories.length;
46
43
  if (cats === 0) return null;
47
44
  const slot = r.plot.w / cats;
48
- const w = Math.min(slot * .8, MAX_BAR_W$1);
45
+ const w = Math.min(slot * .8, 40);
49
46
  const x = r.plot.x + slot * catIdx + (slot - w) / 2;
50
47
  const yTop = r.yScale(Math.max(v0, v1));
51
48
  const yBot = r.yScale(Math.min(v0, v1));
@@ -68,7 +65,7 @@ function renderBar(r) {
68
65
  const slot = r.plot.w / Math.max(1, cats);
69
66
  const animate = r.t < 1;
70
67
  if (r.opts.stacked) {
71
- const w = Math.min(slot * .8, MAX_BAR_W$1);
68
+ const w = Math.min(slot * .8, 40);
72
69
  for (let ci = 0; ci < cats; ci++) {
73
70
  const x = r.plot.x + slot * ci + (slot - w) / 2;
74
71
  let pos = 0;
@@ -91,7 +88,7 @@ function renderBar(r) {
91
88
  }
92
89
  }
93
90
  } else {
94
- const groupW = Math.min(slot * .8, MAX_BAR_W$1 * d.series.length);
91
+ const groupW = Math.min(slot * .8, 40 * d.series.length);
95
92
  const barW = groupW / d.series.length;
96
93
  const base = r.yScale(0);
97
94
  for (let si = 0; si < d.series.length; si++) {
@@ -112,7 +109,7 @@ function renderBar(r) {
112
109
  y,
113
110
  w: barW,
114
111
  h
115
- }, RADIUS$1, v >= 0);
112
+ }, 3, v >= 0);
116
113
  }
117
114
  }
118
115
  }
@@ -375,6 +372,7 @@ function decimateSeriesPx(xs, ys, prev, t, yFallback, xScale, yScale, pixelWidth
375
372
  * ys must be pixel-space y coordinates of equal length.
376
373
  *
377
374
  * Returns interleaved [x, y] pairs ready for path construction.
375
+ * @category Utilities
378
376
  */
379
377
  function decimateM4(xs, ys, pixelWidth) {
380
378
  const n = Math.min(xs.length, ys.length);
@@ -436,6 +434,7 @@ function decimateM4(xs, ys, pixelWidth) {
436
434
  /**
437
435
  * Binary search for the index whose value is nearest to `target`.
438
436
  * `xs` must be sorted ascending. Returns -1 for empty input.
437
+ * @category Utilities
439
438
  */
440
439
  function nearestIndex(xs, target) {
441
440
  const n = xs.length;
@@ -845,8 +844,151 @@ function hitCandle(r, px) {
845
844
  };
846
845
  }
847
846
  //#endregion
847
+ //#region src/charts/combo.ts
848
+ /** pooled scratch for overlay [x, y] pairs — consumed within the call */
849
+ let ovScratch = /* @__PURE__ */ new Float64Array(0);
850
+ function overlayPath(r, pts, n) {
851
+ 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
+ }
866
+ return path;
867
+ }
868
+ function renderCombo(r) {
869
+ if (r.data.kind !== "category") return;
870
+ const d = r.data;
871
+ const ctx = r.ctx;
872
+ ctx.save();
873
+ ctx.beginPath();
874
+ ctx.rect(r.plot.x, r.plot.y, r.plot.w, r.plot.h + 1);
875
+ ctx.clip();
876
+ const cats = d.categories.length;
877
+ const slot = r.plot.w / Math.max(1, cats);
878
+ const animate = r.t < 1;
879
+ const valAt = (s, ci) => {
880
+ const cur = s.values[ci] ?? 0;
881
+ const prev = s.prevValues?.[ci] ?? 0;
882
+ return animate ? prev + (cur - prev) * r.t : cur;
883
+ };
884
+ let nCols = 0;
885
+ for (const s of d.series) if (s.kind === "column") nCols++;
886
+ if (r.opts.stacked) {
887
+ const w = Math.min(slot * .8, 40);
888
+ for (let ci = 0; ci < cats; ci++) {
889
+ const x = r.plot.x + slot * ci + (slot - w) / 2;
890
+ let pos = 0;
891
+ let neg = 0;
892
+ for (const s of d.series) {
893
+ if (s.kind !== "column") continue;
894
+ const v = valAt(s, ci);
895
+ const v0 = v >= 0 ? pos : neg + v;
896
+ const v1 = v >= 0 ? pos + v : neg;
897
+ if (v >= 0) pos = v1;
898
+ else neg = v0;
899
+ const yTop = r.yScale(Math.max(v0, v1));
900
+ const h = Math.abs(r.yScale(Math.min(v0, v1)) - yTop);
901
+ if (h <= 0) continue;
902
+ ctx.fillStyle = s.color;
903
+ ctx.fillRect(x, yTop, w, h);
904
+ }
905
+ }
906
+ } else {
907
+ const groupW = Math.min(slot * .8, 40 * nCols);
908
+ const barW = groupW / Math.max(1, nCols);
909
+ const base = r.yScale(0);
910
+ let k = 0;
911
+ for (const s of d.series) {
912
+ if (s.kind !== "column") continue;
913
+ ctx.fillStyle = s.color;
914
+ for (let ci = 0; ci < cats; ci++) {
915
+ const v = valAt(s, ci);
916
+ const x = r.plot.x + slot * ci + (slot - groupW) / 2 + barW * k;
917
+ const yv = r.yScale(v);
918
+ const y = Math.min(base, yv);
919
+ const h = Math.abs(base - yv);
920
+ if (h <= 0) continue;
921
+ topRounded(ctx, {
922
+ x,
923
+ y,
924
+ w: barW,
925
+ h
926
+ }, 3, v >= 0);
927
+ }
928
+ k++;
929
+ }
930
+ }
931
+ const base = r.yScale(Math.max(0, r.yScale.domain[0]));
932
+ if (ovScratch.length < cats * 2) ovScratch = new Float64Array(cats * 2);
933
+ const pts = ovScratch;
934
+ for (const s of d.series) {
935
+ if (s.kind === "column") continue;
936
+ for (let ci = 0; ci < cats; ci++) {
937
+ pts[2 * ci] = r.plot.x + slot * (ci + .5);
938
+ pts[2 * ci + 1] = r.yScale(valAt(s, ci));
939
+ }
940
+ const path = overlayPath(r, pts, cats);
941
+ if (s.kind === "area" && cats > 1) {
942
+ const area = new Path2D(path);
943
+ area.lineTo(pts[2 * (cats - 1)], base);
944
+ area.lineTo(pts[0], base);
945
+ area.closePath();
946
+ const g = ctx.createLinearGradient(0, r.plot.y, 0, r.plot.y + r.plot.h);
947
+ g.addColorStop(0, withAlpha(s.color, .16));
948
+ g.addColorStop(1, withAlpha(s.color, 0));
949
+ ctx.fillStyle = g;
950
+ ctx.fill(area);
951
+ }
952
+ ctx.strokeStyle = s.color;
953
+ ctx.lineWidth = 2;
954
+ ctx.lineJoin = "round";
955
+ ctx.lineCap = "round";
956
+ ctx.stroke(path);
957
+ }
958
+ ctx.restore();
959
+ }
960
+ function hitCombo(r, px) {
961
+ if (r.data.kind !== "category") return null;
962
+ const d = r.data;
963
+ const cats = d.categories.length;
964
+ if (cats === 0 || px < r.plot.x || px > r.plot.x + r.plot.w) return null;
965
+ const cat = Math.min(cats - 1, Math.max(0, Math.floor((px - r.plot.x) / r.plot.w * cats)));
966
+ const slot = r.plot.w / cats;
967
+ let py = r.plot.y + r.plot.h;
968
+ const values = d.series.map((s) => {
969
+ const y = s.values[cat] ?? 0;
970
+ const sy = r.yScale(y);
971
+ if (sy < py) py = sy;
972
+ return {
973
+ label: s.label,
974
+ color: s.color,
975
+ y
976
+ };
977
+ });
978
+ return {
979
+ kind: "point",
980
+ index: cat,
981
+ x: cat,
982
+ px: r.plot.x + slot * (cat + .5),
983
+ py,
984
+ values,
985
+ title: d.categories[cat] ?? ""
986
+ };
987
+ }
988
+ //#endregion
848
989
  //#region src/core/format.ts
849
- /** Compact number formatting: integers verbatim, else 4 significant digits. */
990
+ /** @category Formatting
991
+ * Compact number formatting: integers verbatim, else 4 significant digits. */
850
992
  function formatNumber(v) {
851
993
  if (!Number.isFinite(v)) return "";
852
994
  if (Number.isInteger(v) && Math.abs(v) < 0x38d7ea4c68000) return String(v);
@@ -857,7 +999,8 @@ const MAGNITUDES = [
857
999
  [1e6, "M"],
858
1000
  [1e3, "k"]
859
1001
  ];
860
- /** SI-suffixed formatting: `1.2k`, `3.4M`, `5B`. */
1002
+ /** @category Formatting
1003
+ * SI-suffixed formatting: `1.2k`, `3.4M`, `5B`. */
861
1004
  function formatSI(v) {
862
1005
  if (!Number.isFinite(v)) return "";
863
1006
  const abs = Math.abs(v);
@@ -1109,13 +1252,13 @@ function makeCanvas(cssW, cssH) {
1109
1252
  c.height = Math.max(1, Math.round(cssH));
1110
1253
  return c;
1111
1254
  }
1112
- function setup2d(canvas, alpha) {
1113
- const ctx = canvas.getContext("2d", { alpha });
1255
+ function setup2d(canvas, settings) {
1256
+ const ctx = canvas.getContext("2d", settings);
1114
1257
  if (!ctx) throw new Error("resolvcharts: could not get 2d context");
1115
1258
  return ctx;
1116
1259
  }
1117
1260
  var CanvasBackend = class {
1118
- constructor(container, cssW, cssH) {
1261
+ constructor(container, cssW, cssH, settings = {}) {
1119
1262
  this.kind = "canvas2d";
1120
1263
  this.plot = {
1121
1264
  x: 0,
@@ -1126,14 +1269,18 @@ var CanvasBackend = class {
1126
1269
  this.gridCache = null;
1127
1270
  this.gridCtx = null;
1128
1271
  this.container = container;
1272
+ this.settings = {
1273
+ alpha: true,
1274
+ ...settings
1275
+ };
1129
1276
  this.dpr = Math.max(1, window.devicePixelRatio || 1);
1130
1277
  this.width = cssW;
1131
1278
  this.height = cssH;
1132
1279
  this.mainCanvas = makeCanvas(cssW, cssH);
1133
1280
  this.overlayCanvas = makeCanvas(cssW, cssH);
1134
1281
  this.overlayCanvas.style.pointerEvents = "none";
1135
- this.main = setup2d(this.mainCanvas, true);
1136
- this.overlay = setup2d(this.overlayCanvas, true);
1282
+ this.main = setup2d(this.mainCanvas, this.settings);
1283
+ this.overlay = setup2d(this.overlayCanvas, this.settings);
1137
1284
  container.append(this.mainCanvas, this.overlayCanvas);
1138
1285
  this.applySize();
1139
1286
  }
@@ -1157,7 +1304,7 @@ var CanvasBackend = class {
1157
1304
  this.gridCache = document.createElement("canvas");
1158
1305
  this.gridCache.width = Math.max(1, this.width * this.dpr);
1159
1306
  this.gridCache.height = Math.max(1, this.height * this.dpr);
1160
- this.gridCtx = setup2d(this.gridCache, true);
1307
+ this.gridCtx = setup2d(this.gridCache, this.settings);
1161
1308
  this.gridCtx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
1162
1309
  }
1163
1310
  const ctx = this.gridCtx;
@@ -1192,7 +1339,8 @@ var CanvasBackend = class {
1192
1339
  };
1193
1340
  //#endregion
1194
1341
  //#region src/core/scales.ts
1195
- /** Linear scale from domain `[d0, d1]` to range `[r0, r1]`, with `invert`. */
1342
+ /** @category Utilities
1343
+ * Linear scale from domain `[d0, d1]` to range `[r0, r1]`, with `invert`. */
1196
1344
  function linearScale(d0, d1, r0, r1) {
1197
1345
  const dd = d1 - d0 || 1;
1198
1346
  const dr = r1 - r0;
@@ -1202,7 +1350,8 @@ function linearScale(d0, d1, r0, r1) {
1202
1350
  fn.range = [r0, r1];
1203
1351
  return fn;
1204
1352
  }
1205
- /** `[min, max]` of `values`; degenerate inputs get a sane fallback domain. */
1353
+ /** @category Utilities
1354
+ * `[min, max]` of `values`; degenerate inputs get a sane fallback domain. */
1206
1355
  function extent(values) {
1207
1356
  let min = Infinity;
1208
1357
  let max = -Infinity;
@@ -1215,7 +1364,8 @@ function extent(values) {
1215
1364
  if (min === max) return min === 0 ? [0, 1] : [min - Math.abs(min) * .1, max + Math.abs(max) * .1];
1216
1365
  return [min, max];
1217
1366
  }
1218
- /** Pad a domain by 5% on each end; `includeZero` anchors the axis at 0. */
1367
+ /** @category Utilities
1368
+ * Pad a domain by 5% on each end; `includeZero` anchors the axis at 0. */
1219
1369
  function padDomain(min, max, includeZero = false) {
1220
1370
  let lo = includeZero ? Math.min(0, min) : min;
1221
1371
  let hi = includeZero ? Math.max(0, max) : max;
@@ -1269,6 +1419,9 @@ const easeOutCubic = (t) => 1 - (1 - t) ** 3;
1269
1419
  //#endregion
1270
1420
  //#region src/core/theme.ts
1271
1421
  const FONT = "Inter, system-ui, -apple-system, \"Segoe UI\", sans-serif";
1422
+ /** Built-in light theme.
1423
+ * @category Theming
1424
+ */
1272
1425
  const lightTheme = {
1273
1426
  bg: "#ffffff",
1274
1427
  text: "#1f1f1f",
@@ -1287,6 +1440,9 @@ const lightTheme = {
1287
1440
  tooltipShadow: "0 4px 14px rgba(0,0,0,0.08)",
1288
1441
  fontFamily: FONT
1289
1442
  };
1443
+ /** Built-in dark theme.
1444
+ * @category Theming
1445
+ */
1290
1446
  const darkTheme = {
1291
1447
  bg: "#0a0a0a",
1292
1448
  text: "#ededed",
@@ -1317,6 +1473,9 @@ const categoricalDark = [
1317
1473
  "#a97ef0",
1318
1474
  "#f5b84d"
1319
1475
  ];
1476
+ /** Merge a theme option (`"light" | "dark" | partial tokens`) into full {@link ThemeTokens}.
1477
+ * @category Theming
1478
+ */
1320
1479
  function resolveTheme(opt, accent) {
1321
1480
  const base = opt === "dark" ? {
1322
1481
  ...darkTheme,
@@ -1357,6 +1516,9 @@ function niceStep(range, target) {
1357
1516
  }
1358
1517
  return step * pow;
1359
1518
  }
1519
+ /** "Nice" round tick values covering `[min, max]`, ~`target` count.
1520
+ * @category Utilities
1521
+ */
1360
1522
  function computeTicks(min, max, target = 5) {
1361
1523
  if (!(min < max)) return [min];
1362
1524
  const step = niceStep(max - min, target);
@@ -1479,6 +1641,12 @@ const MODULES = {
1479
1641
  axes: true,
1480
1642
  defaultTooltip: true
1481
1643
  },
1644
+ combo: {
1645
+ render: renderCombo,
1646
+ hit: (r, px) => hitCombo(r, px),
1647
+ axes: true,
1648
+ defaultTooltip: true
1649
+ },
1482
1650
  sparkline: {
1483
1651
  render: renderSparkline,
1484
1652
  hit: (r, px) => hitXY(r, px),
@@ -1527,6 +1695,9 @@ const GPU_KIND = {
1527
1695
  };
1528
1696
  /** points retained beyond the window's left edge so they slide off-screen instead of vanishing mid-plot */
1529
1697
  const EDGE_KEEP = 3;
1698
+ /** Chart instance. Create via {@link createChart}.
1699
+ * @category Charts
1700
+ */
1530
1701
  var Chart = class {
1531
1702
  constructor(el, options) {
1532
1703
  this.tooltip = null;
@@ -1607,7 +1778,7 @@ var Chart = class {
1607
1778
  if (options.height) el.style.height = `${options.height}px`;
1608
1779
  const w = el.clientWidth || 320;
1609
1780
  const h = el.clientHeight || (options.type === "sparkline" ? 32 : 240);
1610
- this.backend = new CanvasBackend(el, w, h);
1781
+ this.backend = new CanvasBackend(el, w, h, options.canvas);
1611
1782
  this.reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
1612
1783
  if (options.tooltip ?? this.mod.defaultTooltip) this.tooltip = new Tooltip(el, this.theme);
1613
1784
  this.data = this.normalize(options.data, null);
@@ -1718,7 +1889,7 @@ var Chart = class {
1718
1889
  total: slices.reduce((a, s) => a + Math.max(0, s.value), 0)
1719
1890
  };
1720
1891
  }
1721
- if (t === "bar") {
1892
+ if (t === "bar" || t === "combo") {
1722
1893
  const d = data;
1723
1894
  const prevSeries = prev?.kind === "category" ? prev.series : [];
1724
1895
  return {
@@ -1727,6 +1898,7 @@ var Chart = class {
1727
1898
  series: d.series.map((s, i) => ({
1728
1899
  label: s.label ?? `Series ${i + 1}`,
1729
1900
  color: seriesColor(this.theme, i, s.color),
1901
+ kind: s.kind ?? "column",
1730
1902
  values: Float64Array.from(s.values),
1731
1903
  prevValues: prevSeries[i]?.values ?? null
1732
1904
  }))
@@ -1835,18 +2007,26 @@ var Chart = class {
1835
2007
  if (d.kind === "category") {
1836
2008
  let min = Infinity;
1837
2009
  let max = -Infinity;
1838
- if (this.options.stacked) for (let ci = 0; ci < d.categories.length; ci++) {
1839
- let pos = 0;
1840
- let neg = 0;
2010
+ if (this.options.stacked) {
2011
+ for (let ci = 0; ci < d.categories.length; ci++) {
2012
+ let pos = 0;
2013
+ let neg = 0;
2014
+ for (const s of d.series) {
2015
+ if (s.kind !== "column") continue;
2016
+ const v = s.values[ci] ?? 0;
2017
+ if (v >= 0) pos += v;
2018
+ else neg += v;
2019
+ }
2020
+ min = Math.min(min, neg);
2021
+ max = Math.max(max, pos);
2022
+ }
1841
2023
  for (const s of d.series) {
1842
- const v = s.values[ci] ?? 0;
1843
- if (v >= 0) pos += v;
1844
- else neg += v;
2024
+ if (s.kind === "column") continue;
2025
+ const e = extent(s.values);
2026
+ min = Math.min(min, e[0]);
2027
+ max = Math.max(max, e[1]);
1845
2028
  }
1846
- min = Math.min(min, neg);
1847
- max = Math.max(max, pos);
1848
- }
1849
- else for (const s of d.series) {
2029
+ } else for (const s of d.series) {
1850
2030
  const e = extent(s.values);
1851
2031
  min = Math.min(min, e[0]);
1852
2032
  max = Math.max(max, e[1]);
@@ -1980,7 +2160,7 @@ var Chart = class {
1980
2160
  ctx.fillText(fmtX(t), px, plot.y + plot.h + 6);
1981
2161
  }
1982
2162
  }
1983
- if (this.options.type === "bar") {
2163
+ if (this.options.type === "bar" || this.options.type === "combo") {
1984
2164
  const base = Math.round(yScale(0)) + .5;
1985
2165
  if (base >= plot.y && base <= plot.y + plot.h + 1) {
1986
2166
  ctx.strokeStyle = this.theme.axisBaseline;
@@ -2041,9 +2221,9 @@ var Chart = class {
2041
2221
  o.lineTo(x, plot.y + plot.h);
2042
2222
  o.stroke();
2043
2223
  const d = this.data;
2044
- if (d.kind === "xy" && info.kind === "point") for (const v of info.values) {
2224
+ if (info.kind === "point" && (d.kind === "xy" || d.kind === "category")) for (const v of info.values) {
2045
2225
  const s = d.series.find((s2) => s2.label === v.label && s2.color === v.color);
2046
- const yv = s ? s.y[info.index] : v.y;
2226
+ const yv = s ? "y" in s ? s.y[info.index] : s.values[info.index] : v.y;
2047
2227
  o.fillStyle = v.color;
2048
2228
  o.strokeStyle = this.theme.bg;
2049
2229
  o.lineWidth = 2;
@@ -2105,7 +2285,7 @@ var Chart = class {
2105
2285
  color: v.color,
2106
2286
  label: v.label,
2107
2287
  value: fmtY(v.y)
2108
- })), fmtX(info.x));
2288
+ })), info.title ?? fmtX(info.x));
2109
2289
  } else if (info.kind === "bar") this.tooltip.show(info.px, info.py, [{
2110
2290
  color: info.color,
2111
2291
  label: info.label,
@@ -2282,6 +2462,7 @@ function resortXY(d) {
2282
2462
  }
2283
2463
  /**
2284
2464
  * Create a chart inside `el`.
2465
+ * @category Charts
2285
2466
  *
2286
2467
  * @example
2287
2468
  * ```ts