@aeriz/chart 1.0.1 → 2.0.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/LICENSE +201 -201
- package/README.md +64 -431
- package/dist/aeriz.cjs +2781 -2338
- package/dist/aeriz.esm.js +1859 -1459
- package/dist/aeriz.esm.min.js +278 -1
- package/dist/aeriz.js +2781 -2338
- package/dist/aeriz.min.cjs +282 -1
- package/dist/aeriz.min.js +282 -1
- package/dist/esm/base.js +84 -0
- package/dist/esm/cartesian.js +13 -0
- package/dist/esm/charts/area.js +9 -0
- package/dist/esm/charts/bar.js +4 -0
- package/dist/esm/charts/boxplot.js +4 -0
- package/dist/esm/charts/bubble.js +3 -0
- package/dist/esm/charts/composite.js +11 -0
- package/dist/esm/charts/funnel.js +3 -0
- package/dist/esm/charts/gauge.js +5 -0
- package/dist/esm/charts/grouped-bar.js +5 -0
- package/dist/esm/charts/hbar.js +6 -0
- package/dist/esm/charts/heatmap.js +7 -0
- package/dist/esm/charts/line.js +9 -0
- package/dist/esm/charts/multi-line.js +8 -0
- package/dist/esm/charts/pie.js +6 -0
- package/dist/esm/charts/polar.js +3 -0
- package/dist/esm/charts/radar.js +10 -0
- package/dist/esm/charts/realtime.js +14 -0
- package/dist/esm/charts/scatter.js +6 -0
- package/dist/esm/charts/sparkline.js +5 -0
- package/dist/esm/charts/stacked-bar.js +7 -0
- package/dist/esm/charts/treemap.js +10 -0
- package/dist/esm/charts/waterfall.js +5 -0
- package/dist/esm/engine.js +33 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/jquery.js +5 -0
- package/dist/esm/math.js +32 -0
- package/dist/esm/react.js +4 -0
- package/dist/esm/shallow.js +5 -0
- package/dist/esm/svelte.js +4 -0
- package/dist/esm/vue.js +4 -0
- package/dist/index.d.ts +323 -0
- package/dist/jquery.d.ts +21 -0
- package/dist/react.d.ts +59 -0
- package/dist/svelte.d.ts +56 -0
- package/dist/types/area.d.ts +1 -0
- package/dist/types/bar.d.ts +1 -0
- package/dist/types/boxplot.d.ts +1 -0
- package/dist/types/bubble.d.ts +1 -0
- package/dist/types/composite.d.ts +1 -0
- package/dist/types/funnel.d.ts +1 -0
- package/dist/types/gauge.d.ts +1 -0
- package/dist/types/grouped-bar.d.ts +1 -0
- package/dist/types/hbar.d.ts +1 -0
- package/dist/types/heatmap.d.ts +1 -0
- package/dist/types/line.d.ts +1 -0
- package/dist/types/multi-line.d.ts +1 -0
- package/dist/types/pie.d.ts +1 -0
- package/dist/types/polar.d.ts +1 -0
- package/dist/types/radar.d.ts +1 -0
- package/dist/types/realtime.d.ts +1 -0
- package/dist/types/scatter.d.ts +1 -0
- package/dist/types/sparkline.d.ts +1 -0
- package/dist/types/stacked-bar.d.ts +1 -0
- package/dist/types/treemap.d.ts +1 -0
- package/dist/types/waterfall.d.ts +1 -0
- package/dist/vue.d.ts +63 -0
- package/package.json +60 -29
- package/src/base.js +0 -562
- package/src/charts.js +0 -1365
- package/src/composite.js +0 -187
- package/src/engine.js +0 -287
- package/src/index.d.ts +0 -517
- package/src/index.js +0 -18
- package/src/math.js +0 -178
package/src/base.js
DELETED
|
@@ -1,562 +0,0 @@
|
|
|
1
|
-
// base.js — Chart base class. Handles:
|
|
2
|
-
// • Lifecycle (mount, resize, destroy, animation loop)
|
|
3
|
-
// • Theme resolution from CSS variables + reactive invalidation
|
|
4
|
-
// • Hit testing (pixel-perfect, no DPR confusion)
|
|
5
|
-
// • Tooltip (DOM, GPU-composited)
|
|
6
|
-
// • Axes (linear, log, time, category)
|
|
7
|
-
// • State overlays (loading, empty, error)
|
|
8
|
-
|
|
9
|
-
import { Engine } from './engine.js';
|
|
10
|
-
import * as M from './math.js';
|
|
11
|
-
|
|
12
|
-
// ─── Global animation scheduler ─────────────────────────────
|
|
13
|
-
const charts = new Set();
|
|
14
|
-
let rafId = null;
|
|
15
|
-
let lastTime = 0;
|
|
16
|
-
let fps = 60;
|
|
17
|
-
let frames = 0;
|
|
18
|
-
let fpsT = 0;
|
|
19
|
-
|
|
20
|
-
function tick(now) {
|
|
21
|
-
if (now - fpsT > 500) {
|
|
22
|
-
fps = Math.round((1000 * frames) / (now - fpsT));
|
|
23
|
-
frames = 0;
|
|
24
|
-
fpsT = now;
|
|
25
|
-
}
|
|
26
|
-
frames++;
|
|
27
|
-
let active = false;
|
|
28
|
-
for (const c of charts) {
|
|
29
|
-
try {
|
|
30
|
-
const stillActive = c._tick(now);
|
|
31
|
-
if (stillActive) active = true;
|
|
32
|
-
} catch (e) { console.error(e); }
|
|
33
|
-
}
|
|
34
|
-
rafId = (charts.size > 0 && active) ? requestAnimationFrame(tick) : null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function schedule(chart) {
|
|
38
|
-
charts.add(chart);
|
|
39
|
-
if (rafId === null) {
|
|
40
|
-
fpsT = lastTime = performance.now();
|
|
41
|
-
rafId = requestAnimationFrame(tick);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export const scheduler = { schedule, fps: () => fps, count: () => charts.size };
|
|
46
|
-
export function getFPS() { return fps; }
|
|
47
|
-
export function getActiveCount() { return charts.size; }
|
|
48
|
-
|
|
49
|
-
// ─── Theme resolution ───────────────────────────────────────
|
|
50
|
-
const TOKENS = {
|
|
51
|
-
bg: ['--aeriz-bg', '#ffffff'],
|
|
52
|
-
surface: ['--aeriz-surface', 'transparent'],
|
|
53
|
-
text: ['--aeriz-text', '#222'],
|
|
54
|
-
muted: ['--aeriz-muted', '#888'],
|
|
55
|
-
grid: ['--aeriz-grid', 'rgba(0,0,0,0.08)'],
|
|
56
|
-
axis: ['--aeriz-axis', 'rgba(0,0,0,0.3)'],
|
|
57
|
-
accent: ['--aeriz-accent', '#64c8ff'],
|
|
58
|
-
font: ['--aeriz-font', 'ui-monospace, SFMono-Regular, Menlo, monospace'],
|
|
59
|
-
fontSize: ['--aeriz-font-size', '11', 'number'],
|
|
60
|
-
lineWidth: ['--aeriz-line-width', '2', 'number'],
|
|
61
|
-
pointRadius: ['--aeriz-point-radius', '3', 'number'],
|
|
62
|
-
barRadius: ['--aeriz-bar-radius', '3', 'number'],
|
|
63
|
-
tooltipBg: ['--aeriz-tooltip-bg', 'rgba(20,20,28,0.95)'],
|
|
64
|
-
tooltipFg: ['--aeriz-tooltip-fg', '#fff'],
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
function resolveTheme(el) {
|
|
68
|
-
const cs = getComputedStyle(el);
|
|
69
|
-
const out = {};
|
|
70
|
-
for (const [k, [varName, fallback, kind]] of Object.entries(TOKENS)) {
|
|
71
|
-
const raw = (cs.getPropertyValue(varName) || '').trim();
|
|
72
|
-
const v = raw || fallback;
|
|
73
|
-
out[k] = kind === 'number' ? parseFloat(v) : v;
|
|
74
|
-
}
|
|
75
|
-
return out;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ─── Color palette resolution ───────────────────────────────
|
|
79
|
-
function readPalette(el) {
|
|
80
|
-
const cs = getComputedStyle(el);
|
|
81
|
-
const out = [];
|
|
82
|
-
for (let i = 1; i <= 12; i++) {
|
|
83
|
-
const v = (cs.getPropertyValue(`--aeriz-color-${i}`) || '').trim();
|
|
84
|
-
if (v) out.push(v);
|
|
85
|
-
}
|
|
86
|
-
return out.length ? out : M.PALETTE;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ─── Tooltip ────────────────────────────────────────────────
|
|
90
|
-
class Tooltip {
|
|
91
|
-
constructor(parent) {
|
|
92
|
-
this.parent = parent;
|
|
93
|
-
this.el = document.createElement('div');
|
|
94
|
-
Object.assign(this.el.style, {
|
|
95
|
-
position: 'absolute',
|
|
96
|
-
pointerEvents: 'none',
|
|
97
|
-
padding: '6px 10px',
|
|
98
|
-
borderRadius: '4px',
|
|
99
|
-
fontSize: '11px',
|
|
100
|
-
fontFamily: 'inherit',
|
|
101
|
-
lineHeight: '1.5',
|
|
102
|
-
whiteSpace: 'nowrap',
|
|
103
|
-
opacity: '0',
|
|
104
|
-
transform: 'translate(-50%, calc(-100% - 8px))',
|
|
105
|
-
transition: 'opacity 0.1s',
|
|
106
|
-
zIndex: '20',
|
|
107
|
-
boxShadow: '0 4px 16px rgba(0,0,0,0.3)',
|
|
108
|
-
});
|
|
109
|
-
parent.appendChild(this.el);
|
|
110
|
-
}
|
|
111
|
-
show(x, y, content, theme) {
|
|
112
|
-
this.el.style.background = theme.tooltipBg;
|
|
113
|
-
this.el.style.color = theme.tooltipFg;
|
|
114
|
-
if (typeof content === 'string') {
|
|
115
|
-
this.el.textContent = content;
|
|
116
|
-
} else {
|
|
117
|
-
this.el.innerHTML = '';
|
|
118
|
-
if (content.title) {
|
|
119
|
-
const t = document.createElement('div');
|
|
120
|
-
t.style.cssText = 'opacity:0.7;font-size:10px;margin-bottom:4px';
|
|
121
|
-
t.textContent = content.title;
|
|
122
|
-
this.el.appendChild(t);
|
|
123
|
-
}
|
|
124
|
-
for (const r of content.rows) {
|
|
125
|
-
const row = document.createElement('div');
|
|
126
|
-
row.style.cssText = 'display:flex;align-items:center;gap:6px;line-height:1.4';
|
|
127
|
-
if (r.color) {
|
|
128
|
-
const sw = document.createElement('span');
|
|
129
|
-
sw.style.cssText = `width:8px;height:8px;border-radius:2px;background:${r.color};display:inline-block`;
|
|
130
|
-
row.appendChild(sw);
|
|
131
|
-
}
|
|
132
|
-
const nm = document.createElement('span');
|
|
133
|
-
nm.style.cssText = 'opacity:0.8;flex:1';
|
|
134
|
-
nm.textContent = r.name || '';
|
|
135
|
-
row.appendChild(nm);
|
|
136
|
-
const val = document.createElement('span');
|
|
137
|
-
val.style.cssText = 'font-weight:700;margin-left:12px';
|
|
138
|
-
val.textContent = r.value == null ? '—' : String(r.value);
|
|
139
|
-
row.appendChild(val);
|
|
140
|
-
this.el.appendChild(row);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
this.el.style.left = x + 'px';
|
|
144
|
-
this.el.style.top = y + 'px';
|
|
145
|
-
this.el.style.opacity = '1';
|
|
146
|
-
}
|
|
147
|
-
hide() { this.el.style.opacity = '0'; }
|
|
148
|
-
destroy() { this.el.remove(); }
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ─── State overlays (loading / empty / error) ───────────────
|
|
152
|
-
class Overlay {
|
|
153
|
-
constructor(parent) {
|
|
154
|
-
this.parent = parent;
|
|
155
|
-
this.el = document.createElement('div');
|
|
156
|
-
Object.assign(this.el.style, {
|
|
157
|
-
position: 'absolute',
|
|
158
|
-
inset: '0',
|
|
159
|
-
display: 'none',
|
|
160
|
-
alignItems: 'center',
|
|
161
|
-
justifyContent: 'center',
|
|
162
|
-
flexDirection: 'column',
|
|
163
|
-
gap: '8px',
|
|
164
|
-
fontSize: '12px',
|
|
165
|
-
fontFamily: 'inherit',
|
|
166
|
-
pointerEvents: 'none',
|
|
167
|
-
zIndex: '15',
|
|
168
|
-
});
|
|
169
|
-
parent.appendChild(this.el);
|
|
170
|
-
}
|
|
171
|
-
show(kind, msg, theme) {
|
|
172
|
-
const el = this.el;
|
|
173
|
-
el.style.display = 'flex';
|
|
174
|
-
el.style.background = M.colorAlpha(theme.bg, 0.85);
|
|
175
|
-
el.style.color = theme.text;
|
|
176
|
-
el.innerHTML = '';
|
|
177
|
-
const icon = document.createElement('div');
|
|
178
|
-
icon.style.fontSize = '20px';
|
|
179
|
-
icon.style.opacity = '0.6';
|
|
180
|
-
if (kind === 'loading') {
|
|
181
|
-
icon.innerHTML = `<svg width="22" height="22" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="none" stroke="${theme.accent}" stroke-width="2.5" stroke-linecap="round" stroke-dasharray="14 50"><animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="1s" repeatCount="indefinite"/></circle></svg>`;
|
|
182
|
-
} else if (kind === 'empty') {
|
|
183
|
-
icon.textContent = '◌';
|
|
184
|
-
} else if (kind === 'error') {
|
|
185
|
-
icon.textContent = '!';
|
|
186
|
-
icon.style.color = '#ef4444';
|
|
187
|
-
}
|
|
188
|
-
el.appendChild(icon);
|
|
189
|
-
if (msg) {
|
|
190
|
-
const t = document.createElement('div');
|
|
191
|
-
t.style.opacity = '0.85';
|
|
192
|
-
t.textContent = msg;
|
|
193
|
-
el.appendChild(t);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
hide() { this.el.style.display = 'none'; }
|
|
197
|
-
destroy() { this.el.remove(); }
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// ─── Hit index ──────────────────────────────────────────────
|
|
201
|
-
// Canvas-pixel rectangles with arbitrary metadata. Used for hover/click.
|
|
202
|
-
class HitIndex {
|
|
203
|
-
constructor() { this.regions = []; }
|
|
204
|
-
clear() { this.regions.length = 0; }
|
|
205
|
-
add(r) { this.regions.push(r); }
|
|
206
|
-
// Find the topmost (last-added) region containing (x, y).
|
|
207
|
-
find(x, y) {
|
|
208
|
-
for (let i = this.regions.length - 1; i >= 0; i--) {
|
|
209
|
-
const r = this.regions[i];
|
|
210
|
-
if (x >= r.x && x <= r.x + r.w && y >= r.y && y <= r.y + r.h) return r;
|
|
211
|
-
}
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ─── Chart base ─────────────────────────────────────────────
|
|
217
|
-
export class Chart {
|
|
218
|
-
constructor(container, options = {}) {
|
|
219
|
-
if (!container) throw new Error('Chart: container is required');
|
|
220
|
-
this.container = typeof container === 'string'
|
|
221
|
-
? document.querySelector(container)
|
|
222
|
-
: container;
|
|
223
|
-
this.options = options;
|
|
224
|
-
|
|
225
|
-
// Mark container for external CSS targeting.
|
|
226
|
-
this.container.classList.add('aeriz');
|
|
227
|
-
if (getComputedStyle(this.container).position === 'static') {
|
|
228
|
-
this.container.style.position = 'relative';
|
|
229
|
-
}
|
|
230
|
-
if (!this.container.style.height && this.container.offsetHeight === 0) {
|
|
231
|
-
this.container.style.height = '320px';
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Canvas fills the *content area* (inside container's padding/border).
|
|
235
|
-
// We size the canvas from clientWidth/clientHeight (CSS content box).
|
|
236
|
-
this.canvas = document.createElement('canvas');
|
|
237
|
-
this.canvas.style.cssText = 'display:block;width:100%;height:100%;touch-action:none';
|
|
238
|
-
this.container.appendChild(this.canvas);
|
|
239
|
-
|
|
240
|
-
this.engine = new Engine(this.canvas);
|
|
241
|
-
this.tooltip = new Tooltip(this.container);
|
|
242
|
-
this.overlay = new Overlay(this.container);
|
|
243
|
-
this.hits = new HitIndex();
|
|
244
|
-
|
|
245
|
-
this.theme = resolveTheme(this.container);
|
|
246
|
-
this.palette = options.palette || readPalette(this.container);
|
|
247
|
-
this._cursor = null;
|
|
248
|
-
this._hover = null;
|
|
249
|
-
this._dirty = true;
|
|
250
|
-
this._destroyed = false;
|
|
251
|
-
this._mounted = false;
|
|
252
|
-
this._anim = { progress: 0, start: 0, duration: 600 };
|
|
253
|
-
this._state = null; // 'loading' | 'empty' | 'error' | null
|
|
254
|
-
|
|
255
|
-
this._setupEvents();
|
|
256
|
-
this._observeResize();
|
|
257
|
-
this._resize();
|
|
258
|
-
|
|
259
|
-
schedule(this);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// ─── Public API ──────────────────────────────────────────
|
|
263
|
-
// Manually re-resolve theme + palette and trigger a redraw. Use this
|
|
264
|
-
// after changing CSS variables on :root or the container.
|
|
265
|
-
invalidate() {
|
|
266
|
-
this.theme = resolveTheme(this.container);
|
|
267
|
-
this.palette = this.options.palette || readPalette(this.container);
|
|
268
|
-
this.engine._gradCache.clear();
|
|
269
|
-
this._dirty = true;
|
|
270
|
-
schedule(this);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
setData(data) {
|
|
274
|
-
this.options.data = data;
|
|
275
|
-
// Clear auto-set empty state so the new data actually renders.
|
|
276
|
-
if (this._state === 'empty') this._state = null;
|
|
277
|
-
this._anim.start = performance.now();
|
|
278
|
-
this._anim.progress = 0;
|
|
279
|
-
this._dirty = true;
|
|
280
|
-
schedule(this);
|
|
281
|
-
return this;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
setLoading(msg) { this._state = 'loading'; this._stateMsg = msg || ''; this._dirty = true; schedule(this); return this; }
|
|
285
|
-
setEmpty(msg) { this._state = 'empty'; this._stateMsg = msg || 'No data'; this._dirty = true; schedule(this); return this; }
|
|
286
|
-
setError(msg) { this._state = 'error'; this._stateMsg = msg || 'Error'; this._dirty = true; schedule(this); return this; }
|
|
287
|
-
clearState() { this._state = null; this._dirty = true; schedule(this); return this; }
|
|
288
|
-
|
|
289
|
-
destroy() {
|
|
290
|
-
this._destroyed = true;
|
|
291
|
-
charts.delete(this);
|
|
292
|
-
this._ro && this._ro.disconnect();
|
|
293
|
-
this._removeEvents();
|
|
294
|
-
this.tooltip.destroy();
|
|
295
|
-
this.overlay.destroy();
|
|
296
|
-
this.canvas.remove();
|
|
297
|
-
this.container.classList.remove('aeriz');
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// ─── Internals ───────────────────────────────────────────
|
|
301
|
-
_resize() {
|
|
302
|
-
// Use container.clientWidth/Height — these are the *content area*
|
|
303
|
-
// (excludes padding/border), which is exactly where the canvas
|
|
304
|
-
// (a child filling 100%) sits. This guarantees mouse events at
|
|
305
|
-
// canvas-relative coordinates correspond to draw coordinates.
|
|
306
|
-
const w = this.container.clientWidth;
|
|
307
|
-
const h = this.container.clientHeight;
|
|
308
|
-
if (w === this.engine.w && h === this.engine.h) return;
|
|
309
|
-
this.engine.resize(Math.max(50, w), Math.max(50, h));
|
|
310
|
-
this.theme = resolveTheme(this.container);
|
|
311
|
-
this._dirty = true;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
_observeResize() {
|
|
315
|
-
if (typeof ResizeObserver === 'undefined') return;
|
|
316
|
-
this._ro = new ResizeObserver(() => {
|
|
317
|
-
this._resize();
|
|
318
|
-
schedule(this);
|
|
319
|
-
});
|
|
320
|
-
this._ro.observe(this.container);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
_setupEvents() {
|
|
324
|
-
const c = this.canvas;
|
|
325
|
-
this._handlers = {
|
|
326
|
-
move: (e) => this._onMove(e),
|
|
327
|
-
leave: () => this._onLeave(),
|
|
328
|
-
click: (e) => this._onClick(e),
|
|
329
|
-
};
|
|
330
|
-
c.addEventListener('mousemove', this._handlers.move);
|
|
331
|
-
c.addEventListener('mouseleave', this._handlers.leave);
|
|
332
|
-
c.addEventListener('click', this._handlers.click);
|
|
333
|
-
}
|
|
334
|
-
_removeEvents() {
|
|
335
|
-
const c = this.canvas;
|
|
336
|
-
c.removeEventListener('mousemove', this._handlers.move);
|
|
337
|
-
c.removeEventListener('mouseleave', this._handlers.leave);
|
|
338
|
-
c.removeEventListener('click', this._handlers.click);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// Convert a mouse event to canvas-CSS-pixel coordinates.
|
|
342
|
-
_ptr(e) {
|
|
343
|
-
const r = this.canvas.getBoundingClientRect();
|
|
344
|
-
return { x: e.clientX - r.left, y: e.clientY - r.top };
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
_onMove(e) {
|
|
348
|
-
const p = this._ptr(e);
|
|
349
|
-
this._cursor = p;
|
|
350
|
-
const hit = this.hits.find(p.x, p.y);
|
|
351
|
-
const changed = (hit?._id ?? null) !== (this._hover?._id ?? null);
|
|
352
|
-
this._hover = hit;
|
|
353
|
-
if (changed) {
|
|
354
|
-
if (hit && hit.tooltip) {
|
|
355
|
-
this.tooltip.show(
|
|
356
|
-
hit.tipX != null ? hit.tipX : hit.x + hit.w / 2,
|
|
357
|
-
hit.tipY != null ? hit.tipY : hit.y,
|
|
358
|
-
hit.tooltip,
|
|
359
|
-
this.theme,
|
|
360
|
-
);
|
|
361
|
-
} else if (!hit && !this._sharedTooltip) {
|
|
362
|
-
this.tooltip.hide();
|
|
363
|
-
}
|
|
364
|
-
this._dirty = true;
|
|
365
|
-
schedule(this);
|
|
366
|
-
}
|
|
367
|
-
// Subclass hook for shared/follow tooltips
|
|
368
|
-
if (typeof this._onMoveX === 'function') this._onMoveX(p);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
_onLeave() {
|
|
372
|
-
this._cursor = null;
|
|
373
|
-
this._hover = null;
|
|
374
|
-
this.tooltip.hide();
|
|
375
|
-
this._dirty = true;
|
|
376
|
-
schedule(this);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
_onClick(e) {
|
|
380
|
-
const p = this._ptr(e);
|
|
381
|
-
const hit = this.hits.find(p.x, p.y);
|
|
382
|
-
if (this.options.onClick) this.options.onClick(hit, p, this);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// Per-frame: advance animation, draw if dirty, return whether more frames
|
|
386
|
-
// are needed.
|
|
387
|
-
_tick(now) {
|
|
388
|
-
if (this._destroyed) return false;
|
|
389
|
-
let active = false;
|
|
390
|
-
if (this._anim.progress < 1) {
|
|
391
|
-
if (!this._anim.start) this._anim.start = now;
|
|
392
|
-
this._anim.progress = Math.min(1, (now - this._anim.start) / this._anim.duration);
|
|
393
|
-
this._dirty = true;
|
|
394
|
-
if (this._anim.progress < 1) active = true;
|
|
395
|
-
}
|
|
396
|
-
if (this._dirty) {
|
|
397
|
-
this._draw();
|
|
398
|
-
this._dirty = false;
|
|
399
|
-
}
|
|
400
|
-
return active;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
_draw() {
|
|
404
|
-
const e = this.engine;
|
|
405
|
-
e.clear();
|
|
406
|
-
|
|
407
|
-
// Background fill if surface is set.
|
|
408
|
-
if (this.theme.surface && this.theme.surface !== 'transparent') {
|
|
409
|
-
e.rect(0, 0, e.w, e.h, { fill: this.theme.surface });
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// State overlay paths
|
|
413
|
-
if (this._state) {
|
|
414
|
-
this.overlay.show(this._state, this._stateMsg, this.theme);
|
|
415
|
-
this.hits.clear();
|
|
416
|
-
return;
|
|
417
|
-
} else {
|
|
418
|
-
this.overlay.hide();
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Subclass renders here.
|
|
422
|
-
this.hits.clear();
|
|
423
|
-
if (typeof this._render === 'function') this._render();
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// ─── Helpers shared by subclasses ────────────────────────
|
|
427
|
-
|
|
428
|
-
// Compute padding for axes. Subclasses may override / extend.
|
|
429
|
-
_axisPad(opts = {}) {
|
|
430
|
-
const t = this.theme;
|
|
431
|
-
return {
|
|
432
|
-
l: opts.l != null ? opts.l : 50,
|
|
433
|
-
r: opts.r != null ? opts.r : 16,
|
|
434
|
-
t: opts.t != null ? opts.t : 16,
|
|
435
|
-
b: opts.b != null ? opts.b : 32,
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Draw cartesian axes + grid. Returns the inner plot rect.
|
|
440
|
-
_drawCartesian(spec) {
|
|
441
|
-
const e = this.engine;
|
|
442
|
-
const t = this.theme;
|
|
443
|
-
const pad = this._axisPad(spec.pad);
|
|
444
|
-
const px = pad.l, py = pad.t;
|
|
445
|
-
const pw = e.w - pad.l - pad.r;
|
|
446
|
-
const ph = e.h - pad.t - pad.b;
|
|
447
|
-
|
|
448
|
-
const yMin = spec.yMin != null ? spec.yMin : 0;
|
|
449
|
-
const yMax = spec.yMax;
|
|
450
|
-
const yScale = spec.yScale || 'linear';
|
|
451
|
-
const font = `${t.fontSize}px ${t.font}`;
|
|
452
|
-
|
|
453
|
-
// Y ticks
|
|
454
|
-
let yTicks, yToPx;
|
|
455
|
-
if (yScale === 'log') {
|
|
456
|
-
const lo = Math.max(1e-10, spec.yMin || 1);
|
|
457
|
-
const hi = yMax;
|
|
458
|
-
const lLo = Math.log10(lo), lHi = Math.log10(hi);
|
|
459
|
-
yToPx = v => py + ph * (1 - (Math.log10(Math.max(lo, v)) - lLo) / (lHi - lLo));
|
|
460
|
-
yTicks = [];
|
|
461
|
-
for (let p = Math.floor(lLo); p <= Math.ceil(lHi); p++) {
|
|
462
|
-
const v = Math.pow(10, p);
|
|
463
|
-
if (v >= lo && v <= hi) yTicks.push(v);
|
|
464
|
-
}
|
|
465
|
-
} else {
|
|
466
|
-
yTicks = M.niceTicks(yMin, yMax, 5);
|
|
467
|
-
yToPx = v => py + ph * (1 - (v - yMin) / (yMax - yMin));
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Grid lines
|
|
471
|
-
e.ctx.strokeStyle = t.grid;
|
|
472
|
-
e.ctx.lineWidth = 1;
|
|
473
|
-
e.ctx.beginPath();
|
|
474
|
-
for (const v of yTicks) {
|
|
475
|
-
const y = Math.round(yToPx(v)) + 0.5;
|
|
476
|
-
e.ctx.moveTo(px, y);
|
|
477
|
-
e.ctx.lineTo(px + pw, y);
|
|
478
|
-
}
|
|
479
|
-
e.ctx.stroke();
|
|
480
|
-
|
|
481
|
-
// Y labels
|
|
482
|
-
const yFmt = spec.yTickFormat || (v => yScale === 'log' && v >= 1000 ? v.toExponential(0) : String(Math.round(v * 100) / 100));
|
|
483
|
-
for (const v of yTicks) {
|
|
484
|
-
e.text(yFmt(v), px - 6, yToPx(v), {
|
|
485
|
-
fill: t.muted, font, align: 'right', baseline: 'middle',
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// X axis. Subclasses pass either `xLabels: string[]` (categorical) or
|
|
490
|
-
// `xTime: {min, max}` (time scale) or `xLinear: {min, max}`.
|
|
491
|
-
let xToPx, xTicks, xFmt;
|
|
492
|
-
if (spec.xTime) {
|
|
493
|
-
const { min, max } = spec.xTime;
|
|
494
|
-
xToPx = v => px + pw * (v - min) / (max - min || 1);
|
|
495
|
-
xTicks = M.timeNiceTicks(min, max, 6);
|
|
496
|
-
const step = xTicks.length > 1 ? xTicks[1] - xTicks[0] : (max - min);
|
|
497
|
-
xFmt = spec.xTickFormat || M.timeFormat(step);
|
|
498
|
-
} else if (spec.xLinear) {
|
|
499
|
-
const { min, max } = spec.xLinear;
|
|
500
|
-
xToPx = v => px + pw * (v - min) / (max - min || 1);
|
|
501
|
-
xTicks = M.niceTicks(min, max, 6);
|
|
502
|
-
xFmt = spec.xTickFormat || (v => String(v));
|
|
503
|
-
} else if (spec.xLabels) {
|
|
504
|
-
const n = spec.xLabels.length;
|
|
505
|
-
xToPx = i => px + (i + 0.5) * (pw / n);
|
|
506
|
-
xTicks = spec.xLabels.map((_, i) => i);
|
|
507
|
-
xFmt = spec.xTickFormat || (i => String(spec.xLabels[i]));
|
|
508
|
-
}
|
|
509
|
-
if (xToPx) {
|
|
510
|
-
// X label collision avoidance.
|
|
511
|
-
let lastEnd = -Infinity;
|
|
512
|
-
for (const v of xTicks) {
|
|
513
|
-
const x = xToPx(v);
|
|
514
|
-
const txt = xFmt(v);
|
|
515
|
-
const w = e.measure(txt, font);
|
|
516
|
-
if (x - w / 2 < lastEnd + 4) continue;
|
|
517
|
-
lastEnd = x + w / 2;
|
|
518
|
-
e.text(txt, x, py + ph + 6, {
|
|
519
|
-
fill: t.muted, font, align: 'center', baseline: 'top',
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
// Axis line
|
|
525
|
-
e.poly([{x: px, y: py + ph}, {x: px + pw, y: py + ph}], {
|
|
526
|
-
stroke: t.axis, lineWidth: 1,
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
return { px, py, pw, ph, yToPx, xToPx, yTicks, xTicks, yMin, yMax };
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Draw a legend at the top. Returns consumed height for PAD.t adjustment.
|
|
533
|
-
_drawLegend(items, pw) {
|
|
534
|
-
const e = this.engine;
|
|
535
|
-
const t = this.theme;
|
|
536
|
-
const font = `${t.fontSize}px ${t.font}`;
|
|
537
|
-
const swatch = 10, gap = 16, rowH = 18;
|
|
538
|
-
let x = 0, y = 0, maxW = pw;
|
|
539
|
-
for (const it of items) {
|
|
540
|
-
const tw = e.measure(it.label, font);
|
|
541
|
-
const w = swatch + 6 + tw;
|
|
542
|
-
if (x + w > maxW && x > 0) { x = 0; y += rowH; }
|
|
543
|
-
e.rect(x, y + 4, swatch, swatch, { fill: it.hidden ? t.muted : it.color, radius: 2 });
|
|
544
|
-
e.text(it.label, x + swatch + 4, y + swatch / 2 + 4, {
|
|
545
|
-
fill: it.hidden ? t.muted : t.text, font, align: 'left', baseline: 'middle',
|
|
546
|
-
});
|
|
547
|
-
// Hit region for legend toggle
|
|
548
|
-
this.hits.add({
|
|
549
|
-
_id: 'legend-' + it.idx, x, y, w: swatch + 4 + tw, h: rowH,
|
|
550
|
-
legendIdx: it.idx,
|
|
551
|
-
});
|
|
552
|
-
x += w + gap;
|
|
553
|
-
}
|
|
554
|
-
return y + rowH + 6;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// Get color for series index, respecting per-series overrides.
|
|
558
|
-
_colorOf(idx, item) {
|
|
559
|
-
if (item && item.color) return item.color;
|
|
560
|
-
return this.palette[idx % this.palette.length];
|
|
561
|
-
}
|
|
562
|
-
}
|