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/README.md CHANGED
@@ -13,7 +13,13 @@ At [Resolvo Cloud](https://resolvocloud.com), we experienced glitchy live charts
13
13
  npm install resolvcharts
14
14
  ```
15
15
 
16
- ## Usage
16
+ Or drop in the CDN build (exposes a global `ResolvCharts`):
17
+
18
+ ```html
19
+ <script src="https://unpkg.com/resolvcharts/dist/resolvcharts.global.min.js"></script>
20
+ ```
21
+
22
+ ## Quick start
17
23
 
18
24
  ```ts
19
25
  import { createChart } from "resolvcharts";
@@ -27,12 +33,55 @@ const chart = createChart(document.querySelector("#chart")!, {
27
33
  });
28
34
  ```
29
35
 
30
- Chart types: `line`, `area`, `bar` (grouped or `stacked`), `sparkline`, `scatter`, `donut`, `candle`, `ohlc`, `waterfall`, `baseline`.
36
+ ## Chart types
37
+
38
+ The `data` shape depends on `type` — pick the row you need:
39
+
40
+ | `type` | `data` shape | Use case |
41
+ |---|---|---|
42
+ | `line` | `{ x, series: [{ y }] }` | Trends over an x axis |
43
+ | `area` | `{ x, series: [{ y }] }` | Line + filled gradient |
44
+ | `baseline` | `{ x, series: [{ y }] }` | Delta vs a reference value |
45
+ | `sparkline` | `{ x, series: [{ y }] }` | Tiny inline chart, no axes |
46
+ | `scatter` | `{ x, series: [{ y }] }` | Point clouds |
47
+ | `bar` | `{ categories, series: [{ values }] }` | Categorical groups; `stacked` supported |
48
+ | `combo` | `{ categories, series: [{ values, kind }] }` | Columns + line/area overlay |
49
+ | `donut` | `{ slices: [{ label, value }] }` | Share of a total |
50
+ | `candle` / `ohlc` | `{ candles: [{ x, open, high, low, close, volume? }] }` | Financial OHLC (+ volume band) |
51
+ | `waterfall` | `{ categories, values, totals? }` | P&L bridges, signed deltas |
52
+
53
+ ## Examples
31
54
 
32
55
  ```ts
33
- // Candlestick (+ optional volume band)
56
+ // Bar
57
+ createChart(el, {
58
+ type: "bar",
59
+ data: { categories: ["Q1", "Q2", "Q3"], series: [{ label: "Sales", values: [4, 7, 5] }] },
60
+ });
61
+
62
+ // Stacked bar (negative values stack below zero)
63
+ createChart(el, {
64
+ type: "bar", stacked: true,
65
+ data: { categories: ["Q1", "Q2"], series: [{ label: "A", values: [3, 5] }, { label: "B", values: [2, -1] }] },
66
+ });
67
+
68
+ // Combo — per-series `kind`: "column" (default), "line", or "area".
69
+ // Columns group per category; line/area series overlay on category centers.
70
+ createChart(el, {
71
+ type: "combo",
72
+ data: {
73
+ categories: ["Jan", "Feb", "Mar"],
74
+ series: [
75
+ { label: "Inflow", values: [42, 55, 48], kind: "column" },
76
+ { label: "Outflow", values: [30, 41, 37], kind: "column" },
77
+ { label: "Net", values: [12, 14, 11], kind: "area" },
78
+ ],
79
+ },
80
+ });
81
+
82
+ // Candlestick (+ optional volume band); "ohlc" renders bar-style
34
83
  createChart(el, {
35
- type: "candle", // or "ohlc" for bar-style
84
+ type: "candle",
36
85
  data: { candles: [{ x: 0, open: 100, high: 108, low: 98, close: 105, volume: 1200 }] },
37
86
  });
38
87
 
@@ -53,20 +102,6 @@ createChart(el, {
53
102
  data: { x: [0, 1, 2], series: [{ y: [-2, 4, -1] }] },
54
103
  });
55
104
 
56
- // Stacked bar (negative values stack below zero)
57
- createChart(el, {
58
- type: "bar", stacked: true,
59
- data: { categories: ["Q1", "Q2"], series: [{ label: "A", values: [3, 5] }, { label: "B", values: [2, -1] }] },
60
- });
61
- ```
62
-
63
- ```ts
64
- // Bar
65
- createChart(el, {
66
- type: "bar",
67
- data: { categories: ["Q1", "Q2", "Q3"], series: [{ label: "Sales", values: [4, 7, 5] }] },
68
- });
69
-
70
105
  // Donut
71
106
  createChart(el, {
72
107
  type: "donut",
@@ -76,29 +111,60 @@ createChart(el, {
76
111
 
77
112
  ## Options
78
113
 
114
+ Universal:
115
+
79
116
  | Option | Default | Description |
80
117
  |---|---|---|
81
- | `type` | — | `line` `area` `bar` `sparkline` `scatter` `donut` `candle` `ohlc` `waterfall` `baseline` |
82
- | `data` | — | `{x, series}` · `{categories, series}` · `{slices}` · `{candles}` · waterfall shape |
118
+ | `type` | — | Any row in the table above |
119
+ | `data` | — | Matching shape from the table above |
83
120
  | `theme` | `"light"` | `"light"` `"dark"` or partial token object |
84
121
  | `accent` | — | Primary series color |
85
122
  | `curve` | `"linear"` | `linear` `step` `smooth` (monotone cubic, never overshoots) |
86
123
  | `grid` | `true` | Faint horizontal gridlines |
87
124
  | `tooltip` | auto | Hover tooltip (off for sparkline) |
125
+ | `legend` | — | Reserved |
88
126
  | `animate` | `true` | `true` = 400 ms, a number = custom ms; respects `prefers-reduced-motion` |
89
- | `flow` | `false` | Continuous-flow mode for xy charts — ripple easing + tween duration auto-follows append cadence |
90
- | `window` | — | Visible points in the sliding window (xy charts) |
91
- | `maxPoints` | — | Hard cap on retained datapoints during `append()`; shows last N when `window` is unset |
92
- | `stacked` | `false` | Bar charts: stack series per category |
93
- | `baseline` | first value | Baseline chart reference value |
94
127
  | `height` | — | Chart height in px |
95
128
  | `labels` | — | `{x?: (v)=>string, y?: (v)=>string}` tick formatters |
96
129
  | `onHover` | — | `(info \| null) => void` |
97
130
 
131
+ xy charts (`line` `area` `scatter` `sparkline` `baseline`):
132
+
133
+ | Option | Default | Description |
134
+ |---|---|---|
135
+ | `flow` | `false` | Continuous-flow mode — ripple easing + tween duration auto-follows append cadence |
136
+ | `window` | — | Visible points in the sliding window |
137
+ | `maxPoints` | — | Hard cap on retained datapoints during `append()`; shows last N when `window` is unset |
138
+ | `renderer` | `"canvas"` | `"webgpu"` rasterizes series on the GPU; `"auto"` uses it when available |
139
+ | `gpu` | — | WebGPU adapter options (`powerPreference`, `compat`, `timing`) |
140
+
141
+ Type-specific:
142
+
143
+ | Option | Default | Description |
144
+ |---|---|---|
145
+ | `stacked` | `false` | `bar`/`combo`: stack column series per category |
146
+ | `baseline` | first value | `baseline` chart reference value |
147
+ | `canvas` | — | `getContext("2d", …)` settings for all layers, e.g. `{ willReadFrequently: true }` |
148
+
149
+ ## Theming
150
+
151
+ ```ts
152
+ createChart(el, {
153
+ type: "line",
154
+ theme: "dark",
155
+ // or override individual tokens:
156
+ // theme: { bg: "#0b0e14", grid: "#1f2430", series: ["#7aa2f7", "#9ece6a"] },
157
+ accent: "#7aa2f7",
158
+ data: { x, series },
159
+ });
160
+ ```
161
+
162
+ Tokens: `bg` `text` `textMuted` `grid` `axisBaseline` `crosshair` `series[]` `contextGray` `up` `down` `tooltipBg` `tooltipText` `tooltipMuted` `tooltipBorder` `tooltipShadow` `fontFamily`.
163
+
98
164
  ## API
99
165
 
100
166
  ```ts
101
- chart.setData(newData); // animated transition
167
+ chart.setData(newData); // animated transition; shape must match `type`
102
168
  chart.append(x, y); // streaming: append one sample (xy charts)
103
169
  // number → single series, number[] → per-series value
104
170
  chart.resize(); // manual resize (ResizeObserver handles automatically)
package/dist/index.cjs CHANGED
@@ -1,12 +1,9 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- //#region src/charts/bar.ts
3
- const MAX_BAR_W$1 = 40;
4
- const RADIUS$1 = 3;
5
2
  function barGeometry(r, d, catIdx, seriesIdx, value) {
6
3
  const cats = d.categories.length;
7
4
  if (cats === 0) return null;
8
5
  const slot = r.plot.w / cats;
9
- const groupW = Math.min(slot * .8, MAX_BAR_W$1 * d.series.length);
6
+ const groupW = Math.min(slot * .8, 40 * d.series.length);
10
7
  const barW = groupW / d.series.length;
11
8
  const x = r.plot.x + slot * catIdx + (slot - groupW) / 2 + barW * seriesIdx;
12
9
  const base = r.yScale(0);
@@ -46,7 +43,7 @@ function stackedGeometry(r, d, catIdx, v0, v1) {
46
43
  const cats = d.categories.length;
47
44
  if (cats === 0) return null;
48
45
  const slot = r.plot.w / cats;
49
- const w = Math.min(slot * .8, MAX_BAR_W$1);
46
+ const w = Math.min(slot * .8, 40);
50
47
  const x = r.plot.x + slot * catIdx + (slot - w) / 2;
51
48
  const yTop = r.yScale(Math.max(v0, v1));
52
49
  const yBot = r.yScale(Math.min(v0, v1));
@@ -69,7 +66,7 @@ function renderBar(r) {
69
66
  const slot = r.plot.w / Math.max(1, cats);
70
67
  const animate = r.t < 1;
71
68
  if (r.opts.stacked) {
72
- const w = Math.min(slot * .8, MAX_BAR_W$1);
69
+ const w = Math.min(slot * .8, 40);
73
70
  for (let ci = 0; ci < cats; ci++) {
74
71
  const x = r.plot.x + slot * ci + (slot - w) / 2;
75
72
  let pos = 0;
@@ -92,7 +89,7 @@ function renderBar(r) {
92
89
  }
93
90
  }
94
91
  } else {
95
- const groupW = Math.min(slot * .8, MAX_BAR_W$1 * d.series.length);
92
+ const groupW = Math.min(slot * .8, 40 * d.series.length);
96
93
  const barW = groupW / d.series.length;
97
94
  const base = r.yScale(0);
98
95
  for (let si = 0; si < d.series.length; si++) {
@@ -113,7 +110,7 @@ function renderBar(r) {
113
110
  y,
114
111
  w: barW,
115
112
  h
116
- }, RADIUS$1, v >= 0);
113
+ }, 3, v >= 0);
117
114
  }
118
115
  }
119
116
  }
@@ -376,6 +373,7 @@ function decimateSeriesPx(xs, ys, prev, t, yFallback, xScale, yScale, pixelWidth
376
373
  * ys must be pixel-space y coordinates of equal length.
377
374
  *
378
375
  * Returns interleaved [x, y] pairs ready for path construction.
376
+ * @category Utilities
379
377
  */
380
378
  function decimateM4(xs, ys, pixelWidth) {
381
379
  const n = Math.min(xs.length, ys.length);
@@ -437,6 +435,7 @@ function decimateM4(xs, ys, pixelWidth) {
437
435
  /**
438
436
  * Binary search for the index whose value is nearest to `target`.
439
437
  * `xs` must be sorted ascending. Returns -1 for empty input.
438
+ * @category Utilities
440
439
  */
441
440
  function nearestIndex(xs, target) {
442
441
  const n = xs.length;
@@ -846,8 +845,151 @@ function hitCandle(r, px) {
846
845
  };
847
846
  }
848
847
  //#endregion
848
+ //#region src/charts/combo.ts
849
+ /** pooled scratch for overlay [x, y] pairs — consumed within the call */
850
+ let ovScratch = /* @__PURE__ */ new Float64Array(0);
851
+ function overlayPath(r, pts, n) {
852
+ 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
+ }
867
+ return path;
868
+ }
869
+ function renderCombo(r) {
870
+ if (r.data.kind !== "category") return;
871
+ const d = r.data;
872
+ const ctx = r.ctx;
873
+ ctx.save();
874
+ ctx.beginPath();
875
+ ctx.rect(r.plot.x, r.plot.y, r.plot.w, r.plot.h + 1);
876
+ ctx.clip();
877
+ const cats = d.categories.length;
878
+ const slot = r.plot.w / Math.max(1, cats);
879
+ const animate = r.t < 1;
880
+ const valAt = (s, ci) => {
881
+ const cur = s.values[ci] ?? 0;
882
+ const prev = s.prevValues?.[ci] ?? 0;
883
+ return animate ? prev + (cur - prev) * r.t : cur;
884
+ };
885
+ let nCols = 0;
886
+ for (const s of d.series) if (s.kind === "column") nCols++;
887
+ if (r.opts.stacked) {
888
+ const w = Math.min(slot * .8, 40);
889
+ for (let ci = 0; ci < cats; ci++) {
890
+ const x = r.plot.x + slot * ci + (slot - w) / 2;
891
+ let pos = 0;
892
+ let neg = 0;
893
+ for (const s of d.series) {
894
+ if (s.kind !== "column") continue;
895
+ const v = valAt(s, ci);
896
+ const v0 = v >= 0 ? pos : neg + v;
897
+ const v1 = v >= 0 ? pos + v : neg;
898
+ if (v >= 0) pos = v1;
899
+ else neg = v0;
900
+ const yTop = r.yScale(Math.max(v0, v1));
901
+ const h = Math.abs(r.yScale(Math.min(v0, v1)) - yTop);
902
+ if (h <= 0) continue;
903
+ ctx.fillStyle = s.color;
904
+ ctx.fillRect(x, yTop, w, h);
905
+ }
906
+ }
907
+ } else {
908
+ const groupW = Math.min(slot * .8, 40 * nCols);
909
+ const barW = groupW / Math.max(1, nCols);
910
+ const base = r.yScale(0);
911
+ let k = 0;
912
+ for (const s of d.series) {
913
+ if (s.kind !== "column") continue;
914
+ ctx.fillStyle = s.color;
915
+ for (let ci = 0; ci < cats; ci++) {
916
+ const v = valAt(s, ci);
917
+ const x = r.plot.x + slot * ci + (slot - groupW) / 2 + barW * k;
918
+ const yv = r.yScale(v);
919
+ const y = Math.min(base, yv);
920
+ const h = Math.abs(base - yv);
921
+ if (h <= 0) continue;
922
+ topRounded(ctx, {
923
+ x,
924
+ y,
925
+ w: barW,
926
+ h
927
+ }, 3, v >= 0);
928
+ }
929
+ k++;
930
+ }
931
+ }
932
+ const base = r.yScale(Math.max(0, r.yScale.domain[0]));
933
+ if (ovScratch.length < cats * 2) ovScratch = new Float64Array(cats * 2);
934
+ const pts = ovScratch;
935
+ for (const s of d.series) {
936
+ if (s.kind === "column") continue;
937
+ for (let ci = 0; ci < cats; ci++) {
938
+ pts[2 * ci] = r.plot.x + slot * (ci + .5);
939
+ pts[2 * ci + 1] = r.yScale(valAt(s, ci));
940
+ }
941
+ const path = overlayPath(r, pts, cats);
942
+ if (s.kind === "area" && cats > 1) {
943
+ const area = new Path2D(path);
944
+ area.lineTo(pts[2 * (cats - 1)], base);
945
+ area.lineTo(pts[0], base);
946
+ area.closePath();
947
+ const g = ctx.createLinearGradient(0, r.plot.y, 0, r.plot.y + r.plot.h);
948
+ g.addColorStop(0, withAlpha(s.color, .16));
949
+ g.addColorStop(1, withAlpha(s.color, 0));
950
+ ctx.fillStyle = g;
951
+ ctx.fill(area);
952
+ }
953
+ ctx.strokeStyle = s.color;
954
+ ctx.lineWidth = 2;
955
+ ctx.lineJoin = "round";
956
+ ctx.lineCap = "round";
957
+ ctx.stroke(path);
958
+ }
959
+ ctx.restore();
960
+ }
961
+ function hitCombo(r, px) {
962
+ if (r.data.kind !== "category") return null;
963
+ const d = r.data;
964
+ const cats = d.categories.length;
965
+ if (cats === 0 || px < r.plot.x || px > r.plot.x + r.plot.w) return null;
966
+ const cat = Math.min(cats - 1, Math.max(0, Math.floor((px - r.plot.x) / r.plot.w * cats)));
967
+ const slot = r.plot.w / cats;
968
+ let py = r.plot.y + r.plot.h;
969
+ const values = d.series.map((s) => {
970
+ const y = s.values[cat] ?? 0;
971
+ const sy = r.yScale(y);
972
+ if (sy < py) py = sy;
973
+ return {
974
+ label: s.label,
975
+ color: s.color,
976
+ y
977
+ };
978
+ });
979
+ return {
980
+ kind: "point",
981
+ index: cat,
982
+ x: cat,
983
+ px: r.plot.x + slot * (cat + .5),
984
+ py,
985
+ values,
986
+ title: d.categories[cat] ?? ""
987
+ };
988
+ }
989
+ //#endregion
849
990
  //#region src/core/format.ts
850
- /** Compact number formatting: integers verbatim, else 4 significant digits. */
991
+ /** @category Formatting
992
+ * Compact number formatting: integers verbatim, else 4 significant digits. */
851
993
  function formatNumber(v) {
852
994
  if (!Number.isFinite(v)) return "";
853
995
  if (Number.isInteger(v) && Math.abs(v) < 0x38d7ea4c68000) return String(v);
@@ -858,7 +1000,8 @@ const MAGNITUDES = [
858
1000
  [1e6, "M"],
859
1001
  [1e3, "k"]
860
1002
  ];
861
- /** SI-suffixed formatting: `1.2k`, `3.4M`, `5B`. */
1003
+ /** @category Formatting
1004
+ * SI-suffixed formatting: `1.2k`, `3.4M`, `5B`. */
862
1005
  function formatSI(v) {
863
1006
  if (!Number.isFinite(v)) return "";
864
1007
  const abs = Math.abs(v);
@@ -1110,13 +1253,13 @@ function makeCanvas(cssW, cssH) {
1110
1253
  c.height = Math.max(1, Math.round(cssH));
1111
1254
  return c;
1112
1255
  }
1113
- function setup2d(canvas, alpha) {
1114
- const ctx = canvas.getContext("2d", { alpha });
1256
+ function setup2d(canvas, settings) {
1257
+ const ctx = canvas.getContext("2d", settings);
1115
1258
  if (!ctx) throw new Error("resolvcharts: could not get 2d context");
1116
1259
  return ctx;
1117
1260
  }
1118
1261
  var CanvasBackend = class {
1119
- constructor(container, cssW, cssH) {
1262
+ constructor(container, cssW, cssH, settings = {}) {
1120
1263
  this.kind = "canvas2d";
1121
1264
  this.plot = {
1122
1265
  x: 0,
@@ -1127,14 +1270,18 @@ var CanvasBackend = class {
1127
1270
  this.gridCache = null;
1128
1271
  this.gridCtx = null;
1129
1272
  this.container = container;
1273
+ this.settings = {
1274
+ alpha: true,
1275
+ ...settings
1276
+ };
1130
1277
  this.dpr = Math.max(1, window.devicePixelRatio || 1);
1131
1278
  this.width = cssW;
1132
1279
  this.height = cssH;
1133
1280
  this.mainCanvas = makeCanvas(cssW, cssH);
1134
1281
  this.overlayCanvas = makeCanvas(cssW, cssH);
1135
1282
  this.overlayCanvas.style.pointerEvents = "none";
1136
- this.main = setup2d(this.mainCanvas, true);
1137
- this.overlay = setup2d(this.overlayCanvas, true);
1283
+ this.main = setup2d(this.mainCanvas, this.settings);
1284
+ this.overlay = setup2d(this.overlayCanvas, this.settings);
1138
1285
  container.append(this.mainCanvas, this.overlayCanvas);
1139
1286
  this.applySize();
1140
1287
  }
@@ -1158,7 +1305,7 @@ var CanvasBackend = class {
1158
1305
  this.gridCache = document.createElement("canvas");
1159
1306
  this.gridCache.width = Math.max(1, this.width * this.dpr);
1160
1307
  this.gridCache.height = Math.max(1, this.height * this.dpr);
1161
- this.gridCtx = setup2d(this.gridCache, true);
1308
+ this.gridCtx = setup2d(this.gridCache, this.settings);
1162
1309
  this.gridCtx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
1163
1310
  }
1164
1311
  const ctx = this.gridCtx;
@@ -1193,7 +1340,8 @@ var CanvasBackend = class {
1193
1340
  };
1194
1341
  //#endregion
1195
1342
  //#region src/core/scales.ts
1196
- /** Linear scale from domain `[d0, d1]` to range `[r0, r1]`, with `invert`. */
1343
+ /** @category Utilities
1344
+ * Linear scale from domain `[d0, d1]` to range `[r0, r1]`, with `invert`. */
1197
1345
  function linearScale(d0, d1, r0, r1) {
1198
1346
  const dd = d1 - d0 || 1;
1199
1347
  const dr = r1 - r0;
@@ -1203,7 +1351,8 @@ function linearScale(d0, d1, r0, r1) {
1203
1351
  fn.range = [r0, r1];
1204
1352
  return fn;
1205
1353
  }
1206
- /** `[min, max]` of `values`; degenerate inputs get a sane fallback domain. */
1354
+ /** @category Utilities
1355
+ * `[min, max]` of `values`; degenerate inputs get a sane fallback domain. */
1207
1356
  function extent(values) {
1208
1357
  let min = Infinity;
1209
1358
  let max = -Infinity;
@@ -1216,7 +1365,8 @@ function extent(values) {
1216
1365
  if (min === max) return min === 0 ? [0, 1] : [min - Math.abs(min) * .1, max + Math.abs(max) * .1];
1217
1366
  return [min, max];
1218
1367
  }
1219
- /** Pad a domain by 5% on each end; `includeZero` anchors the axis at 0. */
1368
+ /** @category Utilities
1369
+ * Pad a domain by 5% on each end; `includeZero` anchors the axis at 0. */
1220
1370
  function padDomain(min, max, includeZero = false) {
1221
1371
  let lo = includeZero ? Math.min(0, min) : min;
1222
1372
  let hi = includeZero ? Math.max(0, max) : max;
@@ -1270,6 +1420,9 @@ const easeOutCubic = (t) => 1 - (1 - t) ** 3;
1270
1420
  //#endregion
1271
1421
  //#region src/core/theme.ts
1272
1422
  const FONT = "Inter, system-ui, -apple-system, \"Segoe UI\", sans-serif";
1423
+ /** Built-in light theme.
1424
+ * @category Theming
1425
+ */
1273
1426
  const lightTheme = {
1274
1427
  bg: "#ffffff",
1275
1428
  text: "#1f1f1f",
@@ -1288,6 +1441,9 @@ const lightTheme = {
1288
1441
  tooltipShadow: "0 4px 14px rgba(0,0,0,0.08)",
1289
1442
  fontFamily: FONT
1290
1443
  };
1444
+ /** Built-in dark theme.
1445
+ * @category Theming
1446
+ */
1291
1447
  const darkTheme = {
1292
1448
  bg: "#0a0a0a",
1293
1449
  text: "#ededed",
@@ -1318,6 +1474,9 @@ const categoricalDark = [
1318
1474
  "#a97ef0",
1319
1475
  "#f5b84d"
1320
1476
  ];
1477
+ /** Merge a theme option (`"light" | "dark" | partial tokens`) into full {@link ThemeTokens}.
1478
+ * @category Theming
1479
+ */
1321
1480
  function resolveTheme(opt, accent) {
1322
1481
  const base = opt === "dark" ? {
1323
1482
  ...darkTheme,
@@ -1358,6 +1517,9 @@ function niceStep(range, target) {
1358
1517
  }
1359
1518
  return step * pow;
1360
1519
  }
1520
+ /** "Nice" round tick values covering `[min, max]`, ~`target` count.
1521
+ * @category Utilities
1522
+ */
1361
1523
  function computeTicks(min, max, target = 5) {
1362
1524
  if (!(min < max)) return [min];
1363
1525
  const step = niceStep(max - min, target);
@@ -1480,6 +1642,12 @@ const MODULES = {
1480
1642
  axes: true,
1481
1643
  defaultTooltip: true
1482
1644
  },
1645
+ combo: {
1646
+ render: renderCombo,
1647
+ hit: (r, px) => hitCombo(r, px),
1648
+ axes: true,
1649
+ defaultTooltip: true
1650
+ },
1483
1651
  sparkline: {
1484
1652
  render: renderSparkline,
1485
1653
  hit: (r, px) => hitXY(r, px),
@@ -1528,6 +1696,9 @@ const GPU_KIND = {
1528
1696
  };
1529
1697
  /** points retained beyond the window's left edge so they slide off-screen instead of vanishing mid-plot */
1530
1698
  const EDGE_KEEP = 3;
1699
+ /** Chart instance. Create via {@link createChart}.
1700
+ * @category Charts
1701
+ */
1531
1702
  var Chart = class {
1532
1703
  constructor(el, options) {
1533
1704
  this.tooltip = null;
@@ -1608,7 +1779,7 @@ var Chart = class {
1608
1779
  if (options.height) el.style.height = `${options.height}px`;
1609
1780
  const w = el.clientWidth || 320;
1610
1781
  const h = el.clientHeight || (options.type === "sparkline" ? 32 : 240);
1611
- this.backend = new CanvasBackend(el, w, h);
1782
+ this.backend = new CanvasBackend(el, w, h, options.canvas);
1612
1783
  this.reduced = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
1613
1784
  if (options.tooltip ?? this.mod.defaultTooltip) this.tooltip = new Tooltip(el, this.theme);
1614
1785
  this.data = this.normalize(options.data, null);
@@ -1719,7 +1890,7 @@ var Chart = class {
1719
1890
  total: slices.reduce((a, s) => a + Math.max(0, s.value), 0)
1720
1891
  };
1721
1892
  }
1722
- if (t === "bar") {
1893
+ if (t === "bar" || t === "combo") {
1723
1894
  const d = data;
1724
1895
  const prevSeries = prev?.kind === "category" ? prev.series : [];
1725
1896
  return {
@@ -1728,6 +1899,7 @@ var Chart = class {
1728
1899
  series: d.series.map((s, i) => ({
1729
1900
  label: s.label ?? `Series ${i + 1}`,
1730
1901
  color: seriesColor(this.theme, i, s.color),
1902
+ kind: s.kind ?? "column",
1731
1903
  values: Float64Array.from(s.values),
1732
1904
  prevValues: prevSeries[i]?.values ?? null
1733
1905
  }))
@@ -1836,18 +2008,26 @@ var Chart = class {
1836
2008
  if (d.kind === "category") {
1837
2009
  let min = Infinity;
1838
2010
  let max = -Infinity;
1839
- if (this.options.stacked) for (let ci = 0; ci < d.categories.length; ci++) {
1840
- let pos = 0;
1841
- let neg = 0;
2011
+ if (this.options.stacked) {
2012
+ for (let ci = 0; ci < d.categories.length; ci++) {
2013
+ let pos = 0;
2014
+ let neg = 0;
2015
+ for (const s of d.series) {
2016
+ if (s.kind !== "column") continue;
2017
+ const v = s.values[ci] ?? 0;
2018
+ if (v >= 0) pos += v;
2019
+ else neg += v;
2020
+ }
2021
+ min = Math.min(min, neg);
2022
+ max = Math.max(max, pos);
2023
+ }
1842
2024
  for (const s of d.series) {
1843
- const v = s.values[ci] ?? 0;
1844
- if (v >= 0) pos += v;
1845
- else neg += v;
2025
+ if (s.kind === "column") continue;
2026
+ const e = extent(s.values);
2027
+ min = Math.min(min, e[0]);
2028
+ max = Math.max(max, e[1]);
1846
2029
  }
1847
- min = Math.min(min, neg);
1848
- max = Math.max(max, pos);
1849
- }
1850
- else for (const s of d.series) {
2030
+ } else for (const s of d.series) {
1851
2031
  const e = extent(s.values);
1852
2032
  min = Math.min(min, e[0]);
1853
2033
  max = Math.max(max, e[1]);
@@ -1981,7 +2161,7 @@ var Chart = class {
1981
2161
  ctx.fillText(fmtX(t), px, plot.y + plot.h + 6);
1982
2162
  }
1983
2163
  }
1984
- if (this.options.type === "bar") {
2164
+ if (this.options.type === "bar" || this.options.type === "combo") {
1985
2165
  const base = Math.round(yScale(0)) + .5;
1986
2166
  if (base >= plot.y && base <= plot.y + plot.h + 1) {
1987
2167
  ctx.strokeStyle = this.theme.axisBaseline;
@@ -2042,9 +2222,9 @@ var Chart = class {
2042
2222
  o.lineTo(x, plot.y + plot.h);
2043
2223
  o.stroke();
2044
2224
  const d = this.data;
2045
- if (d.kind === "xy" && info.kind === "point") for (const v of info.values) {
2225
+ if (info.kind === "point" && (d.kind === "xy" || d.kind === "category")) for (const v of info.values) {
2046
2226
  const s = d.series.find((s2) => s2.label === v.label && s2.color === v.color);
2047
- const yv = s ? s.y[info.index] : v.y;
2227
+ const yv = s ? "y" in s ? s.y[info.index] : s.values[info.index] : v.y;
2048
2228
  o.fillStyle = v.color;
2049
2229
  o.strokeStyle = this.theme.bg;
2050
2230
  o.lineWidth = 2;
@@ -2106,7 +2286,7 @@ var Chart = class {
2106
2286
  color: v.color,
2107
2287
  label: v.label,
2108
2288
  value: fmtY(v.y)
2109
- })), fmtX(info.x));
2289
+ })), info.title ?? fmtX(info.x));
2110
2290
  } else if (info.kind === "bar") this.tooltip.show(info.px, info.py, [{
2111
2291
  color: info.color,
2112
2292
  label: info.label,
@@ -2283,6 +2463,7 @@ function resortXY(d) {
2283
2463
  }
2284
2464
  /**
2285
2465
  * Create a chart inside `el`.
2466
+ * @category Charts
2286
2467
  *
2287
2468
  * @example
2288
2469
  * ```ts