@aeriz/chart 1.0.0 → 1.1.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.
Files changed (72) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +54 -447
  3. package/dist/aeriz.cjs +2575 -2387
  4. package/dist/aeriz.esm.js +1576 -1423
  5. package/dist/aeriz.esm.min.js +264 -1
  6. package/dist/aeriz.js +2575 -2387
  7. package/dist/aeriz.min.cjs +266 -1
  8. package/dist/aeriz.min.js +266 -1
  9. package/dist/esm/base.js +73 -0
  10. package/dist/esm/cartesian.js +11 -0
  11. package/dist/esm/charts/area.js +9 -0
  12. package/dist/esm/charts/bar.js +4 -0
  13. package/dist/esm/charts/boxplot.js +4 -0
  14. package/dist/esm/charts/bubble.js +3 -0
  15. package/dist/esm/charts/composite.js +11 -0
  16. package/dist/esm/charts/funnel.js +3 -0
  17. package/dist/esm/charts/gauge.js +5 -0
  18. package/dist/esm/charts/grouped-bar.js +5 -0
  19. package/dist/esm/charts/hbar.js +6 -0
  20. package/dist/esm/charts/heatmap.js +7 -0
  21. package/dist/esm/charts/line.js +9 -0
  22. package/dist/esm/charts/multi-line.js +8 -0
  23. package/dist/esm/charts/pie.js +6 -0
  24. package/dist/esm/charts/polar.js +3 -0
  25. package/dist/esm/charts/radar.js +10 -0
  26. package/dist/esm/charts/realtime.js +12 -0
  27. package/dist/esm/charts/scatter.js +6 -0
  28. package/dist/esm/charts/sparkline.js +5 -0
  29. package/dist/esm/charts/stacked-bar.js +7 -0
  30. package/dist/esm/charts/treemap.js +10 -0
  31. package/dist/esm/charts/waterfall.js +5 -0
  32. package/dist/esm/engine.js +33 -0
  33. package/dist/esm/index.js +2 -0
  34. package/dist/esm/jquery.js +4 -0
  35. package/dist/esm/math.js +32 -0
  36. package/dist/esm/react.js +4 -0
  37. package/dist/esm/shallow.js +3 -0
  38. package/dist/esm/svelte.js +4 -0
  39. package/dist/esm/vue.js +2 -0
  40. package/{src → dist}/index.d.ts +160 -30
  41. package/dist/jquery.d.ts +10 -0
  42. package/dist/react.d.ts +22 -0
  43. package/dist/svelte.d.ts +17 -0
  44. package/dist/types/area.d.ts +1 -0
  45. package/dist/types/bar.d.ts +1 -0
  46. package/dist/types/boxplot.d.ts +1 -0
  47. package/dist/types/bubble.d.ts +1 -0
  48. package/dist/types/composite.d.ts +1 -0
  49. package/dist/types/funnel.d.ts +1 -0
  50. package/dist/types/gauge.d.ts +1 -0
  51. package/dist/types/grouped-bar.d.ts +1 -0
  52. package/dist/types/hbar.d.ts +1 -0
  53. package/dist/types/heatmap.d.ts +1 -0
  54. package/dist/types/line.d.ts +1 -0
  55. package/dist/types/multi-line.d.ts +1 -0
  56. package/dist/types/pie.d.ts +1 -0
  57. package/dist/types/polar.d.ts +1 -0
  58. package/dist/types/radar.d.ts +1 -0
  59. package/dist/types/realtime.d.ts +1 -0
  60. package/dist/types/scatter.d.ts +1 -0
  61. package/dist/types/sparkline.d.ts +1 -0
  62. package/dist/types/stacked-bar.d.ts +1 -0
  63. package/dist/types/treemap.d.ts +1 -0
  64. package/dist/types/waterfall.d.ts +1 -0
  65. package/dist/vue.d.ts +13 -0
  66. package/package.json +60 -29
  67. package/src/base.js +0 -562
  68. package/src/charts.js +0 -1365
  69. package/src/composite.js +0 -187
  70. package/src/engine.js +0 -287
  71. package/src/index.js +0 -18
  72. package/src/math.js +0 -178
