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 +92 -26
- package/dist/index.cjs +216 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +111 -22
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +111 -22
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +216 -35
- package/dist/index.mjs.map +1 -1
- package/dist/resolvcharts.global.min.js +8 -8
- package/dist/resolvcharts.global.min.js.map +1 -1
- package/package.json +7 -2
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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",
|
|
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` | — |
|
|
82
|
-
| `data` | — |
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
},
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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,
|
|
1114
|
-
const ctx = canvas.getContext("2d",
|
|
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,
|
|
1137
|
-
this.overlay = setup2d(this.overlayCanvas,
|
|
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,
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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)
|
|
1840
|
-
let
|
|
1841
|
-
|
|
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
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
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
|
-
|
|
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"
|
|
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
|