package/src/composite.js DELETED
@@ -1,187 +0,0 @@
1
- // composite.js — Composite (mixed) chart. Stack any combination of bars,
2
- // lines, areas, scatter on a shared cartesian axis with optional dual y axes.
3
- //
4
- // Layer config: { type: 'bar'|'line'|'area'|'scatter', name, values, axis: 'left'|'right', color?, ... }
5
-
6
- import { Chart } from './base.js';
7
- import * as M from './math.js';
8
-
9
- export class CompositeChart extends Chart {
10
- _render() {
11
- const layers = this.options.layers || [];
12
- const cats = this.options.categories || [];
13
- if (!layers.length || !cats.length) { this.setEmpty('No data'); return; }
14
- const e = this.engine;
15
- const t = this.theme;
16
-
17
- // Compute domain per axis.
18
- const leftLayers = layers.filter(l => l.axis !== 'right');
19
- const rightLayers = layers.filter(l => l.axis === 'right');
20
- const domain = (ll) => {
21
- let mn = Infinity, mx = -Infinity;
22
- for (const l of ll) {
23
- if (l.type === 'scatter') {
24
- for (const p of l.values) {
25
- if (p.y < mn) mn = p.y; if (p.y > mx) mx = p.y;
26
- }
27
- } else {
28
- for (const v of l.values) {
29
- if (+v < mn) mn = +v; if (+v > mx) mx = +v;
30
- }
31
- }
32
- }
33
- if (mn === Infinity) return [0, 1];
34
- return M.niceDomain(Math.min(0, mn), mx, 5);
35
- };
36
- const [yLLo, yLHi] = domain(leftLayers);
37
- const [yRLo, yRHi] = rightLayers.length ? domain(rightLayers) : [0, 1];
38
-
39
- // Padding accommodates right axis when present.
40
- const pad = { l: 50, r: rightLayers.length ? 50 : 16, t: 30, b: 32 };
41
- const px = pad.l, py = pad.t;
42
- const pw = e.w - pad.l - pad.r;
43
- const ph = e.h - pad.t - pad.b;
44
- const font = `${t.fontSize}px ${t.font}`;
45
-
46
- // Y axes
47
- const yLPx = v => py + ph * (1 - (v - yLLo) / (yLHi - yLLo));
48
- const yRPx = v => py + ph * (1 - (v - yRLo) / (yRHi - yRLo));
49
- const yLTicks = M.niceTicks(yLLo, yLHi, 5);
50
- const yRTicks = rightLayers.length ? M.niceTicks(yRLo, yRHi, 5) : [];
51
-
52
- // Grid
53
- e.ctx.strokeStyle = t.grid;
54
- e.ctx.lineWidth = 1;
55
- e.ctx.beginPath();
56
- for (const v of yLTicks) {
57
- const y = Math.round(yLPx(v)) + 0.5;
58
- e.ctx.moveTo(px, y); e.ctx.lineTo(px + pw, y);
59
- }
60
- e.ctx.stroke();
61
-
62
- // Y labels
63
- const yLFmt = this.options.yLeftTickFormat || (v => String(v));
64
- const yRFmt = this.options.yRightTickFormat || (v => String(v));
65
- for (const v of yLTicks) {
66
- e.text(yLFmt(v), px - 6, yLPx(v), {
67
- fill: t.muted, font, align: 'right', baseline: 'middle',
68
- });
69
- }
70
- for (const v of yRTicks) {
71
- e.text(yRFmt(v), px + pw + 6, yRPx(v), {
72
- fill: t.muted, font, align: 'left', baseline: 'middle',
73
- });
74
- }
75
-
76
- // X axis (categorical or time)
77
- const useTime = this.options.xScale === 'time';
78
- const xToPx = useTime
79
- ? (i) => px + ((+cats[i] - +cats[0]) / (+cats[cats.length - 1] - +cats[0] || 1)) * pw
80
- : (i) => px + ((i + 0.5) / cats.length) * pw;
81
-
82
- // X labels (collision-aware)
83
- let lastEnd = -Infinity;
84
- for (let i = 0; i < cats.length; i++) {
85
- const x = xToPx(i);
86
- const txt = useTime
87
- ? new Date(+cats[i]).toISOString().slice(0, 10)
88
- : String(cats[i]);
89
- const w = e.measure(txt, font);
90
- if (x - w / 2 < lastEnd + 4) continue;
91
- lastEnd = x + w / 2;
92
- e.text(txt, x, py + ph + 6, {
93
- fill: t.muted, font, align: 'center', baseline: 'top',
94
- });
95
- }
96
- e.poly([{x: px, y: py + ph}, {x: px + pw, y: py + ph}], { stroke: t.axis, lineWidth: 1 });
97
-
98
- const prog = this._anim.progress;
99
- const lineW = t.lineWidth;
100
-
101
- // Render layers in order (back-to-front).
102
- // Bars first (so lines/scatter can sit on top), grouped in clusters.
103
- const barLayers = layers.filter(l => l.type === 'bar');
104
- const slot = pw / cats.length;
105
- const innerSlot = slot * 0.7;
106
- const barW = barLayers.length ? Math.max(2, innerSlot / barLayers.length) : 0;
107
-
108
- barLayers.forEach((layer, bi) => {
109
- const yPx = layer.axis === 'right' ? yRPx : yLPx;
110
- const yBase = yPx(0);
111
- const color = layer.color || this._colorOf(layers.indexOf(layer), layer);
112
- const items = [];
113
- for (let i = 0; i < cats.length; i++) {
114
- const v = +layer.values[i] || 0;
115
- const xC = xToPx(i);
116
- const x = xC - innerSlot / 2 + bi * barW;
117
- const y0 = yBase;
118
- const y1 = yPx(v);
119
- const top = Math.min(y0, y1);
120
- const h = Math.abs(y1 - y0) * prog;
121
- const animTop = v >= 0 ? y0 - h : y0;
122
- items.push({ x, y: animTop, w: barW - 1, h, radius: t.barRadius });
123
- this.hits.add({
124
- _id: `${layer.name || bi}-${i}`,
125
- x, y: animTop, w: barW - 1, h,
126
- tipX: x + barW / 2, tipY: animTop,
127
- tooltip: `${layer.name || ''} · ${cats[i]}: ${v}`,
128
- });
129
- }
130
- e.rects(items, color);
131
- });
132
-
133
- // Areas
134
- e.clip(px, py, pw * prog, ph);
135
- layers.filter(l => l.type === 'area').forEach((layer) => {
136
- const yPx = layer.axis === 'right' ? yRPx : yLPx;
137
- const color = layer.color || this._colorOf(layers.indexOf(layer), layer);
138
- const pts = layer.values.map((v, i) => ({ x: xToPx(i), y: yPx(+v) }));
139
- const cps = M.bezierControlPoints(pts);
140
- const grad = e.vGradient(`area-${layer.name}-${color}`, px, py, py + ph, [
141
- [0, M.colorAlpha(color, 0.4)],
142
- [1, M.colorAlpha(color, 0)],
143
- ]);
144
- e.smoothArea(pts, cps, py + ph, grad);
145
- e.smooth(pts, cps, { stroke: color, lineWidth: lineW });
146
- });
147
-
148
- // Lines
149
- layers.filter(l => l.type === 'line').forEach((layer) => {
150
- const yPx = layer.axis === 'right' ? yRPx : yLPx;
151
- const color = layer.color || this._colorOf(layers.indexOf(layer), layer);
152
- const pts = layer.values.map((v, i) => ({ x: xToPx(i), y: yPx(+v), i, v }));
153
- const cps = M.bezierControlPoints(pts);
154
- e.smooth(pts, cps, { stroke: color, lineWidth: lineW });
155
- if (pts.length <= 50) e.circles(pts, color, t.pointRadius);
156
- // Hits per point
157
- for (const p of pts) {
158
- this.hits.add({
159
- _id: `${layer.name || 'line'}-${p.i}`,
160
- x: p.x - 6, y: p.y - 6, w: 12, h: 12,
161
- tipX: p.x, tipY: p.y - t.pointRadius,
162
- tooltip: `${layer.name || ''} · ${cats[p.i]}: ${p.v}`,
163
- });
164
- }
165
- });
166
- e.unclip();
167
-
168
- // Scatter
169
- layers.filter(l => l.type === 'scatter').forEach((layer) => {
170
- const yPx = layer.axis === 'right' ? yRPx : yLPx;
171
- const color = layer.color || this._colorOf(layers.indexOf(layer), layer);
172
- const items = layer.values.map((p) => ({
173
- x: xToPx(typeof p.x === 'number' ? p.x : cats.indexOf(p.x)),
174
- y: yPx(p.y),
175
- }));
176
- e.withAlpha(0.7, () => e.circles(items, color, layer.radius || t.pointRadius + 1));
177
- });
178
-
179
- // Legend
180
- this._drawLegend(layers.map((l, i) => ({
181
- label: (l.name || l.type) + (l.axis === 'right' ? ' (R)' : ''),
182
- color: l.color || this._colorOf(i, l),
183
- idx: i,
184
- hidden: false,
185
- })), pw);
186
- }
187
- }
package/src/engine.js DELETED
@@ -1,287 +0,0 @@
1
- // engine.js — Canvas 2D rendering primitives.
2
- // All public coordinates are CSS pixels. DPR is handled internally.
3
-
4
- import { TAU } from './math.js';
5
-
6
- export class Engine {
7
- constructor(canvas) {
8
- this.canvas = canvas;
9
- this.ctx = canvas.getContext('2d');
10
- this.w = 0;
11
- this.h = 0;
12
- this.dpr = 1;
13
- this._gradCache = new Map();
14
- this._gradCacheMax = 32;
15
- this._snap = true;
16
- }
17
-
18
- // Resize the backing store to (w × h) CSS pixels at the current DPR.
19
- // The internal coordinate system is reset so all subsequent draw calls
20
- // use CSS pixels.
21
- resize(w, h) {
22
- const dpr = (typeof window !== 'undefined' && window.devicePixelRatio) || 1;
23
- this.canvas.width = Math.round(w * dpr);
24
- this.canvas.height = Math.round(h * dpr);
25
- this.canvas.style.width = w + 'px';
26
- this.canvas.style.height = h + 'px';
27
- this.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
28
- this.w = w;
29
- this.h = h;
30
- this.dpr = dpr;
31
- this._gradCache.clear();
32
- }
33
-
34
- clear(color) {
35
- if (color) {
36
- this.ctx.fillStyle = color;
37
- this.ctx.fillRect(0, 0, this.w, this.h);
38
- } else {
39
- this.ctx.clearRect(0, 0, this.w, this.h);
40
- }
41
- }
42
-
43
- // ─── Primitives ─────────────────────────────────────────
44
- // All draw methods take CSS-pixel coordinates and apply pixel snapping
45
- // for crisp 1px lines. Snap is intentionally simple: x.5 alignment for
46
- // odd-width strokes, integer alignment for fills.
47
-
48
- rect(x, y, w, h, opts = {}) {
49
- // Guard against negative/zero dims that animations can momentarily produce.
50
- if (!(w > 0) || !(h > 0)) return;
51
- const ctx = this.ctx;
52
- if (opts.radius && opts.radius > 0) {
53
- const r = Math.max(0, Math.min(opts.radius, w / 2, h / 2));
54
- ctx.beginPath();
55
- ctx.moveTo(x + r, y);
56
- ctx.arcTo(x + w, y, x + w, y + h, r);
57
- ctx.arcTo(x + w, y + h, x, y + h, r);
58
- ctx.arcTo(x, y + h, x, y, r);
59
- ctx.arcTo(x, y, x + w, y, r);
60
- ctx.closePath();
61
- } else {
62
- ctx.beginPath();
63
- ctx.rect(x, y, w, h);
64
- }
65
- if (opts.fill) { ctx.fillStyle = opts.fill; ctx.fill(); }
66
- if (opts.stroke) {
67
- ctx.strokeStyle = opts.stroke;
68
- ctx.lineWidth = opts.lineWidth || 1;
69
- ctx.stroke();
70
- }
71
- }
72
-
73
- // Batch many rectangles of the same color in a single fill.
74
- rects(items, fill) {
75
- const ctx = this.ctx;
76
- ctx.fillStyle = fill;
77
- ctx.beginPath();
78
- for (const r of items) {
79
- // Skip degenerate items that would crash arcTo / rect.
80
- if (!(r.w > 0) || !(r.h > 0)) continue;
81
- if (r.radius && r.radius > 0) {
82
- const rad = Math.max(0, Math.min(r.radius, r.w / 2, r.h / 2));
83
- ctx.moveTo(r.x + rad, r.y);
84
- ctx.arcTo(r.x + r.w, r.y, r.x + r.w, r.y + r.h, rad);
85
- ctx.arcTo(r.x + r.w, r.y + r.h, r.x, r.y + r.h, rad);
86
- ctx.arcTo(r.x, r.y + r.h, r.x, r.y, rad);
87
- ctx.arcTo(r.x, r.y, r.x + r.w, r.y, rad);
88
- ctx.closePath();
89
- } else {
90
- ctx.rect(r.x, r.y, r.w, r.h);
91
- }
92
- }
93
- ctx.fill();
94
- }
95
-
96
- circle(x, y, r, opts = {}) {
97
- // arc() throws on negative radii; bail silently instead.
98
- if (!(r > 0)) return;
99
- const ctx = this.ctx;
100
- ctx.beginPath();
101
- ctx.arc(x, y, r, 0, TAU);
102
- if (opts.fill) { ctx.fillStyle = opts.fill; ctx.fill(); }
103
- if (opts.stroke) {
104
- ctx.strokeStyle = opts.stroke;
105
- ctx.lineWidth = opts.lineWidth || 1;
106
- ctx.stroke();
107
- }
108
- }
109
-
110
- // Batch many circles of the same color and radius.
111
- circles(items, fill, fixedRadius) {
112
- const ctx = this.ctx;
113
- ctx.fillStyle = fill;
114
- ctx.beginPath();
115
- for (const c of items) {
116
- const r = fixedRadius != null ? fixedRadius : c.r;
117
- if (!(r > 0)) continue;
118
- ctx.moveTo(c.x + r, c.y);
119
- ctx.arc(c.x, c.y, r, 0, TAU);
120
- }
121
- ctx.fill();
122
- }
123
-
124
- // Path of straight segments, optionally closed and filled.
125
- poly(points, opts = {}) {
126
- if (points.length < 2) return;
127
- const ctx = this.ctx;
128
- const snap = this._snap && opts.lineWidth === 1;
129
- const adj = snap ? 0.5 : 0;
130
- ctx.beginPath();
131
- ctx.moveTo(points[0].x + adj, points[0].y + adj);
132
- for (let i = 1; i < points.length; i++) {
133
- ctx.lineTo(points[i].x + adj, points[i].y + adj);
134
- }
135
- if (opts.close) ctx.closePath();
136
- if (opts.fill) { ctx.fillStyle = opts.fill; ctx.fill(); }
137
- if (opts.stroke) {
138
- ctx.strokeStyle = opts.stroke;
139
- ctx.lineWidth = opts.lineWidth || 1.5;
140
- ctx.lineCap = opts.cap || 'butt';
141
- ctx.lineJoin = opts.join || 'miter';
142
- if (opts.dash) ctx.setLineDash(opts.dash);
143
- ctx.stroke();
144
- if (opts.dash) ctx.setLineDash([]);
145
- }
146
- }
147
-
148
- // Smooth path drawn through `points` using bezier control points `cps`.
149
- smooth(points, cps, opts = {}) {
150
- if (points.length < 2) return;
151
- const ctx = this.ctx;
152
- ctx.beginPath();
153
- ctx.moveTo(points[0].x, points[0].y);
154
- for (let i = 0; i < cps.length; i++) {
155
- const c = cps[i];
156
- const p = points[i + 1];
157
- ctx.bezierCurveTo(c.c1x, c.c1y, c.c2x, c.c2y, p.x, p.y);
158
- }
159
- if (opts.fill) { ctx.fillStyle = opts.fill; ctx.fill(); }
160
- if (opts.stroke) {
161
- ctx.strokeStyle = opts.stroke;
162
- ctx.lineWidth = opts.lineWidth || 2;
163
- ctx.lineCap = opts.cap || 'round';
164
- ctx.lineJoin = opts.join || 'round';
165
- ctx.stroke();
166
- }
167
- }
168
-
169
- // Smooth-filled area from `points` down to baseline `y0`.
170
- smoothArea(points, cps, y0, fill) {
171
- if (points.length < 2) return;
172
- const ctx = this.ctx;
173
- ctx.beginPath();
174
- ctx.moveTo(points[0].x, y0);
175
- ctx.lineTo(points[0].x, points[0].y);
176
- for (let i = 0; i < cps.length; i++) {
177
- const c = cps[i];
178
- const p = points[i + 1];
179
- ctx.bezierCurveTo(c.c1x, c.c1y, c.c2x, c.c2y, p.x, p.y);
180
- }
181
- ctx.lineTo(points[points.length - 1].x, y0);
182
- ctx.closePath();
183
- ctx.fillStyle = fill;
184
- ctx.fill();
185
- }
186
-
187
- // Stepped path (3 modes).
188
- step(points, mode, opts = {}) {
189
- if (points.length < 2) return;
190
- const ctx = this.ctx;
191
- ctx.beginPath();
192
- ctx.moveTo(points[0].x, points[0].y);
193
- for (let i = 1; i < points.length; i++) {
194
- const a = points[i - 1], b = points[i];
195
- if (mode === 'after') {
196
- ctx.lineTo(b.x, a.y);
197
- ctx.lineTo(b.x, b.y);
198
- } else if (mode === 'middle') {
199
- const mx = (a.x + b.x) / 2;
200
- ctx.lineTo(mx, a.y);
201
- ctx.lineTo(mx, b.y);
202
- ctx.lineTo(b.x, b.y);
203
- } else {
204
- ctx.lineTo(a.x, b.y);
205
- ctx.lineTo(b.x, b.y);
206
- }
207
- }
208
- if (opts.stroke) {
209
- ctx.strokeStyle = opts.stroke;
210
- ctx.lineWidth = opts.lineWidth || 1.5;
211
- ctx.stroke();
212
- }
213
- }
214
-
215
- text(str, x, y, opts = {}) {
216
- const ctx = this.ctx;
217
- ctx.font = opts.font || '11px ui-monospace, monospace';
218
- ctx.fillStyle = opts.fill || '#000';
219
- ctx.textAlign = opts.align || 'left';
220
- ctx.textBaseline = opts.baseline || 'alphabetic';
221
- if (opts.rotate) {
222
- ctx.save();
223
- ctx.translate(x, y);
224
- ctx.rotate(opts.rotate);
225
- ctx.fillText(str, 0, 0);
226
- ctx.restore();
227
- } else {
228
- ctx.fillText(str, x, y);
229
- }
230
- }
231
-
232
- measure(str, font) {
233
- this.ctx.font = font || '11px ui-monospace, monospace';
234
- return this.ctx.measureText(str).width;
235
- }
236
-
237
- // Arc segment for donut/polar charts.
238
- arc(cx, cy, rIn, rOut, a0, a1, opts = {}) {
239
- // Negative radii crash ctx.arc; clamp at 0 and bail when both collapse.
240
- const ri = Math.max(0, rIn);
241
- const ro = Math.max(0, rOut);
242
- if (ro <= 0) return;
243
- const ctx = this.ctx;
244
- ctx.beginPath();
245
- ctx.arc(cx, cy, ro, a0, a1);
246
- ctx.arc(cx, cy, ri, a1, a0, true);
247
- ctx.closePath();
248
- if (opts.fill) { ctx.fillStyle = opts.fill; ctx.fill(); }
249
- if (opts.stroke) {
250
- ctx.strokeStyle = opts.stroke;
251
- ctx.lineWidth = opts.lineWidth || 1;
252
- ctx.stroke();
253
- }
254
- }
255
-
256
- // ─── Gradient cache ─────────────────────────────────────
257
- // Vertical gradient between two colors. Cached by key.
258
- vGradient(key, x0, y0, y1, stops) {
259
- if (this._gradCache.has(key)) return this._gradCache.get(key);
260
- const g = this.ctx.createLinearGradient(x0, y0, x0, y1);
261
- for (const [pos, color] of stops) g.addColorStop(pos, color);
262
- if (this._gradCache.size >= this._gradCacheMax) {
263
- const first = this._gradCache.keys().next().value;
264
- this._gradCache.delete(first);
265
- }
266
- this._gradCache.set(key, g);
267
- return g;
268
- }
269
-
270
- // ─── Clip region for plot area ──────────────────────────
271
- clip(x, y, w, h) {
272
- const ctx = this.ctx;
273
- ctx.save();
274
- ctx.beginPath();
275
- ctx.rect(x, y, w, h);
276
- ctx.clip();
277
- }
278
- unclip() { this.ctx.restore(); }
279
-
280
- withAlpha(a, fn) {
281
- const ctx = this.ctx;
282
- const prev = ctx.globalAlpha;
283
- ctx.globalAlpha = prev * a;
284
- fn();
285
- ctx.globalAlpha = prev;
286
- }
287
- }
package/src/index.js DELETED
@@ -1,18 +0,0 @@
1
- // index.js — public API.
2
-
3
- export { Chart, getFPS, getActiveCount, scheduler } from './base.js';
4
- export { Engine } from './engine.js';
5
- export * as math from './math.js';
6
- export { PALETTE } from './math.js';
7
-
8
- export {
9
- BarChart, HBarChart, LineChart, AreaChart,
10
- StackedBarChart, GroupedBarChart, MultiLineChart,
11
- ScatterChart, BubbleChart,
12
- PieChart, RadarChart, PolarChart,
13
- GaugeChart, HeatmapChart, BoxplotChart,
14
- FunnelChart, TreemapChart, Sparkline,
15
- RealtimeChart, WaterfallChart,
16
- } from './charts.js';
17
-
18
- export { CompositeChart } from './composite.js';
package/src/math.js DELETED
@@ -1,178 +0,0 @@
1
- // math.js — Pure math: scales, ticks, color ops, smoothing, decimation
2
- // No DOM, no canvas, no side effects.
3
-
4
- export const TAU = Math.PI * 2;
5
-
6
- export const clamp = (v, lo, hi) => v < lo ? lo : v > hi ? hi : v;
7
- export const lerp = (a, b, t) => a + (b - a) * t;
8
-
9
- export function extent(arr, fn = (x) => x) {
10
- let mn = Infinity, mx = -Infinity;
11
- for (let i = 0; i < arr.length; i++) {
12
- const v = fn(arr[i], i);
13
- if (v < mn) mn = v;
14
- if (v > mx) mx = v;
15
- }
16
- return [mn === Infinity ? 0 : mn, mx === -Infinity ? 1 : mx];
17
- }
18
-
19
- // "Nice" tick generator for linear axes. Returns ticks at human-readable
20
- // values (1, 2, 5 × 10^k) covering the input domain.
21
- export function niceTicks(min, max, count = 5) {
22
- if (min === max) { min -= 0.5; max += 0.5; }
23
- const span = max - min;
24
- const step0 = span / Math.max(1, count);
25
- const mag = Math.pow(10, Math.floor(Math.log10(step0)));
26
- const norm = step0 / mag;
27
- let step;
28
- if (norm < 1.5) step = mag;
29
- else if (norm < 3) step = 2 * mag;
30
- else if (norm < 7) step = 5 * mag;
31
- else step = 10 * mag;
32
- const lo = Math.ceil(min / step) * step;
33
- const hi = Math.floor(max / step) * step;
34
- const ticks = [];
35
- for (let v = lo; v <= hi + step / 2; v += step) {
36
- ticks.push(Math.round(v / step) * step);
37
- }
38
- return ticks;
39
- }
40
-
41
- export function niceDomain(min, max, count = 5) {
42
- if (min === max) { min -= 0.5; max += 0.5; }
43
- const span = max - min;
44
- const step0 = span / Math.max(1, count);
45
- const mag = Math.pow(10, Math.floor(Math.log10(step0)));
46
- const norm = step0 / mag;
47
- let step;
48
- if (norm < 1.5) step = mag;
49
- else if (norm < 3) step = 2 * mag;
50
- else if (norm < 7) step = 5 * mag;
51
- else step = 10 * mag;
52
- return [Math.floor(min / step) * step, Math.ceil(max / step) * step];
53
- }
54
-
55
- // Time axis: pick a nice unit (second, minute, hour, day, week, month) and
56
- // generate ticks aligned to it. Returns timestamps in ms.
57
- const T = { sec: 1e3, min: 6e4, hour: 36e5, day: 864e5, week: 6.048e8, month: 2.628e9 };
58
- export function timeNiceTicks(minMs, maxMs, count = 6) {
59
- if (minMs === maxMs) { minMs -= 1000; maxMs += 1000; }
60
- const span = maxMs - minMs;
61
- const target = span / count;
62
- const units = [
63
- [T.sec, 1], [T.sec, 5], [T.sec, 15], [T.sec, 30],
64
- [T.min, 1], [T.min, 5], [T.min, 15], [T.min, 30],
65
- [T.hour, 1], [T.hour, 3], [T.hour, 6], [T.hour, 12],
66
- [T.day, 1], [T.day, 2], [T.week, 1],
67
- [T.month, 1], [T.month, 3], [T.month, 6], [T.month, 12],
68
- ];
69
- let step = T.sec;
70
- for (const [u, n] of units) if (u * n <= target * 1.5) step = u * n;
71
- const lo = Math.ceil(minMs / step) * step;
72
- const ticks = [];
73
- for (let v = lo; v <= maxMs; v += step) ticks.push(v);
74
- return ticks;
75
- }
76
-
77
- export function timeFormat(step) {
78
- if (step < T.min) return (t) => new Date(t).toISOString().slice(11, 19);
79
- if (step < T.hour) return (t) => new Date(t).toISOString().slice(11, 16);
80
- if (step < T.day) return (t) => new Date(t).toISOString().slice(11, 16);
81
- if (step < T.month) return (t) => new Date(t).toISOString().slice(5, 10);
82
- return (t) => new Date(t).toISOString().slice(0, 7);
83
- }
84
-
85
- // Min-max decimation: when N points exceed pixel count, compress to ~2 per
86
- // pixel column preserving local min and max. Used for line charts with
87
- // huge datasets.
88
- export function minMaxDecimate(data, targetCount, fn = (d) => d.value) {
89
- if (data.length <= targetCount) return data;
90
- const bucket = data.length / targetCount;
91
- const out = [];
92
- for (let i = 0; i < targetCount; i++) {
93
- const lo = Math.floor(i * bucket);
94
- const hi = Math.min(data.length, Math.floor((i + 1) * bucket));
95
- let mnI = lo, mxI = lo;
96
- let mn = fn(data[lo], lo), mx = mn;
97
- for (let j = lo + 1; j < hi; j++) {
98
- const v = fn(data[j], j);
99
- if (v < mn) { mn = v; mnI = j; }
100
- if (v > mx) { mx = v; mxI = j; }
101
- }
102
- if (mnI < mxI) { out.push(data[mnI]); out.push(data[mxI]); }
103
- else { out.push(data[mxI]); out.push(data[mnI]); }
104
- }
105
- return out;
106
- }
107
-
108
- export function shouldDecimate(n, pxWidth) {
109
- return n > pxWidth * 2;
110
- }
111
-
112
- // Smooth path control points (centripetal Catmull-Rom-ish). Returns array
113
- // of {c1x,c1y,c2x,c2y} to use with bezierCurveTo.
114
- export function bezierControlPoints(pts, tension = 1 / 6) {
115
- const n = pts.length;
116
- if (n < 2) return [];
117
- const cps = [];
118
- for (let i = 0; i < n - 1; i++) {
119
- const p0 = pts[i - 1] || pts[i];
120
- const p1 = pts[i];
121
- const p2 = pts[i + 1];
122
- const p3 = pts[i + 2] || p2;
123
- cps.push({
124
- c1x: p1.x + (p2.x - p0.x) * tension,
125
- c1y: p1.y + (p2.y - p0.y) * tension,
126
- c2x: p2.x - (p3.x - p1.x) * tension,
127
- c2y: p2.y - (p3.y - p1.y) * tension,
128
- });
129
- }
130
- return cps;
131
- }
132
-
133
- // Spring physics step. Used for animated transitions.
134
- export function springStep(v, tgt, vel, cfg = {}) {
135
- const stiffness = cfg.stiffness || 170;
136
- const damping = cfg.damping || 26;
137
- const dt = 1 / 60;
138
- const F = -stiffness * (v - tgt);
139
- const D = -damping * vel;
140
- const a = F + D;
141
- vel = vel + a * dt;
142
- v = v + vel * dt;
143
- const done = Math.abs(vel) < 0.05 && Math.abs(v - tgt) < 0.05;
144
- if (done) { v = tgt; vel = 0; }
145
- return { value: v, velocity: vel, done };
146
- }
147
-
148
- // Color helpers: hex/rgb/rgba parsing, alpha mixing, lightening.
149
- export function colorParse(c) {
150
- if (!c) return [0, 0, 0, 1];
151
- if (c[0] === '#') {
152
- const h = c.slice(1);
153
- if (h.length === 3) return [parseInt(h[0]+h[0],16), parseInt(h[1]+h[1],16), parseInt(h[2]+h[2],16), 1];
154
- if (h.length === 6) return [parseInt(h.slice(0,2),16), parseInt(h.slice(2,4),16), parseInt(h.slice(4,6),16), 1];
155
- if (h.length === 8) return [parseInt(h.slice(0,2),16), parseInt(h.slice(2,4),16), parseInt(h.slice(4,6),16), parseInt(h.slice(6,8),16)/255];
156
- }
157
- const m = c.match(/rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)(?:[,\s/]+([\d.]+))?\s*\)/);
158
- if (m) return [+m[1], +m[2], +m[3], m[4] !== undefined ? +m[4] : 1];
159
- return [0, 0, 0, 1];
160
- }
161
-
162
- export function colorAlpha(c, a) {
163
- const [r, g, b] = colorParse(c);
164
- return `rgba(${r|0},${g|0},${b|0},${a})`;
165
- }
166
-
167
- export function colorMix(a, b, t) {
168
- const [r1, g1, b1, a1] = colorParse(a);
169
- const [r2, g2, b2, a2] = colorParse(b);
170
- return `rgba(${(r1+(r2-r1)*t)|0},${(g1+(g2-g1)*t)|0},${(b1+(b2-b1)*t)|0},${a1+(a2-a1)*t})`;
171
- }
172
-
173
- // Default qualitative palette (8 colors, distinct hues, balanced lightness).
174
- // Lead color: #64c8ff (rgba(100,200,255,0.85) brand accent).
175
- export const PALETTE = [
176
- '#64c8ff', '#10b981', '#f59e0b', '#ef4444',
177
- '#8b5cf6', '#ec4899', '#84cc16', '#6366f1',
178
- ];