@aeriz/chart 1.1.0 → 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/README.md +60 -34
- package/dist/aeriz.cjs +609 -354
- package/dist/aeriz.esm.js +578 -331
- package/dist/aeriz.esm.min.js +119 -105
- package/dist/aeriz.js +609 -354
- package/dist/aeriz.min.cjs +123 -107
- package/dist/aeriz.min.js +123 -107
- package/dist/esm/base.js +33 -22
- package/dist/esm/cartesian.js +7 -5
- package/dist/esm/charts/area.js +6 -6
- package/dist/esm/charts/bar.js +3 -3
- package/dist/esm/charts/boxplot.js +3 -3
- package/dist/esm/charts/bubble.js +3 -3
- package/dist/esm/charts/composite.js +8 -8
- package/dist/esm/charts/funnel.js +3 -3
- package/dist/esm/charts/gauge.js +5 -5
- package/dist/esm/charts/grouped-bar.js +4 -4
- package/dist/esm/charts/hbar.js +5 -5
- package/dist/esm/charts/heatmap.js +6 -6
- package/dist/esm/charts/line.js +6 -6
- package/dist/esm/charts/multi-line.js +5 -5
- package/dist/esm/charts/pie.js +6 -6
- package/dist/esm/charts/polar.js +3 -3
- package/dist/esm/charts/radar.js +6 -6
- package/dist/esm/charts/realtime.js +11 -9
- package/dist/esm/charts/scatter.js +4 -4
- package/dist/esm/charts/sparkline.js +4 -4
- package/dist/esm/charts/stacked-bar.js +5 -5
- package/dist/esm/charts/treemap.js +4 -4
- package/dist/esm/charts/waterfall.js +4 -4
- package/dist/esm/engine.js +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/jquery.js +5 -4
- package/dist/esm/math.js +1 -1
- package/dist/esm/react.js +4 -4
- package/dist/esm/shallow.js +4 -2
- package/dist/esm/svelte.js +4 -4
- package/dist/esm/vue.js +4 -2
- package/dist/index.d.ts +228 -552
- package/dist/jquery.d.ts +19 -8
- package/dist/react.d.ts +55 -18
- package/dist/svelte.d.ts +53 -14
- package/dist/types/sparkline.d.ts +1 -1
- package/dist/vue.d.ts +60 -10
- package/package.json +2 -2
package/dist/aeriz.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! @aeriz/chart
|
|
1
|
+
/*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
|
|
2
2
|
var Aeriz = (function () {
|
|
3
3
|
'use strict';
|
|
4
4
|
// ── src/math.js
|
|
@@ -543,17 +543,8 @@ const M = { TAU, clamp, lerp, extent, niceTicks, niceDomain, timeNiceTicks, time
|
|
|
543
543
|
// while some chart is animating or has something to redraw.
|
|
544
544
|
const charts = new Set();
|
|
545
545
|
let rafId = null;
|
|
546
|
-
let fps = 60;
|
|
547
|
-
let frames = 0;
|
|
548
|
-
let fpsT = 0;
|
|
549
546
|
|
|
550
547
|
function tick(now) {
|
|
551
|
-
if (now - fpsT > 500) {
|
|
552
|
-
fps = Math.round((1000 * frames) / (now - fpsT));
|
|
553
|
-
frames = 0;
|
|
554
|
-
fpsT = now;
|
|
555
|
-
}
|
|
556
|
-
frames++;
|
|
557
548
|
let active = false;
|
|
558
549
|
for (const c of charts) {
|
|
559
550
|
try {
|
|
@@ -566,22 +557,15 @@ function tick(now) {
|
|
|
566
557
|
function schedule(chart) {
|
|
567
558
|
if (chart._destroyed) return;
|
|
568
559
|
charts.add(chart);
|
|
569
|
-
if (rafId === null)
|
|
570
|
-
fpsT = performance.now();
|
|
571
|
-
rafId = requestAnimationFrame(tick);
|
|
572
|
-
}
|
|
560
|
+
if (rafId === null) rafId = requestAnimationFrame(tick);
|
|
573
561
|
}
|
|
574
562
|
|
|
575
|
-
const scheduler = { schedule, fps: () => fps, count: () => charts.size };
|
|
576
|
-
function getFPS() { return fps; }
|
|
577
|
-
function getActiveCount() { return charts.size; }
|
|
578
|
-
|
|
579
563
|
// Media queries are shared by every chart (one listener each, not per chart).
|
|
580
564
|
const mq = (q) => (typeof matchMedia === 'function' ? matchMedia(q) : null);
|
|
581
565
|
const reducedMotion = mq('(prefers-reduced-motion: reduce)');
|
|
582
566
|
const darkScheme = mq('(prefers-color-scheme: dark)');
|
|
583
567
|
// OS light/dark switch → every chart re-reads its theme.
|
|
584
|
-
darkScheme?.addEventListener?.('change', () => { for (const c of charts) c.
|
|
568
|
+
darkScheme?.addEventListener?.('change', () => { for (const c of charts) { c._theme(); schedule(c); } });
|
|
585
569
|
|
|
586
570
|
// ─── Theme resolution ───────────────────────────────────────
|
|
587
571
|
// Theme key → CSS variable: fontSize → --aeriz-font-size, tooltipBg → --aeriz-tooltip-bg …
|
|
@@ -733,28 +717,163 @@ class HitIndex {
|
|
|
733
717
|
}
|
|
734
718
|
}
|
|
735
719
|
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
//
|
|
720
|
+
// ─── Data: one accepted shape per kind ──────────────────────
|
|
721
|
+
// Single-series rows. All three give the same [{ label, value }] rows:
|
|
722
|
+
// [3, 7, 4] → labels 1, 2, 3
|
|
723
|
+
// { Jan: 3, Feb: 7 } → labels Jan, Feb
|
|
724
|
+
// [{ label: 'Jan', value: 3 }, …] (other fields: use the label/value options)
|
|
725
|
+
const isList = (d) => Array.isArray(d) || ArrayBuffer.isView(d);
|
|
726
|
+
const isObj = (d) => d != null && typeof d === 'object' && !isList(d);
|
|
727
|
+
|
|
740
728
|
function toRows(data) {
|
|
741
|
-
if (
|
|
742
|
-
|
|
743
|
-
return typeof data === '
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
729
|
+
if (isList(data)) {
|
|
730
|
+
if (!data.length || Array.isArray(data[0])) return [];
|
|
731
|
+
return typeof data[0] === 'number' ? Array.from(data, (v, i) => ({ label: String(i + 1), value: v })) : data;
|
|
732
|
+
}
|
|
733
|
+
return isObj(data) ? Object.keys(data).map((k) => ({ label: k, value: +data[k] })) : [];
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// ─── Developer warnings ─────────────────────────────────────
|
|
737
|
+
// Mistakes don't fail silently: the console says what is wrong and how to fix
|
|
738
|
+
// it, once per message.
|
|
739
|
+
const said = new Set();
|
|
740
|
+
function warn(msg) {
|
|
741
|
+
if (said.has(msg)) return;
|
|
742
|
+
said.add(msg);
|
|
743
|
+
console.warn('[aeriz] ' + msg);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Options every chart takes.
|
|
747
|
+
const COMMON = ['data', 'title', 'colors', 'format', 'legend', 'animate', 'emptyText', 'onClick', 'onLegend'];
|
|
748
|
+
|
|
749
|
+
const SHAPES = {
|
|
750
|
+
rows: 'an array of numbers, an object like { Jan: 64 }, or rows like [{ label, value }]',
|
|
751
|
+
series: '{ categories: [...], series: [{ name, values: [...] }] }',
|
|
752
|
+
stream: '{ series: [{ name }] } (values arrive with chart.push())',
|
|
753
|
+
points: 'points like [{ x, y }]',
|
|
754
|
+
matrix: '{ x: [...], y: [...], values: [[...], ...] } with values[y][x]',
|
|
755
|
+
boxes: 'rows like [{ label, min, q1, median, q3, max }]',
|
|
756
|
+
steps: 'rows like [{ label, value }] (add total: true for a total bar)',
|
|
757
|
+
number: 'a number',
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
// 1.x option names → what to write in 2.0.
|
|
761
|
+
const DATA = 'data: { categories, series }';
|
|
762
|
+
const RENAMED = {
|
|
763
|
+
ariaLabel: 'title', palette: 'colors', color: 'colors',
|
|
764
|
+
xTickFormat: 'labelFormat (category axis) or format (value axis)', yTickFormat: 'format',
|
|
765
|
+
yLeftTickFormat: 'format on the series', yRightTickFormat: 'format on the series',
|
|
766
|
+
categories: DATA, series: DATA, layers: DATA, axes: DATA,
|
|
767
|
+
xLabels: 'data: { x, y, values }', yLabels: 'data: { x, y, values }',
|
|
768
|
+
interpolation: 'curve', windowSize: 'window', yMin: 'min', yMax: 'max', waitingText: 'emptyText',
|
|
769
|
+
colorLow: 'colors: { low, high }', colorHigh: 'colors: { low, high }',
|
|
770
|
+
colorUp: 'colors: { up, down, total }', colorDown: 'colors: { up, down, total }', colorTotal: 'colors: { up, down, total }',
|
|
771
|
+
minRadius: 'radius: [min, max]', maxRadius: 'radius: [min, max]',
|
|
772
|
+
centerLabel: 'center', totalLabel: 'center', type: 'the chart class itself',
|
|
773
|
+
};
|
|
774
|
+
const FUNCS = ['label', 'value', 'labelFormat', 'onClick', 'onLegend'];
|
|
775
|
+
|
|
776
|
+
function distance(a, b) {
|
|
777
|
+
const d = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
778
|
+
for (let i = 1; i <= a.length; i++) {
|
|
779
|
+
let p = d[0];
|
|
780
|
+
d[0] = i;
|
|
781
|
+
for (let j = 1; j <= b.length; j++) {
|
|
782
|
+
const t = d[j];
|
|
783
|
+
d[j] = Math.min(d[j] + 1, d[j - 1] + 1, p + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
784
|
+
p = t;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return d[b.length];
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function checkOptions(meta, o) {
|
|
791
|
+
const name = meta.name;
|
|
792
|
+
const allowed = [...COMMON, ...meta.options];
|
|
793
|
+
for (const k of Object.keys(o)) {
|
|
794
|
+
const v = o[k];
|
|
795
|
+
if (!allowed.includes(k)) {
|
|
796
|
+
const hint = meta.renamed?.[k] || RENAMED[k];
|
|
797
|
+
if (hint) { warn(`${name}: "${k}" is a 1.x option. In 2.0 use ${hint}.`); continue; }
|
|
798
|
+
let best = null, bd = 3;
|
|
799
|
+
for (const a of allowed) {
|
|
800
|
+
const dd = a.toLowerCase() === k.toLowerCase() ? 0 : distance(k, a);
|
|
801
|
+
if (dd < bd) { bd = dd; best = a; }
|
|
802
|
+
}
|
|
803
|
+
warn(`${name}: unknown option "${k}"${best ? ` — did you mean "${best}"?` : `. Options: ${allowed.join(', ')}`}`);
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (v == null) continue;
|
|
807
|
+
if (FUNCS.includes(k) && typeof v !== 'function') warn(`${name}: "${k}" must be a function${k === 'label' || k === 'value' ? `, like ${k}: (row) => row.${k === 'label' ? 'name' : 'amount'}` : ''}.`);
|
|
808
|
+
const en = meta.enums?.[k];
|
|
809
|
+
if (en && !en.includes(v)) warn(`${name}: ${k} must be ${en.map((x) => `'${x}'`).join(' | ')}, got ${JSON.stringify(v)}.`);
|
|
810
|
+
if (k === 'format' && typeof v !== 'function' && !isObj(v)) warn(`${name}: format must be Intl.NumberFormat options like { style: 'percent' } or a function (v) => string.`);
|
|
811
|
+
if (k === 'colors' && (meta.colors ? !isObj(v) : !Array.isArray(v))) warn(`${name}: colors must be ${meta.colors || 'an array like [\'#64c8ff\', \'#10b981\']'}.`);
|
|
812
|
+
}
|
|
813
|
+
if ('data' in o) checkData(meta, o.data, o);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function checkData(meta, d, o) {
|
|
817
|
+
if (d == null) return;
|
|
818
|
+
const kind = meta.data;
|
|
819
|
+
const bad = (why) => warn(`${meta.name}: data must be ${SHAPES[kind]}${why ? ` — ${why}` : ''}.`);
|
|
820
|
+
const first = isList(d) ? d[0] : undefined;
|
|
821
|
+
switch (kind) {
|
|
822
|
+
case 'rows':
|
|
823
|
+
if (isList(d)) {
|
|
824
|
+
if (Array.isArray(first)) return bad(`pairs like ['Jan', 64] were removed in 2.0`);
|
|
825
|
+
if (first != null && typeof first !== 'number' && !isObj(first)) return bad(`got ${typeof first} items`);
|
|
826
|
+
if (isObj(first) && !o.value && !('value' in first)) warn(`${meta.name}: rows need a "value" field, or pass value: (row) => row.amount.`);
|
|
827
|
+
if (isObj(first) && !o.label && !('label' in first)) warn(`${meta.name}: rows need a "label" field, or pass label: (row) => row.name.`);
|
|
828
|
+
} else if (!isObj(d)) bad(`got ${typeof d}`);
|
|
829
|
+
return;
|
|
830
|
+
case 'series':
|
|
831
|
+
case 'stream': {
|
|
832
|
+
if (!isObj(d) || !Array.isArray(d.series) || (kind === 'series' && !isList(d.categories))) return bad(isList(d) ? 'got an array' : '');
|
|
833
|
+
const n = kind === 'series' ? d.categories.length : 0;
|
|
834
|
+
d.series.forEach((s, i) => {
|
|
835
|
+
const who = `series[${i}]${s?.name ? ` "${s.name}"` : ''}`;
|
|
836
|
+
if (kind === 'stream') return;
|
|
837
|
+
if (!s || !isList(s.values)) return warn(`${meta.name}: ${who} needs values: [...].`);
|
|
838
|
+
if (s.values.length !== n) warn(`${meta.name}: ${who} has ${s.values.length} values but there are ${n} categories.`);
|
|
839
|
+
});
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
case 'matrix':
|
|
843
|
+
if (!isObj(d) || !isList(d.x) || !isList(d.y) || !Array.isArray(d.values)) return bad(isList(d) ? 'got an array' : '');
|
|
844
|
+
if (d.values.length !== d.y.length) warn(`${meta.name}: values has ${d.values.length} rows but y has ${d.y.length} labels.`);
|
|
845
|
+
return;
|
|
846
|
+
case 'number':
|
|
847
|
+
if (typeof d !== 'number') bad(`got ${typeof d}`);
|
|
848
|
+
return;
|
|
849
|
+
default: {
|
|
850
|
+
// points · boxes · steps: arrays of objects with known fields
|
|
851
|
+
if (!Array.isArray(d)) return bad(`got ${isObj(d) ? 'an object' : typeof d}`);
|
|
852
|
+
if (!first) return;
|
|
853
|
+
const need = { points: ['x', 'y'], boxes: ['q1', 'median', 'q3'], steps: ['value'] }[kind];
|
|
854
|
+
const miss = need.filter((k) => !(k in first));
|
|
855
|
+
if (miss.length) bad(`the first item has no ${miss.join(', ')}`);
|
|
856
|
+
if (kind === 'steps' && 'isTotal' in first) warn(`${meta.name}: "isTotal" is 1.x. In 2.0 use total: true.`);
|
|
857
|
+
if (kind === 'points' && ('r' in first || 'z' in first)) warn(`${meta.name}: point "${'r' in first ? 'r' : 'z'}" is 1.x. In 2.0 use size.`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
749
860
|
}
|
|
750
861
|
|
|
751
862
|
// ─── Chart base ─────────────────────────────────────────────
|
|
863
|
+
const STATES = ['loading', 'empty', 'error'];
|
|
864
|
+
|
|
752
865
|
class Chart {
|
|
866
|
+
// Each chart type describes itself: name for messages, aria for the default
|
|
867
|
+
// accessible name, data shape, and the options it takes beyond COMMON.
|
|
868
|
+
static get meta() { return null; }
|
|
869
|
+
|
|
753
870
|
constructor(container, options = {}) {
|
|
871
|
+
const meta = this.constructor.meta;
|
|
754
872
|
const el = typeof container === 'string' ? document.querySelector(container) : container;
|
|
755
|
-
if (!el) throw new Error(`aeriz: container not found${typeof container === 'string' ? ': ' + container : ''}`);
|
|
873
|
+
if (!el) throw new Error(`aeriz: ${meta ? meta.name + ': ' : ''}container not found${typeof container === 'string' ? ': ' + container : ''}`);
|
|
756
874
|
this.container = el;
|
|
757
|
-
this.options = options;
|
|
875
|
+
this.options = { ...options };
|
|
876
|
+
if (meta) checkOptions(meta, this.options);
|
|
758
877
|
|
|
759
878
|
// Remember what we touch on the container so destroy() can undo it.
|
|
760
879
|
this._restore = { position: el.style.position, height: el.style.height };
|
|
@@ -782,8 +901,6 @@ class Chart {
|
|
|
782
901
|
this.overlay = new Overlay(this.plot);
|
|
783
902
|
this.hits = new HitIndex();
|
|
784
903
|
|
|
785
|
-
this.theme = resolveTheme(el);
|
|
786
|
-
this.palette = options.palette || readPalette(el);
|
|
787
904
|
this._off = new Set(); // legend-hidden series / item indices
|
|
788
905
|
this._legendKey = '';
|
|
789
906
|
this._cursor = null;
|
|
@@ -796,6 +913,7 @@ class Chart {
|
|
|
796
913
|
this._anim = { progress: 0, start: 0, duration: 600 };
|
|
797
914
|
this._state = null; // 'loading' | 'empty' | 'error' | null
|
|
798
915
|
this._auto = false; // _state was set automatically by "No data"
|
|
916
|
+
this._theme();
|
|
799
917
|
this._label();
|
|
800
918
|
|
|
801
919
|
this._setupEvents();
|
|
@@ -806,34 +924,44 @@ class Chart {
|
|
|
806
924
|
schedule(this);
|
|
807
925
|
}
|
|
808
926
|
|
|
809
|
-
// ─── Public API
|
|
810
|
-
//
|
|
811
|
-
invalidate() {
|
|
812
|
-
this.theme = resolveTheme(this.container);
|
|
813
|
-
this.palette = this.options.palette || readPalette(this.container);
|
|
814
|
-
this.engine._gradCache.clear();
|
|
815
|
-
this._legendKey = '';
|
|
816
|
-
this._dirty = true;
|
|
817
|
-
schedule(this);
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
// Merge new options (any option, including data) and redraw with animation.
|
|
927
|
+
// ─── Public API: update · setState · render · destroy ────
|
|
928
|
+
// Change any options (data included). Changed values animate in.
|
|
821
929
|
update(patch = {}) {
|
|
930
|
+
const meta = this.constructor.meta;
|
|
931
|
+
if (meta) checkOptions(meta, patch);
|
|
822
932
|
Object.assign(this.options, patch);
|
|
823
933
|
this._cache = null; // charts may cache work per data array; data can be mutated in place
|
|
824
|
-
if ('
|
|
934
|
+
if ('colors' in patch) this._theme();
|
|
825
935
|
if (this._auto) { this._state = null; this._auto = false; }
|
|
826
936
|
this._label();
|
|
827
937
|
this._restart();
|
|
828
938
|
return this;
|
|
829
939
|
}
|
|
830
940
|
|
|
831
|
-
|
|
941
|
+
// Show a loading / empty / error overlay, or remove it with null.
|
|
942
|
+
setState(state, text) {
|
|
943
|
+
if (state != null && !STATES.includes(state)) {
|
|
944
|
+
throw new Error(`aeriz: setState() takes 'loading', 'empty', 'error' or null — got ${JSON.stringify(state)}`);
|
|
945
|
+
}
|
|
946
|
+
this._state = state || null;
|
|
947
|
+
this._stateMsg = text ?? (state === 'empty' ? this.options.emptyText || 'No data' : state === 'error' ? 'Error' : '');
|
|
948
|
+
this._auto = false;
|
|
949
|
+
this._dirty = true;
|
|
950
|
+
schedule(this);
|
|
951
|
+
return this;
|
|
952
|
+
}
|
|
832
953
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
954
|
+
// Re-read CSS variables / theme and draw the final frame right now — also
|
|
955
|
+
// when the chart is off-screen (printing, export, measuring).
|
|
956
|
+
render() {
|
|
957
|
+
if (this._destroyed) return this;
|
|
958
|
+
this._theme();
|
|
959
|
+
this._anim.progress = 1;
|
|
960
|
+
this._resize();
|
|
961
|
+
this._dirty = false;
|
|
962
|
+
this._draw();
|
|
963
|
+
return this;
|
|
964
|
+
}
|
|
837
965
|
|
|
838
966
|
destroy() {
|
|
839
967
|
if (this._destroyed) return;
|
|
@@ -850,17 +978,17 @@ class Chart {
|
|
|
850
978
|
}
|
|
851
979
|
|
|
852
980
|
// ─── Internals ───────────────────────────────────────────
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
this.
|
|
856
|
-
this.
|
|
981
|
+
_theme() {
|
|
982
|
+
const o = this.options;
|
|
983
|
+
this.theme = resolveTheme(this.container);
|
|
984
|
+
this.palette = Array.isArray(o.colors) && o.colors.length ? o.colors : readPalette(this.container);
|
|
985
|
+
this.engine._gradCache.clear();
|
|
986
|
+
this._legendKey = '';
|
|
857
987
|
this._dirty = true;
|
|
858
|
-
schedule(this);
|
|
859
|
-
return this;
|
|
860
988
|
}
|
|
861
989
|
|
|
862
|
-
// Charts call this when they find nothing to draw. Unlike
|
|
863
|
-
// clears itself on the next update()
|
|
990
|
+
// Charts call this when they find nothing to draw. Unlike setState('empty')
|
|
991
|
+
// it clears itself on the next update().
|
|
864
992
|
_empty(msg = this.options.emptyText || 'No data') {
|
|
865
993
|
this._state = 'empty';
|
|
866
994
|
this._stateMsg = msg;
|
|
@@ -877,13 +1005,41 @@ class Chart {
|
|
|
877
1005
|
}
|
|
878
1006
|
|
|
879
1007
|
_label() {
|
|
880
|
-
|
|
881
|
-
this.canvas.setAttribute('aria-label', o.ariaLabel || o.title || this._kind || 'Chart');
|
|
1008
|
+
this.canvas.setAttribute('aria-label', this.options.title || this.constructor.meta?.aria || 'Chart');
|
|
882
1009
|
}
|
|
883
1010
|
|
|
884
1011
|
_fmt(v) { return M.formatNumber(v, this.options.format); }
|
|
885
1012
|
_tickFmt(v) { return M.formatNumber(v, this.options.format, true); }
|
|
1013
|
+
// Category / time label as text (labelFormat wins).
|
|
1014
|
+
_lab(l, time) {
|
|
1015
|
+
const f = this._fn('labelFormat');
|
|
1016
|
+
return f ? String(f(l)) : time ? new Date(+l).toLocaleString() : String(l);
|
|
1017
|
+
}
|
|
1018
|
+
// Numeric x value (scatter, bubble) as text.
|
|
1019
|
+
_xFmt(v) {
|
|
1020
|
+
const f = this._fn('labelFormat');
|
|
1021
|
+
return f ? String(f(v)) : this._fmt(v);
|
|
1022
|
+
}
|
|
886
1023
|
_isOff(i) { return this._off.has(i); }
|
|
1024
|
+
// label / value accessor, when it is a function (otherwise warned about).
|
|
1025
|
+
_fn(k) {
|
|
1026
|
+
const f = this.options[k];
|
|
1027
|
+
return typeof f === 'function' ? f : null;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// What onClick receives: the clicked item, without drawing internals.
|
|
1031
|
+
_hit(h) {
|
|
1032
|
+
if (!h) return null;
|
|
1033
|
+
const out = { index: h.index, label: h.label };
|
|
1034
|
+
if ('value' in h) out.value = h.value;
|
|
1035
|
+
if ('series' in h) out.series = h.series;
|
|
1036
|
+
if ('row' in h) out.row = h.row;
|
|
1037
|
+
return out;
|
|
1038
|
+
}
|
|
1039
|
+
_click(h) {
|
|
1040
|
+
const hit = this._hit(h);
|
|
1041
|
+
if (hit) this.options.onClick?.(hit, this);
|
|
1042
|
+
}
|
|
887
1043
|
|
|
888
1044
|
_resize() {
|
|
889
1045
|
const w = this.plot.clientWidth;
|
|
@@ -983,8 +1139,7 @@ class Chart {
|
|
|
983
1139
|
|
|
984
1140
|
_onClick(e) {
|
|
985
1141
|
const p = this._ptr(e);
|
|
986
|
-
|
|
987
|
-
this.options.onClick?.(hit, p, this);
|
|
1142
|
+
this._click(this.hits.find(p.x, p.y) || (this._follow ? this._hover : null));
|
|
988
1143
|
}
|
|
989
1144
|
|
|
990
1145
|
// ← → (↑ ↓) walk through the data points, Home/End jump, Enter clicks,
|
|
@@ -994,7 +1149,7 @@ class Chart {
|
|
|
994
1149
|
const fwd = k === 'ArrowRight' || k === 'ArrowDown';
|
|
995
1150
|
const back = k === 'ArrowLeft' || k === 'ArrowUp';
|
|
996
1151
|
if (k === 'Escape') return this._onLeave();
|
|
997
|
-
if (k === 'Enter' && this._hover) return this.
|
|
1152
|
+
if (k === 'Enter' && this._hover) return this._click(this._hover);
|
|
998
1153
|
if (!fwd && !back && k !== 'Home' && k !== 'End') return;
|
|
999
1154
|
const steps = this._steps?.length
|
|
1000
1155
|
? this._steps.map((x) => ({ x, y: this.engine.h / 2 }))
|
|
@@ -1101,6 +1256,15 @@ class Chart {
|
|
|
1101
1256
|
}
|
|
1102
1257
|
}
|
|
1103
1258
|
|
|
1259
|
+
// Removed in 2.0 — fail loudly with the replacement instead of "is not a function".
|
|
1260
|
+
const GONE = {
|
|
1261
|
+
setData: 'update({ data })', setLoading: "setState('loading', text)", setEmpty: "setState('empty', text)",
|
|
1262
|
+
setError: "setState('error', text)", clearState: 'setState(null)', invalidate: 'render()', setValue: 'update({ data: value })',
|
|
1263
|
+
};
|
|
1264
|
+
for (const k in GONE) {
|
|
1265
|
+
Chart.prototype[k] = function () { throw new Error(`aeriz 2.0: chart.${k}() was removed. Use chart.${GONE[k]}.`); };
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1104
1268
|
// ── src/cartesian.js
|
|
1105
1269
|
// cartesian.js — Axes + grid for x/y charts. Only charts with axes import it.
|
|
1106
1270
|
|
|
@@ -1125,9 +1289,10 @@ function localTimeFormat(step) {
|
|
|
1125
1289
|
|
|
1126
1290
|
// Draw grid, y axis (left, optional right) and x axis. Returns plot geometry.
|
|
1127
1291
|
//
|
|
1128
|
-
// spec: { yMin, yMax, yScale?: 'linear'|'log',
|
|
1129
|
-
// xLabels?:
|
|
1130
|
-
// right?: { yMin, yMax,
|
|
1292
|
+
// spec: { yMin, yMax, yScale?: 'linear'|'log', yFormat?,
|
|
1293
|
+
// xLabels?: any[] | xTime?: {min,max} | xLinear?: {min,max},
|
|
1294
|
+
// right?: { yMin, yMax, yFormat? }, pad?: {l,r,t,b} }
|
|
1295
|
+
// The value axis uses the chart's `format`; x labels use its `labelFormat`.
|
|
1131
1296
|
function drawCartesian(chart, spec) {
|
|
1132
1297
|
const e = chart.engine;
|
|
1133
1298
|
const t = chart.theme;
|
|
@@ -1151,7 +1316,7 @@ function drawCartesian(chart, spec) {
|
|
|
1151
1316
|
yTicks = M.niceTicks(yMin, yMax, 5);
|
|
1152
1317
|
yNorm = (v) => (v - yMin) / (yMax - yMin || 1);
|
|
1153
1318
|
}
|
|
1154
|
-
const yFmt = spec.
|
|
1319
|
+
const yFmt = spec.yFormat ? (v) => M.formatNumber(v, spec.yFormat, true) : (v) => chart._tickFmt(v);
|
|
1155
1320
|
const yText = yTicks.map((v) => yFmt(v));
|
|
1156
1321
|
let maxW = 0;
|
|
1157
1322
|
for (const s of yText) maxW = Math.max(maxW, e.measure(s, font));
|
|
@@ -1161,7 +1326,7 @@ function drawCartesian(chart, spec) {
|
|
|
1161
1326
|
if (right) {
|
|
1162
1327
|
rTicks = M.niceTicks(right.yMin, right.yMax, 5);
|
|
1163
1328
|
rNorm = (v) => (v - right.yMin) / (right.yMax - right.yMin || 1);
|
|
1164
|
-
const rf = right.
|
|
1329
|
+
const rf = right.yFormat ? (v) => M.formatNumber(v, right.yFormat, true) : (v) => chart._tickFmt(v);
|
|
1165
1330
|
rText = rTicks.map((v) => rf(v));
|
|
1166
1331
|
for (const s of rText) rMaxW = Math.max(rMaxW, e.measure(s, font));
|
|
1167
1332
|
}
|
|
@@ -1196,23 +1361,24 @@ function drawCartesian(chart, spec) {
|
|
|
1196
1361
|
|
|
1197
1362
|
// X axis: categorical labels, time, or linear.
|
|
1198
1363
|
let xToPx, xTicks, xFmt;
|
|
1364
|
+
const lf = chart._fn('labelFormat');
|
|
1199
1365
|
if (spec.xTime) {
|
|
1200
1366
|
const { min, max } = spec.xTime;
|
|
1201
1367
|
xToPx = (v) => px + (pw * (v - min)) / (max - min || 1);
|
|
1202
1368
|
xTicks = M.timeNiceTicks(min, max, Math.max(2, Math.floor(pw / 90)));
|
|
1203
1369
|
const step = xTicks.length > 1 ? xTicks[1] - xTicks[0] : max - min;
|
|
1204
|
-
xFmt =
|
|
1370
|
+
xFmt = lf ? (v) => String(lf(v)) : localTimeFormat(step);
|
|
1205
1371
|
} else if (spec.xLinear) {
|
|
1206
1372
|
const { min, max } = spec.xLinear;
|
|
1207
1373
|
xToPx = (v) => px + (pw * (v - min)) / (max - min || 1);
|
|
1208
1374
|
xTicks = M.niceTicks(min, max, 6);
|
|
1209
|
-
xFmt =
|
|
1375
|
+
xFmt = lf ? (v) => String(lf(v)) : (v) => chart._tickFmt(v);
|
|
1210
1376
|
} else if (spec.xLabels) {
|
|
1211
1377
|
const n = spec.xLabels.length || 1;
|
|
1212
1378
|
xToPx = (i) => px + (i + 0.5) * (pw / n);
|
|
1213
|
-
xFmt =
|
|
1379
|
+
xFmt = (i) => chart._lab(spec.xLabels[i]);
|
|
1214
1380
|
// Only look at every k-th label: with 1M categories we can't measure them all.
|
|
1215
|
-
const guess = Math.max(e.measure(xFmt(0
|
|
1381
|
+
const guess = Math.max(e.measure(xFmt(0), font), e.measure(xFmt(n - 1), font)) + 6;
|
|
1216
1382
|
const k = Math.max(1, Math.ceil((n * guess) / pw));
|
|
1217
1383
|
xTicks = [];
|
|
1218
1384
|
for (let i = 0; i < n; i += k) xTicks.push(i);
|
|
@@ -1221,7 +1387,7 @@ function drawCartesian(chart, spec) {
|
|
|
1221
1387
|
// Skip labels that would collide; keep the outer ones inside the canvas.
|
|
1222
1388
|
let lastEnd = -Infinity;
|
|
1223
1389
|
for (const v of xTicks) {
|
|
1224
|
-
const txt = xFmt(v
|
|
1390
|
+
const txt = xFmt(v);
|
|
1225
1391
|
const w = e.measure(txt, font);
|
|
1226
1392
|
const x = Math.max(w / 2, Math.min(e.w - w / 2, xToPx(v)));
|
|
1227
1393
|
if (x - w / 2 < lastEnd + 6) continue;
|
|
@@ -1249,13 +1415,24 @@ function range(arrays, lo = Infinity, hi = -Infinity) {
|
|
|
1249
1415
|
return [lo, hi];
|
|
1250
1416
|
}
|
|
1251
1417
|
|
|
1418
|
+
// Value-axis domain from the data range, with the chart's min / max options
|
|
1419
|
+
// taking precedence over the nice rounding.
|
|
1420
|
+
function valueDomain(o, lo, hi) {
|
|
1421
|
+
const [a, b] = M.niceDomain(o.min ?? lo, o.max ?? hi, 5);
|
|
1422
|
+
return [o.min ?? a, o.max ?? b];
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Option names shared by charts with x/y axes.
|
|
1426
|
+
const AXIS = ['labelFormat', 'min', 'max'];
|
|
1427
|
+
|
|
1252
1428
|
// ── src/charts/bar.js
|
|
1253
1429
|
// BarChart — vertical bars, negative values grow downward from zero.
|
|
1254
1430
|
|
|
1255
1431
|
|
|
1256
|
-
|
|
1257
1432
|
class BarChart extends Chart {
|
|
1258
|
-
get
|
|
1433
|
+
static get meta() {
|
|
1434
|
+
return { id: 'bar', name: 'BarChart', aria: 'Bar chart', data: 'rows', options: ['label', 'value', 'labelFormat', 'min', 'max', 'dataLabels'] };
|
|
1435
|
+
}
|
|
1259
1436
|
|
|
1260
1437
|
_render() {
|
|
1261
1438
|
const data = toRows(this.options.data);
|
|
@@ -1263,25 +1440,24 @@ class BarChart extends Chart {
|
|
|
1263
1440
|
const e = this.engine;
|
|
1264
1441
|
const t = this.theme;
|
|
1265
1442
|
const o = this.options;
|
|
1266
|
-
const labelOf =
|
|
1267
|
-
const valueOf =
|
|
1443
|
+
const labelOf = this._fn('label') || ((d) => d.label);
|
|
1444
|
+
const valueOf = this._fn('value') || ((d) => d.value);
|
|
1268
1445
|
const values = data.map((d, i) => +valueOf(d, i) || 0);
|
|
1446
|
+
const labels = data.map((d, i) => labelOf(d, i));
|
|
1269
1447
|
const [lo, hi] = range([values], 0, 0);
|
|
1270
|
-
const [yLo, yHi] =
|
|
1448
|
+
const [yLo, yHi] = valueDomain(o, lo, hi);
|
|
1449
|
+
const dl = o.dataLabels;
|
|
1271
1450
|
const ax = drawCartesian(this, {
|
|
1272
1451
|
yMin: yLo, yMax: yHi,
|
|
1273
|
-
xLabels:
|
|
1274
|
-
|
|
1275
|
-
xTickFormat: o.xTickFormat,
|
|
1276
|
-
pad: o.dataLabels ? { t: t.fontSize + 10 } : undefined,
|
|
1452
|
+
xLabels: labels,
|
|
1453
|
+
pad: dl ? { t: t.fontSize + 10 } : undefined,
|
|
1277
1454
|
});
|
|
1278
1455
|
|
|
1279
1456
|
const slot = ax.pw / data.length;
|
|
1280
1457
|
const barW = Math.max(1, Math.min(60, slot * 0.8, slot - 2));
|
|
1281
1458
|
const prog = this._anim.progress;
|
|
1282
|
-
const y0 = ax.yToPx(0);
|
|
1459
|
+
const y0 = ax.yToPx(Math.max(yLo, 0));
|
|
1283
1460
|
const groups = new Map();
|
|
1284
|
-
const dl = o.dataLabels;
|
|
1285
1461
|
const font = `${t.fontSize}px ${t.font}`;
|
|
1286
1462
|
|
|
1287
1463
|
for (let i = 0; i < data.length; i++) {
|
|
@@ -1289,17 +1465,18 @@ class BarChart extends Chart {
|
|
|
1289
1465
|
const x = ax.px + (i + 0.5) * slot - barW / 2;
|
|
1290
1466
|
const h = Math.abs(ax.yToPx(v) - y0) * prog;
|
|
1291
1467
|
const top = v >= 0 ? y0 - h : y0;
|
|
1292
|
-
const color = this._colorOf(
|
|
1468
|
+
const color = this._colorOf(i, data[i]);
|
|
1293
1469
|
if (!groups.has(color)) groups.set(color, []);
|
|
1294
1470
|
groups.get(color).push({ x, y: top, w: barW, h, radius: t.barRadius });
|
|
1471
|
+
const label = this._lab(labels[i]);
|
|
1295
1472
|
this.hits.add({
|
|
1296
1473
|
_id: i, x, y: Math.min(top, y0 - 2), w: barW, h: Math.max(h, 4),
|
|
1297
1474
|
tipX: x + barW / 2, tipY: top,
|
|
1298
|
-
tooltip: `${
|
|
1299
|
-
|
|
1475
|
+
tooltip: `${label}: ${this._fmt(v)}`,
|
|
1476
|
+
index: i, label: labels[i], value: v, row: data[i],
|
|
1300
1477
|
});
|
|
1301
1478
|
if (dl && prog > 0.95) {
|
|
1302
|
-
const txt = typeof dl === 'function' ? dl(data[i], i) : this._fmt(v);
|
|
1479
|
+
const txt = typeof dl === 'function' ? dl(v, data[i], i) : this._fmt(v);
|
|
1303
1480
|
e.text(txt, x + barW / 2, v >= 0 ? top - 4 : top + h + 4, {
|
|
1304
1481
|
fill: t.text, font, align: 'center', baseline: v >= 0 ? 'bottom' : 'top',
|
|
1305
1482
|
});
|
|
@@ -1320,7 +1497,9 @@ class BarChart extends Chart {
|
|
|
1320
1497
|
|
|
1321
1498
|
|
|
1322
1499
|
class HBarChart extends Chart {
|
|
1323
|
-
get
|
|
1500
|
+
static get meta() {
|
|
1501
|
+
return { id: 'hbar', name: 'HBarChart', aria: 'Horizontal bar chart', data: 'rows', options: ['label', 'value', 'labelFormat', 'min', 'max'] };
|
|
1502
|
+
}
|
|
1324
1503
|
|
|
1325
1504
|
_render() {
|
|
1326
1505
|
const data = toRows(this.options.data);
|
|
@@ -1328,12 +1507,13 @@ class HBarChart extends Chart {
|
|
|
1328
1507
|
const e = this.engine;
|
|
1329
1508
|
const t = this.theme;
|
|
1330
1509
|
const o = this.options;
|
|
1331
|
-
const labelOf =
|
|
1332
|
-
const valueOf =
|
|
1510
|
+
const labelOf = this._fn('label') || ((d) => d.label);
|
|
1511
|
+
const valueOf = this._fn('value') || ((d) => d.value);
|
|
1333
1512
|
const values = data.map((d, i) => +valueOf(d, i) || 0);
|
|
1334
|
-
const
|
|
1513
|
+
const raw = data.map((d, i) => labelOf(d, i));
|
|
1514
|
+
const labels = raw.map((l) => this._lab(l));
|
|
1335
1515
|
const [lo, hi] = range([values], 0, 0);
|
|
1336
|
-
const [xLo, xHi] =
|
|
1516
|
+
const [xLo, xHi] = valueDomain(o, lo, hi);
|
|
1337
1517
|
const font = `${t.fontSize}px ${t.font}`;
|
|
1338
1518
|
|
|
1339
1519
|
let labelW = 0;
|
|
@@ -1344,7 +1524,6 @@ class HBarChart extends Chart {
|
|
|
1344
1524
|
|
|
1345
1525
|
const xTicks = M.niceTicks(xLo, xHi, Math.max(2, Math.min(6, Math.floor(pw / 70))));
|
|
1346
1526
|
const xToPx = (v) => px + (pw * (v - xLo)) / (xHi - xLo || 1);
|
|
1347
|
-
const xFmt = o.xTickFormat || ((v) => this._tickFmt(v));
|
|
1348
1527
|
e.ctx.strokeStyle = t.grid;
|
|
1349
1528
|
e.ctx.lineWidth = 1;
|
|
1350
1529
|
e.ctx.beginPath();
|
|
@@ -1355,14 +1534,14 @@ class HBarChart extends Chart {
|
|
|
1355
1534
|
}
|
|
1356
1535
|
e.ctx.stroke();
|
|
1357
1536
|
for (const v of xTicks) {
|
|
1358
|
-
e.text(
|
|
1537
|
+
e.text(this._tickFmt(v), xToPx(v), py + ph + 6, { fill: t.muted, font, align: 'center', baseline: 'top' });
|
|
1359
1538
|
}
|
|
1360
1539
|
e.poly([{ x: px, y: py + ph }, { x: px + pw, y: py + ph }], { stroke: t.axis, lineWidth: 1 });
|
|
1361
1540
|
|
|
1362
1541
|
const slot = ph / data.length;
|
|
1363
1542
|
const barH = Math.max(1, Math.min(40, slot * 0.75, slot - 2));
|
|
1364
1543
|
const prog = this._anim.progress;
|
|
1365
|
-
const x0 = xToPx(0);
|
|
1544
|
+
const x0 = xToPx(Math.max(xLo, 0));
|
|
1366
1545
|
const groups = new Map();
|
|
1367
1546
|
|
|
1368
1547
|
for (let i = 0; i < data.length; i++) {
|
|
@@ -1371,14 +1550,14 @@ class HBarChart extends Chart {
|
|
|
1371
1550
|
const y = yMid - barH / 2;
|
|
1372
1551
|
const w = Math.abs(xToPx(v) - x0) * prog;
|
|
1373
1552
|
const left = v >= 0 ? x0 : x0 - w;
|
|
1374
|
-
const color = this._colorOf(
|
|
1553
|
+
const color = this._colorOf(i, data[i]);
|
|
1375
1554
|
if (!groups.has(color)) groups.set(color, []);
|
|
1376
1555
|
groups.get(color).push({ x: left, y, w, h: barH, radius: t.barRadius });
|
|
1377
1556
|
this.hits.add({
|
|
1378
1557
|
_id: i, x: Math.min(left, x0 - 2), y, w: Math.max(w, 4), h: barH,
|
|
1379
1558
|
tipX: left + w / 2, tipY: y,
|
|
1380
1559
|
tooltip: `${labels[i]}: ${this._fmt(v)}`,
|
|
1381
|
-
|
|
1560
|
+
index: i, label: raw[i], value: v, row: data[i],
|
|
1382
1561
|
});
|
|
1383
1562
|
e.text(labels[i], px - 6, yMid, { fill: t.muted, font, align: 'right', baseline: 'middle' });
|
|
1384
1563
|
}
|
|
@@ -1400,42 +1579,50 @@ const LABEL = (d) => d.label;
|
|
|
1400
1579
|
const VALUE = (d) => d.value;
|
|
1401
1580
|
|
|
1402
1581
|
class LineChart extends Chart {
|
|
1403
|
-
get
|
|
1582
|
+
static get meta() {
|
|
1583
|
+
return {
|
|
1584
|
+
id: 'line', name: 'LineChart', aria: 'Line chart', data: 'rows',
|
|
1585
|
+
options: ['label', 'value', 'labelFormat', 'min', 'max', 'curve', 'fill', 'points', 'xScale', 'yScale'],
|
|
1586
|
+
enums: { curve: ['smooth', 'linear', 'step'], xScale: ['category', 'time'], yScale: ['linear', 'log'] },
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1404
1589
|
|
|
1405
1590
|
_render() {
|
|
1406
|
-
const data = toRows(this.options.data);
|
|
1407
|
-
if (!data.length) return this._empty();
|
|
1408
|
-
const e = this.engine;
|
|
1409
|
-
const t = this.theme;
|
|
1410
1591
|
const o = this.options;
|
|
1411
|
-
const labelOf =
|
|
1412
|
-
const valueOf =
|
|
1413
|
-
const n = data.length;
|
|
1592
|
+
const labelOf = this._fn('label') || LABEL;
|
|
1593
|
+
const valueOf = this._fn('value') || VALUE;
|
|
1414
1594
|
|
|
1415
|
-
//
|
|
1595
|
+
// Rows and values are cached per data array so hover redraws don't
|
|
1596
|
+
// re-map 1M rows.
|
|
1416
1597
|
let c = this._cache;
|
|
1417
1598
|
if (!c || c.data !== o.data || c.valueOf !== valueOf || c.labelOf !== labelOf) {
|
|
1418
|
-
const
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1599
|
+
const d = o.data;
|
|
1600
|
+
// Plain numbers (incl. typed arrays): skip building a row object per point.
|
|
1601
|
+
const nums = d && typeof d[0] === 'number' && (Array.isArray(d) || ArrayBuffer.isView(d)) && !this._fn('value') && !this._fn('label');
|
|
1602
|
+
const rows = nums ? d : toRows(d);
|
|
1603
|
+
const values = nums ? Float64Array.from(d) : new Float64Array(rows.length);
|
|
1604
|
+
if (!nums) for (let i = 0; i < rows.length; i++) values[i] = +valueOf(rows[i], i);
|
|
1605
|
+
const labels = nums ? Array.from({ length: d.length }, (_, i) => i + 1) : null;
|
|
1606
|
+
c = this._cache = { data: o.data, rows, valueOf, labelOf, values, idx: null, idxW: 0, xs: null, labels, range: null };
|
|
1607
|
+
}
|
|
1608
|
+
const data = c.rows;
|
|
1609
|
+
if (!data.length) return this._empty();
|
|
1610
|
+
const e = this.engine;
|
|
1611
|
+
const t = this.theme;
|
|
1612
|
+
const n = data.length;
|
|
1422
1613
|
const values = c.values;
|
|
1423
|
-
const
|
|
1424
|
-
const [
|
|
1614
|
+
const log = o.yScale === 'log';
|
|
1615
|
+
const [lo, hi] = (c.range ||= range([values], log ? Infinity : 0));
|
|
1616
|
+
const [yLo, yHi] = log ? [o.min ?? lo, o.max ?? hi] : valueDomain(o, lo, hi);
|
|
1425
1617
|
const useTime = o.xScale === 'time';
|
|
1426
|
-
const spec = {
|
|
1427
|
-
|
|
1428
|
-
yScale: o.yScale,
|
|
1429
|
-
yTickFormat: o.yTickFormat,
|
|
1430
|
-
xTickFormat: o.xTickFormat,
|
|
1431
|
-
};
|
|
1432
|
-
const xs = useTime ? (c.xs ||= data.map((d, i) => +labelOf(d, i))) : null;
|
|
1618
|
+
const spec = { yMin: yLo, yMax: yHi, yScale: o.yScale };
|
|
1619
|
+
const xs = useTime ? (c.xs ||= Array.from(data, (d, i) => +labelOf(d, i))) : null;
|
|
1433
1620
|
if (useTime) spec.xTime = { min: xs[0], max: xs[n - 1] };
|
|
1434
|
-
else spec.xLabels = c.labels ||=
|
|
1621
|
+
else spec.xLabels = c.labels ||= Array.from(data, (d, i) => labelOf(d, i));
|
|
1435
1622
|
const ax = drawCartesian(this, spec);
|
|
1436
1623
|
|
|
1437
|
-
const color =
|
|
1438
|
-
const interp = o.
|
|
1624
|
+
const color = this.palette[0];
|
|
1625
|
+
const interp = o.curve || 'smooth';
|
|
1439
1626
|
const lineW = t.lineWidth;
|
|
1440
1627
|
const prog = this._anim.progress;
|
|
1441
1628
|
const xAt = useTime ? (i) => ax.xToPx(xs[i]) : (i) => ax.px + ((i + 0.5) / n) * ax.pw;
|
|
@@ -1470,19 +1657,20 @@ class LineChart extends Chart {
|
|
|
1470
1657
|
}
|
|
1471
1658
|
if (cps) e.smooth(points, cps, { stroke: color, lineWidth: lineW });
|
|
1472
1659
|
else e.poly(points, { stroke: color, lineWidth: lineW, cap: 'round', join: 'round' });
|
|
1473
|
-
} else
|
|
1474
|
-
e.step(points,
|
|
1660
|
+
} else {
|
|
1661
|
+
e.step(points, 'before', { stroke: color, lineWidth: lineW });
|
|
1475
1662
|
}
|
|
1476
1663
|
e.unclip();
|
|
1477
1664
|
|
|
1478
|
-
const
|
|
1665
|
+
const labelAt = (i) => (useTime ? xs[i] : c.labels[i]);
|
|
1666
|
+
const tip = (i) => `${this._lab(labelAt(i), useTime)}: ${this._fmt(values[i])}`;
|
|
1479
1667
|
if (!decimated && n <= 50) {
|
|
1480
1668
|
if (o.points !== false) e.circles(points, color, t.pointRadius);
|
|
1481
1669
|
for (const p of points) {
|
|
1482
1670
|
this.hits.add({
|
|
1483
1671
|
_id: p.i, x: p.x - 8, y: p.y - 8, w: 16, h: 16,
|
|
1484
1672
|
tipX: p.x, tipY: p.y - t.pointRadius,
|
|
1485
|
-
tooltip: tip(p.i),
|
|
1673
|
+
tooltip: tip(p.i), index: p.i, label: labelAt(p.i), value: values[p.i], row: data[p.i],
|
|
1486
1674
|
});
|
|
1487
1675
|
}
|
|
1488
1676
|
} else {
|
|
@@ -1495,7 +1683,7 @@ class LineChart extends Chart {
|
|
|
1495
1683
|
this.hits.add({
|
|
1496
1684
|
_id: p.i, x: l, y: ax.py, w: Math.max(1, r - l), h: ax.ph,
|
|
1497
1685
|
tipX: p.x, tipY: p.y - 8,
|
|
1498
|
-
tooltip: tip(p.i),
|
|
1686
|
+
tooltip: tip(p.i), index: p.i, label: labelAt(p.i), value: values[p.i], row: data[p.i],
|
|
1499
1687
|
});
|
|
1500
1688
|
}
|
|
1501
1689
|
}
|
|
@@ -1513,13 +1701,14 @@ class LineChart extends Chart {
|
|
|
1513
1701
|
|
|
1514
1702
|
|
|
1515
1703
|
class AreaChart extends Chart {
|
|
1516
|
-
get
|
|
1704
|
+
static get meta() {
|
|
1705
|
+
return { id: 'area', name: 'AreaChart', aria: 'Area chart', data: 'series', options: ['labelFormat', 'min', 'max', 'stacked'] };
|
|
1706
|
+
}
|
|
1517
1707
|
|
|
1518
|
-
_legend() { return this._seriesLegend(this.options.series || []); }
|
|
1708
|
+
_legend() { return this._seriesLegend(this.options.data?.series || []); }
|
|
1519
1709
|
_render() {
|
|
1520
1710
|
const o = this.options;
|
|
1521
|
-
const series = o.
|
|
1522
|
-
const cats = o.categories || [];
|
|
1711
|
+
const { categories: cats = [], series = [] } = o.data || {};
|
|
1523
1712
|
if (!series.length || !cats.length) return this._empty();
|
|
1524
1713
|
const e = this.engine;
|
|
1525
1714
|
const t = this.theme;
|
|
@@ -1537,8 +1726,8 @@ class AreaChart extends Chart {
|
|
|
1537
1726
|
} else {
|
|
1538
1727
|
[yMin, yMax] = range(on.map((s) => series[s].values), 0, 0);
|
|
1539
1728
|
}
|
|
1540
|
-
const [yLo, yHi] =
|
|
1541
|
-
const ax = drawCartesian(this, { yMin: yLo, yMax: yHi, xLabels: cats
|
|
1729
|
+
const [yLo, yHi] = valueDomain(o, yMin, yMax);
|
|
1730
|
+
const ax = drawCartesian(this, { yMin: yLo, yMax: yHi, xLabels: cats });
|
|
1542
1731
|
const lineW = t.lineWidth;
|
|
1543
1732
|
const prog = this._anim.progress;
|
|
1544
1733
|
const xAt = (i) => ax.px + ((i + 0.5) / cats.length) * ax.pw;
|
|
@@ -1577,9 +1766,9 @@ class AreaChart extends Chart {
|
|
|
1577
1766
|
for (let i = 0; i < cats.length; i++) {
|
|
1578
1767
|
this.hits.add({
|
|
1579
1768
|
_id: i, x: ax.px + i * slot, y: ax.py, w: slot, h: ax.ph,
|
|
1580
|
-
tipX: xAt(i), tipY: ax.py + 8, index: i,
|
|
1769
|
+
tipX: xAt(i), tipY: ax.py + 8, index: i, label: cats[i],
|
|
1581
1770
|
tooltip: {
|
|
1582
|
-
title:
|
|
1771
|
+
title: this._lab(cats[i]),
|
|
1583
1772
|
rows: on.map((s) => ({ color: this._colorOf(s, series[s]), name: series[s].name || 'Series ' + (s + 1), value: this._fmt(+series[s].values[i]) })),
|
|
1584
1773
|
},
|
|
1585
1774
|
});
|
|
@@ -1595,15 +1784,15 @@ class AreaChart extends Chart {
|
|
|
1595
1784
|
// StackedBarChart — positive values stack up from zero, negative values down.
|
|
1596
1785
|
|
|
1597
1786
|
|
|
1598
|
-
|
|
1599
1787
|
class StackedBarChart extends Chart {
|
|
1600
|
-
get
|
|
1788
|
+
static get meta() {
|
|
1789
|
+
return { id: 'stacked-bar', name: 'StackedBarChart', aria: 'Stacked bar chart', data: 'series', options: ['labelFormat', 'min', 'max'] };
|
|
1790
|
+
}
|
|
1601
1791
|
|
|
1602
|
-
_legend() { return this._seriesLegend(this.options.series || []); }
|
|
1792
|
+
_legend() { return this._seriesLegend(this.options.data?.series || []); }
|
|
1603
1793
|
_render() {
|
|
1604
1794
|
const o = this.options;
|
|
1605
|
-
const series = o.
|
|
1606
|
-
const cats = o.categories || [];
|
|
1795
|
+
const { categories: cats = [], series = [] } = o.data || {};
|
|
1607
1796
|
if (!series.length || !cats.length) return this._empty();
|
|
1608
1797
|
const e = this.engine;
|
|
1609
1798
|
const t = this.theme;
|
|
@@ -1619,8 +1808,8 @@ class StackedBarChart extends Chart {
|
|
|
1619
1808
|
if (pos > hi) hi = pos;
|
|
1620
1809
|
if (neg < lo) lo = neg;
|
|
1621
1810
|
}
|
|
1622
|
-
const [yLo, yHi] =
|
|
1623
|
-
const ax = drawCartesian(this, { yMin: yLo, yMax: yHi, xLabels: cats
|
|
1811
|
+
const [yLo, yHi] = valueDomain(o, lo, hi);
|
|
1812
|
+
const ax = drawCartesian(this, { yMin: yLo, yMax: yHi, xLabels: cats });
|
|
1624
1813
|
|
|
1625
1814
|
const slot = ax.pw / cats.length;
|
|
1626
1815
|
const barW = Math.max(1, Math.min(50, slot * 0.8, slot - 2));
|
|
@@ -1649,8 +1838,8 @@ class StackedBarChart extends Chart {
|
|
|
1649
1838
|
(v > 0 ? lastPos : lastNeg)[i] = item;
|
|
1650
1839
|
this.hits.add({
|
|
1651
1840
|
_id: `${i}-${s}`, x, y, w: barW, h,
|
|
1652
|
-
tipX: x + barW / 2, tipY: y, index: i, series: s, value: v,
|
|
1653
|
-
tooltip: { title:
|
|
1841
|
+
tipX: x + barW / 2, tipY: y, index: i, label: cats[i], series: s, value: v, row: series[s],
|
|
1842
|
+
tooltip: { title: this._lab(cats[i]), rows: [{ color, name: series[s].name || 'Series ' + (s + 1), value: this._fmt(v) }] },
|
|
1654
1843
|
});
|
|
1655
1844
|
}
|
|
1656
1845
|
}
|
|
@@ -1669,23 +1858,23 @@ class StackedBarChart extends Chart {
|
|
|
1669
1858
|
// GroupedBarChart — series side by side in each category.
|
|
1670
1859
|
|
|
1671
1860
|
|
|
1672
|
-
|
|
1673
1861
|
class GroupedBarChart extends Chart {
|
|
1674
|
-
get
|
|
1862
|
+
static get meta() {
|
|
1863
|
+
return { id: 'grouped-bar', name: 'GroupedBarChart', aria: 'Grouped bar chart', data: 'series', options: ['labelFormat', 'min', 'max'] };
|
|
1864
|
+
}
|
|
1675
1865
|
|
|
1676
|
-
_legend() { return this._seriesLegend(this.options.series || []); }
|
|
1866
|
+
_legend() { return this._seriesLegend(this.options.data?.series || []); }
|
|
1677
1867
|
_render() {
|
|
1678
1868
|
const o = this.options;
|
|
1679
|
-
const series = o.
|
|
1680
|
-
const cats = o.categories || [];
|
|
1869
|
+
const { categories: cats = [], series = [] } = o.data || {};
|
|
1681
1870
|
if (!series.length || !cats.length) return this._empty();
|
|
1682
1871
|
const e = this.engine;
|
|
1683
1872
|
const t = this.theme;
|
|
1684
1873
|
const on = series.map((s, i) => i).filter((i) => !this._isOff(i));
|
|
1685
1874
|
|
|
1686
1875
|
const [lo, hi] = range(on.map((s) => series[s].values), 0, 0);
|
|
1687
|
-
const [yLo, yHi] =
|
|
1688
|
-
const ax = drawCartesian(this, { yMin: yLo, yMax: yHi, xLabels: cats
|
|
1876
|
+
const [yLo, yHi] = valueDomain(o, lo, hi);
|
|
1877
|
+
const ax = drawCartesian(this, { yMin: yLo, yMax: yHi, xLabels: cats });
|
|
1689
1878
|
|
|
1690
1879
|
const slot = ax.pw / cats.length;
|
|
1691
1880
|
const inner = slot * 0.8;
|
|
@@ -1707,8 +1896,8 @@ class GroupedBarChart extends Chart {
|
|
|
1707
1896
|
groups.get(color).push({ x, y, w, h, radius: t.barRadius });
|
|
1708
1897
|
this.hits.add({
|
|
1709
1898
|
_id: `${i}-${s}`, x, y: Math.min(y, y0 - 2), w, h: Math.max(h, 4),
|
|
1710
|
-
tipX: x + barW / 2, tipY: y, index: i, series: s, value: v,
|
|
1711
|
-
tooltip: { title:
|
|
1899
|
+
tipX: x + barW / 2, tipY: y, index: i, label: cats[i], series: s, value: v, row: series[s],
|
|
1900
|
+
tooltip: { title: this._lab(cats[i]), rows: [{ color, name: series[s].name || 'Series ' + (s + 1), value: this._fmt(v) }] },
|
|
1712
1901
|
});
|
|
1713
1902
|
});
|
|
1714
1903
|
}
|
|
@@ -1727,26 +1916,27 @@ class GroupedBarChart extends Chart {
|
|
|
1727
1916
|
|
|
1728
1917
|
|
|
1729
1918
|
class MultiLineChart extends Chart {
|
|
1730
|
-
get
|
|
1919
|
+
static get meta() {
|
|
1920
|
+
return { id: 'multi-line', name: 'MultiLineChart', aria: 'Multi line chart', data: 'series', options: ['labelFormat', 'min', 'max', 'curve', 'xScale'], enums: { curve: ['smooth', 'linear', 'step'], xScale: ['category', 'time'] } };
|
|
1921
|
+
}
|
|
1731
1922
|
constructor(container, options) {
|
|
1732
1923
|
super(container, options);
|
|
1733
1924
|
this._follow = true;
|
|
1734
1925
|
}
|
|
1735
|
-
_legend() { return this._seriesLegend(this.options.series || []); }
|
|
1926
|
+
_legend() { return this._seriesLegend(this.options.data?.series || []); }
|
|
1736
1927
|
_render() {
|
|
1737
1928
|
const o = this.options;
|
|
1738
|
-
const series = o.
|
|
1739
|
-
const cats = o.categories || [];
|
|
1929
|
+
const { categories: cats = [], series = [] } = o.data || {};
|
|
1740
1930
|
if (!series.length || !cats.length) return this._empty();
|
|
1741
1931
|
const e = this.engine;
|
|
1742
1932
|
const t = this.theme;
|
|
1743
|
-
const interp = o.
|
|
1744
|
-
const on = series.map((s, i) => i).filter((i) => !this._isOff(i)
|
|
1933
|
+
const interp = o.curve || 'smooth';
|
|
1934
|
+
const on = series.map((s, i) => i).filter((i) => !this._isOff(i));
|
|
1745
1935
|
|
|
1746
1936
|
const [lo, hi] = range(on.map((s) => series[s].values), 0);
|
|
1747
|
-
const [yLo, yHi] =
|
|
1937
|
+
const [yLo, yHi] = valueDomain(o, lo, hi);
|
|
1748
1938
|
const useTime = o.xScale === 'time';
|
|
1749
|
-
const spec = { yMin: yLo, yMax: yHi
|
|
1939
|
+
const spec = { yMin: yLo, yMax: yHi };
|
|
1750
1940
|
if (useTime) spec.xTime = { min: +cats[0], max: +cats[cats.length - 1] };
|
|
1751
1941
|
else spec.xLabels = cats;
|
|
1752
1942
|
const ax = drawCartesian(this, spec);
|
|
@@ -1764,7 +1954,7 @@ class MultiLineChart extends Chart {
|
|
|
1764
1954
|
all.push({ pts, color, s, ser });
|
|
1765
1955
|
if (interp === 'smooth') e.smooth(pts, M.bezierControlPoints(pts), { stroke: color, lineWidth: lineW });
|
|
1766
1956
|
else if (interp === 'linear') e.poly(pts, { stroke: color, lineWidth: lineW, cap: 'round', join: 'round' });
|
|
1767
|
-
else e.step(pts,
|
|
1957
|
+
else e.step(pts, 'before', { stroke: color, lineWidth: lineW });
|
|
1768
1958
|
if (pts.length <= 50) e.circles(pts, color, t.pointRadius);
|
|
1769
1959
|
}
|
|
1770
1960
|
e.unclip();
|
|
@@ -1787,9 +1977,9 @@ class MultiLineChart extends Chart {
|
|
|
1787
1977
|
e.circle(p.x, p.y, t.pointRadius + 2, { fill: t.bg, stroke: sp.color, lineWidth: 2 });
|
|
1788
1978
|
rows.push({ color: sp.color, name: sp.ser.name || 'Series ' + (sp.s + 1), value: this._fmt(+sp.ser.values[idx]) });
|
|
1789
1979
|
}
|
|
1790
|
-
const title =
|
|
1980
|
+
const title = this._lab(cats[idx], useTime);
|
|
1791
1981
|
this._tipNow = { title, rows };
|
|
1792
|
-
this._hover = { _id: idx, index: idx };
|
|
1982
|
+
this._hover = { _id: idx, index: idx, label: cats[idx] };
|
|
1793
1983
|
this.tooltip.show(xPx, ax.py + 8, this._tipNow, t);
|
|
1794
1984
|
} else if (this._cursor) {
|
|
1795
1985
|
this.tooltip.hide();
|
|
@@ -1798,7 +1988,8 @@ class MultiLineChart extends Chart {
|
|
|
1798
1988
|
}
|
|
1799
1989
|
|
|
1800
1990
|
// ── src/charts/scatter.js
|
|
1801
|
-
// ScatterChart — points
|
|
1991
|
+
// ScatterChart — points [{ x, y, size?, group?, label?, color? }], batched per
|
|
1992
|
+
// color/radius; `group` picks the color, `size` is the radius in px.
|
|
1802
1993
|
|
|
1803
1994
|
|
|
1804
1995
|
|
|
@@ -1814,7 +2005,9 @@ function xyExtent(data) {
|
|
|
1814
2005
|
}
|
|
1815
2006
|
|
|
1816
2007
|
class ScatterChart extends Chart {
|
|
1817
|
-
get
|
|
2008
|
+
static get meta() {
|
|
2009
|
+
return { id: 'scatter', name: 'ScatterChart', aria: 'Scatter chart', data: 'points', options: ['labelFormat', 'min', 'max'] };
|
|
2010
|
+
}
|
|
1818
2011
|
|
|
1819
2012
|
_render() {
|
|
1820
2013
|
const o = this.options;
|
|
@@ -1824,10 +2017,9 @@ class ScatterChart extends Chart {
|
|
|
1824
2017
|
const t = this.theme;
|
|
1825
2018
|
const [xMin, xMax, yMin, yMax] = xyExtent(data);
|
|
1826
2019
|
const [xLo, xHi] = M.niceDomain(xMin, xMax, 6);
|
|
1827
|
-
const [yLo, yHi] =
|
|
2020
|
+
const [yLo, yHi] = valueDomain(o, yMin, yMax);
|
|
1828
2021
|
const ax = drawCartesian(this, {
|
|
1829
2022
|
yMin: yLo, yMax: yHi, xLinear: { min: xLo, max: xHi },
|
|
1830
|
-
yTickFormat: o.yTickFormat, xTickFormat: o.xTickFormat,
|
|
1831
2023
|
});
|
|
1832
2024
|
const prog = this._anim.progress;
|
|
1833
2025
|
const groups = new Map();
|
|
@@ -1839,7 +2031,7 @@ class ScatterChart extends Chart {
|
|
|
1839
2031
|
const d = data[i];
|
|
1840
2032
|
const cx = ax.xToPx(d.x);
|
|
1841
2033
|
const cy = ax.yToPx(d.y);
|
|
1842
|
-
const r = (d.
|
|
2034
|
+
const r = (d.size || defaultR) * prog;
|
|
1843
2035
|
const color = d.color || this._colorOf(d.group != null ? d.group : 0);
|
|
1844
2036
|
const key = color + ':' + r.toFixed(1);
|
|
1845
2037
|
if (!groups.has(key)) groups.set(key, { color, r, items: [] });
|
|
@@ -1847,8 +2039,8 @@ class ScatterChart extends Chart {
|
|
|
1847
2039
|
if (hitsOn) {
|
|
1848
2040
|
this.hits.add({
|
|
1849
2041
|
_id: i, x: cx - r - 2, y: cy - r - 2, w: r * 2 + 4, h: r * 2 + 4,
|
|
1850
|
-
tipX: cx, tipY: cy - r,
|
|
1851
|
-
tooltip:
|
|
2042
|
+
tipX: cx, tipY: cy - r, index: i, label: d.label, value: d.y, row: d,
|
|
2043
|
+
tooltip: `${d.label ? d.label + ' ' : ''}(${this._xFmt(d.x)}, ${this._fmt(d.y)})`,
|
|
1852
2044
|
});
|
|
1853
2045
|
}
|
|
1854
2046
|
}
|
|
@@ -1863,13 +2055,16 @@ class ScatterChart extends Chart {
|
|
|
1863
2055
|
}
|
|
1864
2056
|
|
|
1865
2057
|
// ── src/charts/bubble.js
|
|
1866
|
-
// BubbleChart — scatter where `
|
|
2058
|
+
// BubbleChart — scatter where `size` (a data value) sets the bubble area,
|
|
2059
|
+
// scaled into the `radius: [min, max]` range in px.
|
|
1867
2060
|
|
|
1868
2061
|
|
|
1869
2062
|
|
|
1870
2063
|
|
|
1871
2064
|
class BubbleChart extends Chart {
|
|
1872
|
-
get
|
|
2065
|
+
static get meta() {
|
|
2066
|
+
return { id: 'bubble', name: 'BubbleChart', aria: 'Bubble chart', data: 'points', options: ['labelFormat', 'min', 'max', 'radius'] };
|
|
2067
|
+
}
|
|
1873
2068
|
|
|
1874
2069
|
_render() {
|
|
1875
2070
|
const o = this.options;
|
|
@@ -1879,38 +2074,38 @@ class BubbleChart extends Chart {
|
|
|
1879
2074
|
const t = this.theme;
|
|
1880
2075
|
const [xMin, xMax, yMin, yMax] = xyExtent(data);
|
|
1881
2076
|
let zMax = 0;
|
|
1882
|
-
for (const d of data) if ((d.
|
|
2077
|
+
for (const d of data) if ((d.size || 0) > zMax) zMax = d.size;
|
|
1883
2078
|
// Pad the domain so the biggest bubbles stay inside the plot.
|
|
1884
2079
|
const px = (xMax - xMin || 1) * 0.08, py = (yMax - yMin || 1) * 0.1;
|
|
1885
2080
|
const [xLo, xHi] = M.niceDomain(xMin - px, xMax + px, 6);
|
|
1886
|
-
const [yLo, yHi] =
|
|
2081
|
+
const [yLo, yHi] = valueDomain(o, yMin - py, yMax + py);
|
|
1887
2082
|
const ax = drawCartesian(this, {
|
|
1888
2083
|
yMin: yLo, yMax: yHi, xLinear: { min: xLo, max: xHi },
|
|
1889
|
-
yTickFormat: o.yTickFormat, xTickFormat: o.xTickFormat,
|
|
1890
2084
|
});
|
|
1891
2085
|
const prog = this._anim.progress;
|
|
1892
|
-
const maxR = o.
|
|
2086
|
+
const [minR, maxR] = Array.isArray(o.radius) ? o.radius : [4, 32];
|
|
1893
2087
|
|
|
1894
2088
|
for (let i = 0; i < data.length; i++) {
|
|
1895
2089
|
const d = data[i];
|
|
1896
2090
|
const cx = ax.xToPx(d.x);
|
|
1897
2091
|
const cy = ax.yToPx(d.y);
|
|
1898
|
-
const r = (minR + Math.sqrt((d.
|
|
2092
|
+
const r = (minR + Math.sqrt((d.size || 0) / Math.max(1e-9, zMax)) * (maxR - minR)) * prog;
|
|
1899
2093
|
const color = d.color || this._colorOf(d.group != null ? d.group : i);
|
|
1900
2094
|
const hover = this._hover && this._hover._id === i;
|
|
1901
2095
|
e.circle(cx, cy, r, { fill: M.colorAlpha(color, hover ? 0.75 : 0.5), stroke: color, lineWidth: 1.5 });
|
|
1902
2096
|
this.hits.add({
|
|
1903
2097
|
_id: i, x: cx - r, y: cy - r, w: r * 2, h: r * 2,
|
|
1904
2098
|
test: (x, y) => (x - cx) ** 2 + (y - cy) ** 2 <= r * r,
|
|
1905
|
-
tipX: cx, tipY: cy - r,
|
|
1906
|
-
tooltip: `${d.label ? d.label + ' ' : ''}(${this.
|
|
2099
|
+
tipX: cx, tipY: cy - r, index: i, label: d.label, value: d.y, row: d,
|
|
2100
|
+
tooltip: `${d.label ? d.label + ' ' : ''}(${this._xFmt(d.x)}, ${this._fmt(d.y)}) · ${this._fmt(d.size)}`,
|
|
1907
2101
|
});
|
|
1908
2102
|
}
|
|
1909
2103
|
}
|
|
1910
2104
|
}
|
|
1911
2105
|
|
|
1912
2106
|
// ── src/charts/pie.js
|
|
1913
|
-
// PieChart — `innerRadius` turns it into a donut with
|
|
2107
|
+
// PieChart — `innerRadius` (0–0.9 of the radius) turns it into a donut with
|
|
2108
|
+
// a center label (`center`: text, or false for none).
|
|
1914
2109
|
// Slices are hit-tested by angle and radius, and toggle from the legend.
|
|
1915
2110
|
|
|
1916
2111
|
|
|
@@ -1927,11 +2122,13 @@ function inSector(cx, cy, rIn, rOut, a0, a1) {
|
|
|
1927
2122
|
}
|
|
1928
2123
|
|
|
1929
2124
|
class PieChart extends Chart {
|
|
1930
|
-
get
|
|
2125
|
+
static get meta() {
|
|
2126
|
+
return { id: 'pie', name: 'PieChart', aria: 'Pie chart', data: 'rows', options: ['label', 'value', 'innerRadius', 'center'] };
|
|
2127
|
+
}
|
|
1931
2128
|
|
|
1932
2129
|
_legend() {
|
|
1933
2130
|
const o = this.options;
|
|
1934
|
-
const labelOf =
|
|
2131
|
+
const labelOf = this._fn('label') || ((d) => d.label);
|
|
1935
2132
|
return toRows(o.data).map((d, i) => ({ label: String(labelOf(d, i)), color: this._colorOf(i, d) }));
|
|
1936
2133
|
}
|
|
1937
2134
|
_render() {
|
|
@@ -1940,16 +2137,20 @@ class PieChart extends Chart {
|
|
|
1940
2137
|
if (!data.length) return this._empty();
|
|
1941
2138
|
const e = this.engine;
|
|
1942
2139
|
const t = this.theme;
|
|
1943
|
-
const labelOf =
|
|
1944
|
-
const valueOf =
|
|
1945
|
-
const
|
|
2140
|
+
const labelOf = this._fn('label') || ((d) => d.label);
|
|
2141
|
+
const valueOf = this._fn('value') || ((d) => d.value);
|
|
2142
|
+
const raw = data.map((d, i) => +valueOf(d, i) || 0);
|
|
2143
|
+
const neg = raw.findIndex((v) => v < 0);
|
|
2144
|
+
if (neg >= 0) warn(`PieChart: values can't be negative — "${labelOf(data[neg], neg)}" is ${raw[neg]}. It is drawn as 0.`);
|
|
2145
|
+
const vals = raw.map((v, i) => (this._isOff(i) ? 0 : Math.max(0, v)));
|
|
1946
2146
|
const total = vals.reduce((a, v) => a + v, 0);
|
|
1947
2147
|
if (total <= 0) return this._empty();
|
|
1948
2148
|
|
|
1949
2149
|
const cx = e.w / 2, cy = e.h / 2;
|
|
1950
2150
|
const R = Math.max(4, Math.min(e.w, e.h) / 2 - 12);
|
|
1951
|
-
const ir = o.innerRadius;
|
|
1952
|
-
|
|
2151
|
+
const ir = +o.innerRadius || 0;
|
|
2152
|
+
if (ir > 0.9) warn(`PieChart: innerRadius is a ratio from 0 to 0.9 (0.6 = donut) — got ${ir}.`);
|
|
2153
|
+
const innerR = R * Math.min(0.9, Math.max(0, ir));
|
|
1953
2154
|
const prog = this._anim.progress;
|
|
1954
2155
|
let a0 = -Math.PI / 2;
|
|
1955
2156
|
for (let i = 0; i < data.length; i++) {
|
|
@@ -1969,14 +2170,14 @@ class PieChart extends Chart {
|
|
|
1969
2170
|
_id: i, x: cx - R, y: cy - R, w: R * 2, h: R * 2,
|
|
1970
2171
|
test: inSector(cx, cy, innerR, R + off, a0, a1),
|
|
1971
2172
|
tipX: cx + Math.cos(mid) * midR, tipY: cy + Math.sin(mid) * midR - 8,
|
|
1972
|
-
|
|
1973
|
-
tooltip: `${labelOf(data[i], i)}: ${this._fmt(v)} (${((v / total) * 100).toFixed(1)}%)`,
|
|
2173
|
+
index: i, label: labelOf(data[i], i), value: v, row: data[i],
|
|
2174
|
+
tooltip: `${this._lab(labelOf(data[i], i))}: ${this._fmt(v)} (${((v / total) * 100).toFixed(1)}%)`,
|
|
1974
2175
|
});
|
|
1975
2176
|
a0 = a1;
|
|
1976
2177
|
}
|
|
1977
|
-
if (innerR > 0 && o.
|
|
2178
|
+
if (innerR > 0 && o.center !== false) {
|
|
1978
2179
|
const h = this._hover;
|
|
1979
|
-
const center = h ? `${((vals[h._id] / total) * 100).toFixed(0)}%` : o.
|
|
2180
|
+
const center = h ? `${((vals[h._id] / total) * 100).toFixed(0)}%` : o.center ?? this._fmt(total);
|
|
1980
2181
|
e.text(center, cx, cy, {
|
|
1981
2182
|
fill: t.text, font: `600 ${Math.max(12, Math.min(t.fontSize * 1.8, innerR / 2.2))}px ${t.font}`,
|
|
1982
2183
|
align: 'center', baseline: 'middle',
|
|
@@ -1992,19 +2193,21 @@ class PieChart extends Chart {
|
|
|
1992
2193
|
|
|
1993
2194
|
|
|
1994
2195
|
class RadarChart extends Chart {
|
|
1995
|
-
get
|
|
2196
|
+
static get meta() {
|
|
2197
|
+
return { id: 'radar', name: 'RadarChart', aria: 'Radar chart', data: 'series', options: ['labelFormat', 'max'] };
|
|
2198
|
+
}
|
|
1996
2199
|
|
|
1997
|
-
_legend() { return this._seriesLegend(this.options.series || []); }
|
|
2200
|
+
_legend() { return this._seriesLegend(this.options.data?.series || []); }
|
|
1998
2201
|
_render() {
|
|
1999
2202
|
const o = this.options;
|
|
2000
|
-
const series = o.
|
|
2001
|
-
const axes = o.axes || [];
|
|
2203
|
+
const { categories: axes = [], series = [] } = o.data || {};
|
|
2002
2204
|
if (!series.length || !axes.length) return this._empty();
|
|
2003
2205
|
const e = this.engine;
|
|
2004
2206
|
const t = this.theme;
|
|
2005
2207
|
const font = `${t.fontSize}px ${t.font}`;
|
|
2006
2208
|
let labelW = 0;
|
|
2007
|
-
|
|
2209
|
+
const names = axes.map((a) => this._lab(a));
|
|
2210
|
+
for (const a of names) labelW = Math.max(labelW, e.measure(a, font));
|
|
2008
2211
|
const cx = e.w / 2;
|
|
2009
2212
|
const cy = e.h / 2;
|
|
2010
2213
|
const R = Math.max(10, Math.min(e.w / 2 - labelW - 18, e.h / 2 - t.fontSize - 16));
|
|
@@ -2012,7 +2215,7 @@ class RadarChart extends Chart {
|
|
|
2012
2215
|
const angle = (i) => -Math.PI / 2 + (i / N) * TAU;
|
|
2013
2216
|
const on = series.map((s, i) => i).filter((i) => !this._isOff(i));
|
|
2014
2217
|
const max = range(on.map((s) => series[s].values), 0)[1];
|
|
2015
|
-
const niceMax = M.niceDomain(0, max, 5)[1] || 1;
|
|
2218
|
+
const niceMax = o.max ?? (M.niceDomain(0, max, 5)[1] || 1);
|
|
2016
2219
|
|
|
2017
2220
|
const ctx = e.ctx;
|
|
2018
2221
|
ctx.strokeStyle = t.grid;
|
|
@@ -2037,7 +2240,7 @@ class RadarChart extends Chart {
|
|
|
2037
2240
|
ctx.stroke();
|
|
2038
2241
|
for (let i = 0; i < N; i++) {
|
|
2039
2242
|
const a = angle(i);
|
|
2040
|
-
e.text(
|
|
2243
|
+
e.text(names[i], cx + Math.cos(a) * (R + 10), cy + Math.sin(a) * (R + 10), {
|
|
2041
2244
|
fill: t.muted, font,
|
|
2042
2245
|
align: Math.cos(a) > 0.3 ? 'left' : Math.cos(a) < -0.3 ? 'right' : 'center',
|
|
2043
2246
|
baseline: Math.sin(a) > 0.3 ? 'top' : Math.sin(a) < -0.3 ? 'bottom' : 'middle',
|
|
@@ -2050,14 +2253,14 @@ class RadarChart extends Chart {
|
|
|
2050
2253
|
const pts = [];
|
|
2051
2254
|
for (let i = 0; i < N; i++) {
|
|
2052
2255
|
const v = +ser.values[i] || 0;
|
|
2053
|
-
const r = ((v * prog) / niceMax) * R;
|
|
2256
|
+
const r = Math.min(1, (v * prog) / niceMax) * R;
|
|
2054
2257
|
const a = angle(i);
|
|
2055
2258
|
const p = { x: cx + Math.cos(a) * r, y: cy + Math.sin(a) * r };
|
|
2056
2259
|
pts.push(p);
|
|
2057
2260
|
this.hits.add({
|
|
2058
2261
|
_id: `${s}-${i}`, x: p.x - 7, y: p.y - 7, w: 14, h: 14,
|
|
2059
|
-
tipX: p.x, tipY: p.y - 4, index: i, series: s, value: v,
|
|
2060
|
-
tooltip: { title:
|
|
2262
|
+
tipX: p.x, tipY: p.y - 4, index: i, label: axes[i], series: s, value: v, row: ser,
|
|
2263
|
+
tooltip: { title: names[i], rows: [{ color, name: ser.name || 'Series ' + (s + 1), value: this._fmt(v) }] },
|
|
2061
2264
|
});
|
|
2062
2265
|
}
|
|
2063
2266
|
e.poly(pts, { fill: M.colorAlpha(color, 0.2), stroke: color, lineWidth: 2, close: true, join: 'round' });
|
|
@@ -2076,7 +2279,9 @@ class RadarChart extends Chart {
|
|
|
2076
2279
|
|
|
2077
2280
|
|
|
2078
2281
|
class PolarChart extends Chart {
|
|
2079
|
-
get
|
|
2282
|
+
static get meta() {
|
|
2283
|
+
return { id: 'polar', name: 'PolarChart', aria: 'Polar chart', data: 'rows', options: ['label', 'value', 'scale'], enums: { scale: ['sqrt', 'linear'] } };
|
|
2284
|
+
}
|
|
2080
2285
|
|
|
2081
2286
|
_render() {
|
|
2082
2287
|
const o = this.options;
|
|
@@ -2084,8 +2289,8 @@ class PolarChart extends Chart {
|
|
|
2084
2289
|
if (!data.length) return this._empty();
|
|
2085
2290
|
const e = this.engine;
|
|
2086
2291
|
const t = this.theme;
|
|
2087
|
-
const labelOf =
|
|
2088
|
-
const valueOf =
|
|
2292
|
+
const labelOf = this._fn('label') || ((d) => d.label);
|
|
2293
|
+
const valueOf = this._fn('value') || ((d) => d.value);
|
|
2089
2294
|
const N = data.length;
|
|
2090
2295
|
const font = `${t.fontSize}px ${t.font}`;
|
|
2091
2296
|
const cx = e.w / 2, cy = e.h / 2;
|
|
@@ -2116,7 +2321,7 @@ class PolarChart extends Chart {
|
|
|
2116
2321
|
_id: i, x: cx - R, y: cy - R, w: R * 2, h: R * 2,
|
|
2117
2322
|
test: inSector(cx, cy, 0, Math.max(r, 12), a0, a1),
|
|
2118
2323
|
tipX: cx + c * r * 0.7, tipY: cy + s * r * 0.7 - 6,
|
|
2119
|
-
|
|
2324
|
+
index: i, label: labelOf(data[i], i), value: v, row: data[i],
|
|
2120
2325
|
tooltip: `${labelOf(data[i], i)}: ${this._fmt(v)}`,
|
|
2121
2326
|
});
|
|
2122
2327
|
}
|
|
@@ -2124,21 +2329,26 @@ class PolarChart extends Chart {
|
|
|
2124
2329
|
}
|
|
2125
2330
|
|
|
2126
2331
|
// ── src/charts/gauge.js
|
|
2127
|
-
// GaugeChart — animated arc;
|
|
2332
|
+
// GaugeChart — animated arc; update({ data }) tweens from the shown value.
|
|
2128
2333
|
|
|
2129
2334
|
class GaugeChart extends Chart {
|
|
2130
|
-
get
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2335
|
+
static get meta() {
|
|
2336
|
+
return {
|
|
2337
|
+
id: 'gauge', name: 'GaugeChart', aria: 'Gauge', data: 'number', options: ['min', 'max', 'unit'],
|
|
2338
|
+
renamed: { value: 'data', label: 'title' },
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
update(patch = {}) {
|
|
2342
|
+
if ('data' in patch) this._from = this._shown;
|
|
2343
|
+
return super.update(patch);
|
|
2134
2344
|
}
|
|
2135
2345
|
_render() {
|
|
2136
2346
|
const e = this.engine;
|
|
2137
2347
|
const t = this.theme;
|
|
2138
2348
|
const o = this.options;
|
|
2139
|
-
const min = o.min
|
|
2140
|
-
const max = o.max
|
|
2141
|
-
const target = Math.max(min, Math.min(max, +o.
|
|
2349
|
+
const min = o.min ?? 0;
|
|
2350
|
+
const max = o.max ?? 100;
|
|
2351
|
+
const target = Math.max(min, Math.min(max, +o.data || 0));
|
|
2142
2352
|
const from = this._from ?? min;
|
|
2143
2353
|
const val = from + (target - from) * this._anim.progress;
|
|
2144
2354
|
this._shown = val;
|
|
@@ -2148,7 +2358,7 @@ class GaugeChart extends Chart {
|
|
|
2148
2358
|
const r = R * 0.8;
|
|
2149
2359
|
const a0 = Math.PI * 1.1;
|
|
2150
2360
|
const a1 = Math.PI * 1.9;
|
|
2151
|
-
const color =
|
|
2361
|
+
const color = this.palette[0];
|
|
2152
2362
|
e.arc(cx, cy, r, R, a0, a1, { fill: t.grid });
|
|
2153
2363
|
const aEnd = a0 + ((a1 - a0) * (val - min)) / (max - min || 1);
|
|
2154
2364
|
if (aEnd > a0) e.arc(cx, cy, r, R, a0, aEnd, { fill: color });
|
|
@@ -2156,66 +2366,70 @@ class GaugeChart extends Chart {
|
|
|
2156
2366
|
e.text(txt, cx, cy - R * 0.08, {
|
|
2157
2367
|
fill: t.text, font: `700 ${Math.max(12, R / 2.6)}px ${t.font}`, align: 'center', baseline: 'middle',
|
|
2158
2368
|
});
|
|
2159
|
-
if (o.
|
|
2160
|
-
e.text(o.
|
|
2369
|
+
if (o.title) {
|
|
2370
|
+
e.text(o.title, cx, cy + R * 0.22, { fill: t.muted, font: `${t.fontSize}px ${t.font}`, align: 'center', baseline: 'top' });
|
|
2161
2371
|
}
|
|
2162
|
-
this.canvas.setAttribute('aria-label', `${o.
|
|
2372
|
+
this.canvas.setAttribute('aria-label', `${o.title || 'Gauge'}: ${txt}`);
|
|
2163
2373
|
}
|
|
2164
2374
|
}
|
|
2165
2375
|
|
|
2166
2376
|
// ── src/charts/heatmap.js
|
|
2167
|
-
// HeatmapChart —
|
|
2377
|
+
// HeatmapChart — data { x: [...], y: [...], values: [[...]] } (values[y][x]),
|
|
2378
|
+
// cells colored between colors.low and colors.high.
|
|
2168
2379
|
|
|
2169
2380
|
|
|
2170
2381
|
class HeatmapChart extends Chart {
|
|
2171
|
-
get
|
|
2382
|
+
static get meta() {
|
|
2383
|
+
return { id: 'heatmap', name: 'HeatmapChart', aria: 'Heatmap', data: 'matrix', options: ['labelFormat'], colors: 'an object like { low: \'#eef\', high: \'#36c\' }' };
|
|
2384
|
+
}
|
|
2172
2385
|
|
|
2173
2386
|
_render() {
|
|
2174
2387
|
const o = this.options;
|
|
2175
|
-
const
|
|
2176
|
-
|
|
2177
|
-
const yLabels = o.yLabels || [];
|
|
2178
|
-
if (!data.length || !xLabels.length || !yLabels.length) return this._empty();
|
|
2388
|
+
const { x: xLabels = [], y: yLabels = [], values = [] } = o.data || {};
|
|
2389
|
+
if (!values.length || !xLabels.length || !yLabels.length) return this._empty();
|
|
2179
2390
|
const e = this.engine;
|
|
2180
2391
|
const t = this.theme;
|
|
2181
2392
|
const font = `${t.fontSize}px ${t.font}`;
|
|
2182
2393
|
let labelW = 0;
|
|
2183
|
-
|
|
2394
|
+
const yText = Array.from(yLabels, (l) => this._lab(l));
|
|
2395
|
+
for (const s of yText) labelW = Math.max(labelW, e.measure(s, font));
|
|
2184
2396
|
const px = Math.ceil(labelW) + 12, py = 8;
|
|
2185
2397
|
const pw = Math.max(1, e.w - px - 8);
|
|
2186
2398
|
const ph = Math.max(1, e.h - py - t.fontSize - 14);
|
|
2187
2399
|
|
|
2188
2400
|
let vMax = -Infinity, vMin = Infinity;
|
|
2189
|
-
for (const
|
|
2190
|
-
|
|
2191
|
-
|
|
2401
|
+
for (const row of values) {
|
|
2402
|
+
for (const v of row || []) {
|
|
2403
|
+
if (v > vMax) vMax = v;
|
|
2404
|
+
if (v < vMin) vMin = v;
|
|
2405
|
+
}
|
|
2192
2406
|
}
|
|
2193
2407
|
const cellW = pw / xLabels.length;
|
|
2194
2408
|
const cellH = ph / yLabels.length;
|
|
2195
|
-
const
|
|
2196
|
-
const hi =
|
|
2409
|
+
const c = o.colors || {};
|
|
2410
|
+
const hi = c.high || this.palette[0];
|
|
2411
|
+
const lo = c.low || M.colorAlpha(hi, 0.08);
|
|
2197
2412
|
const gap = cellW > 6 && cellH > 6 ? 1 : 0;
|
|
2198
2413
|
const prog = this._anim.progress;
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
});
|
|
2414
|
+
for (let cy = 0; cy < yLabels.length; cy++) {
|
|
2415
|
+
const row = values[cy] || [];
|
|
2416
|
+
for (let cx = 0; cx < xLabels.length; cx++) {
|
|
2417
|
+
const v = row[cx];
|
|
2418
|
+
if (v == null || v !== v) continue;
|
|
2419
|
+
const x = px + cx * cellW;
|
|
2420
|
+
const y = py + cy * cellH;
|
|
2421
|
+
const n = vMax > vMin ? (v - vMin) / (vMax - vMin) : 0.5;
|
|
2422
|
+
e.rect(x + gap, y + gap, cellW - gap * 2, cellH - gap * 2, { fill: M.colorMix(lo, hi, n * prog), radius: gap ? 2 : 0 });
|
|
2423
|
+
this.hits.add({
|
|
2424
|
+
_id: cy * xLabels.length + cx, x, y, w: cellW, h: cellH, tipX: x + cellW / 2, tipY: y,
|
|
2425
|
+
index: cx, series: cy, label: xLabels[cx], value: v, row: { x: xLabels[cx], y: yLabels[cy], value: v },
|
|
2426
|
+
tooltip: `${this._lab(xLabels[cx])} · ${yText[cy]}: ${this._fmt(v)}`,
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2215
2429
|
}
|
|
2216
2430
|
let lastEnd = -Infinity;
|
|
2217
2431
|
for (let i = 0; i < xLabels.length; i++) {
|
|
2218
|
-
const txt =
|
|
2432
|
+
const txt = this._lab(xLabels[i]);
|
|
2219
2433
|
const w = e.measure(txt, font);
|
|
2220
2434
|
const x = px + (i + 0.5) * cellW;
|
|
2221
2435
|
if (x - w / 2 < lastEnd + 4) continue;
|
|
@@ -2224,7 +2438,7 @@ class HeatmapChart extends Chart {
|
|
|
2224
2438
|
}
|
|
2225
2439
|
const step = Math.ceil((t.fontSize + 2) / cellH);
|
|
2226
2440
|
for (let i = 0; i < yLabels.length; i += step) {
|
|
2227
|
-
e.text(
|
|
2441
|
+
e.text(yText[i], px - 6, py + (i + 0.5) * cellH, { fill: t.muted, font, align: 'right', baseline: 'middle' });
|
|
2228
2442
|
}
|
|
2229
2443
|
if (this._hover) {
|
|
2230
2444
|
const r = this._hover;
|
|
@@ -2239,7 +2453,9 @@ class HeatmapChart extends Chart {
|
|
|
2239
2453
|
|
|
2240
2454
|
|
|
2241
2455
|
class BoxplotChart extends Chart {
|
|
2242
|
-
get
|
|
2456
|
+
static get meta() {
|
|
2457
|
+
return { id: 'boxplot', name: 'BoxplotChart', aria: 'Box plot', data: 'boxes', options: ['labelFormat', 'min', 'max'] };
|
|
2458
|
+
}
|
|
2243
2459
|
|
|
2244
2460
|
_render() {
|
|
2245
2461
|
const o = this.options;
|
|
@@ -2255,8 +2471,8 @@ class BoxplotChart extends Chart {
|
|
|
2255
2471
|
if (v < yMin) yMin = v;
|
|
2256
2472
|
}
|
|
2257
2473
|
}
|
|
2258
|
-
const [yLo, yHi] =
|
|
2259
|
-
const ax = drawCartesian(this, { yMin: yLo, yMax: yHi, xLabels: data.map((d) => d.label)
|
|
2474
|
+
const [yLo, yHi] = valueDomain(o, yMin, yMax);
|
|
2475
|
+
const ax = drawCartesian(this, { yMin: yLo, yMax: yHi, xLabels: data.map((d) => d.label) });
|
|
2260
2476
|
const slot = ax.pw / data.length;
|
|
2261
2477
|
const boxW = Math.min(40, slot * 0.5);
|
|
2262
2478
|
const prog = this._anim.progress;
|
|
@@ -2279,9 +2495,9 @@ class BoxplotChart extends Chart {
|
|
|
2279
2495
|
const top = ax.yToPx(d.max), bot = ax.yToPx(d.min);
|
|
2280
2496
|
this.hits.add({
|
|
2281
2497
|
_id: i, x: cx - boxW / 2, y: top, w: boxW, h: Math.max(4, bot - top),
|
|
2282
|
-
tipX: cx, tipY: ax.yToPx(d.q3),
|
|
2498
|
+
tipX: cx, tipY: ax.yToPx(d.q3), index: i, label: d.label, value: d.median, row: d,
|
|
2283
2499
|
tooltip: {
|
|
2284
|
-
title:
|
|
2500
|
+
title: this._lab(d.label),
|
|
2285
2501
|
rows: [['max', d.max], ['Q3', d.q3], ['median', d.median], ['Q1', d.q1], ['min', d.min]]
|
|
2286
2502
|
.map(([name, v]) => ({ name, value: this._fmt(v) })),
|
|
2287
2503
|
},
|
|
@@ -2294,7 +2510,9 @@ class BoxplotChart extends Chart {
|
|
|
2294
2510
|
// FunnelChart — conversion stages; each segment narrows to the next value.
|
|
2295
2511
|
|
|
2296
2512
|
class FunnelChart extends Chart {
|
|
2297
|
-
get
|
|
2513
|
+
static get meta() {
|
|
2514
|
+
return { id: 'funnel', name: 'FunnelChart', aria: 'Funnel chart', data: 'rows', options: ['label', 'value', 'labelColor'] };
|
|
2515
|
+
}
|
|
2298
2516
|
|
|
2299
2517
|
_render() {
|
|
2300
2518
|
const o = this.options;
|
|
@@ -2302,8 +2520,8 @@ class FunnelChart extends Chart {
|
|
|
2302
2520
|
if (!data.length) return this._empty();
|
|
2303
2521
|
const e = this.engine;
|
|
2304
2522
|
const t = this.theme;
|
|
2305
|
-
const labelOf =
|
|
2306
|
-
const valueOf =
|
|
2523
|
+
const labelOf = this._fn('label') || ((d) => d.label);
|
|
2524
|
+
const valueOf = this._fn('value') || ((d) => d.value);
|
|
2307
2525
|
const N = data.length;
|
|
2308
2526
|
const vals = data.map((d, i) => Math.max(0, +valueOf(d, i) || 0));
|
|
2309
2527
|
const max = Math.max(1e-9, vals.reduce((a, v) => (v > a ? v : a), 0));
|
|
@@ -2336,7 +2554,7 @@ class FunnelChart extends Chart {
|
|
|
2336
2554
|
});
|
|
2337
2555
|
this.hits.add({
|
|
2338
2556
|
_id: i, x: cx - Math.max(wTop, 8) / 2, y: yTop, w: Math.max(wTop, 8), h: segH,
|
|
2339
|
-
tipX: cx, tipY: yTop,
|
|
2557
|
+
tipX: cx, tipY: yTop, index: i, label: labelOf(data[i], i), value: v, row: data[i],
|
|
2340
2558
|
tooltip: `${labelOf(data[i], i)}: ${this._fmt(v)} (${((v / first) * 100).toFixed(1)}%)`,
|
|
2341
2559
|
});
|
|
2342
2560
|
}
|
|
@@ -2383,7 +2601,9 @@ function squarify(items, rect, place) {
|
|
|
2383
2601
|
}
|
|
2384
2602
|
|
|
2385
2603
|
class TreemapChart extends Chart {
|
|
2386
|
-
get
|
|
2604
|
+
static get meta() {
|
|
2605
|
+
return { id: 'treemap', name: 'TreemapChart', aria: 'Treemap', data: 'rows', options: ['label', 'value', 'labelColor'] };
|
|
2606
|
+
}
|
|
2387
2607
|
|
|
2388
2608
|
_render() {
|
|
2389
2609
|
const o = this.options;
|
|
@@ -2391,10 +2611,10 @@ class TreemapChart extends Chart {
|
|
|
2391
2611
|
if (!data.length) return this._empty();
|
|
2392
2612
|
const e = this.engine;
|
|
2393
2613
|
const t = this.theme;
|
|
2394
|
-
const labelOf =
|
|
2395
|
-
const valueOf =
|
|
2614
|
+
const labelOf = this._fn('label') || ((d) => d.label);
|
|
2615
|
+
const valueOf = this._fn('value') || ((d) => d.value);
|
|
2396
2616
|
const items = data
|
|
2397
|
-
.map((d, i) => ({ label: String(labelOf(d, i)), value: Math.max(0, +valueOf(d, i) || 0), color: this._colorOf(i, d), data: d, _id: i }))
|
|
2617
|
+
.map((d, i) => ({ raw: labelOf(d, i), label: String(labelOf(d, i)), value: Math.max(0, +valueOf(d, i) || 0), color: this._colorOf(i, d), data: d, _id: i }))
|
|
2398
2618
|
.filter((it) => it.value > 0)
|
|
2399
2619
|
.sort((a, b) => b.value - a.value);
|
|
2400
2620
|
if (!items.length) return this._empty();
|
|
@@ -2414,7 +2634,7 @@ class TreemapChart extends Chart {
|
|
|
2414
2634
|
e.withAlpha(0.85, () => e.text(this._fmt(it.value), x + 6, y + 8 + t.fontSize, { fill: fg, font: `${t.fontSize}px ${t.font}`, align: 'left', baseline: 'top' }));
|
|
2415
2635
|
}
|
|
2416
2636
|
this.hits.add({
|
|
2417
|
-
_id: it._id, x, y, w, h, tipX: x + w / 2, tipY: y,
|
|
2637
|
+
_id: it._id, x, y, w, h, tipX: x + w / 2, tipY: y, index: it._id, label: it.raw, value: it.value, row: it.data,
|
|
2418
2638
|
tooltip: `${it.label}: ${this._fmt(it.value)}`,
|
|
2419
2639
|
});
|
|
2420
2640
|
});
|
|
@@ -2422,22 +2642,26 @@ class TreemapChart extends Chart {
|
|
|
2422
2642
|
}
|
|
2423
2643
|
|
|
2424
2644
|
// ── src/charts/sparkline.js
|
|
2425
|
-
//
|
|
2645
|
+
// SparklineChart — tiny trend line, no axes. Same data as LineChart.
|
|
2426
2646
|
|
|
2427
2647
|
|
|
2428
2648
|
|
|
2429
|
-
class
|
|
2430
|
-
get
|
|
2649
|
+
class SparklineChart extends Chart {
|
|
2650
|
+
static get meta() {
|
|
2651
|
+
return { id: 'sparkline', name: 'SparklineChart', aria: 'Sparkline', data: 'rows', options: ['label', 'value', 'min', 'max', 'fill', 'lastDot'] };
|
|
2652
|
+
}
|
|
2431
2653
|
|
|
2432
2654
|
_render() {
|
|
2433
2655
|
const o = this.options;
|
|
2434
|
-
const data = o.data
|
|
2656
|
+
const data = toRows(o.data);
|
|
2435
2657
|
if (!data.length) return;
|
|
2436
2658
|
const e = this.engine;
|
|
2437
2659
|
const t = this.theme;
|
|
2438
|
-
const valueOf =
|
|
2439
|
-
const
|
|
2440
|
-
const
|
|
2660
|
+
const valueOf = this._fn('value') || ((d) => d.value);
|
|
2661
|
+
const labelOf = this._fn('label') || ((d) => d.label);
|
|
2662
|
+
const values = data.map((d, i) => +valueOf(d, i));
|
|
2663
|
+
const [lo, hi] = range([values]);
|
|
2664
|
+
const yMin = o.min ?? lo, yMax = o.max ?? hi;
|
|
2441
2665
|
const yRange = yMax - yMin || 1;
|
|
2442
2666
|
const pad = Math.max(2, t.pointRadius + 1);
|
|
2443
2667
|
const w = e.w - pad * 2;
|
|
@@ -2446,7 +2670,7 @@ class Sparkline extends Chart {
|
|
|
2446
2670
|
x: pad + (i / (values.length - 1 || 1)) * w,
|
|
2447
2671
|
y: pad + h - ((v - yMin) / yRange) * h,
|
|
2448
2672
|
}));
|
|
2449
|
-
const color =
|
|
2673
|
+
const color = this.palette[0];
|
|
2450
2674
|
const cps = M.bezierControlPoints(points);
|
|
2451
2675
|
e.clip(0, 0, e.w * this._anim.progress, e.h);
|
|
2452
2676
|
if (o.fill !== false) {
|
|
@@ -2463,7 +2687,8 @@ class Sparkline extends Chart {
|
|
|
2463
2687
|
points.forEach((p, i) => {
|
|
2464
2688
|
const l = i ? (points[i - 1].x + p.x) / 2 : 0;
|
|
2465
2689
|
const r = i < n - 1 ? (points[i + 1].x + p.x) / 2 : e.w;
|
|
2466
|
-
|
|
2690
|
+
const label = labelOf(data[i], i);
|
|
2691
|
+
this.hits.add({ _id: i, x: l, y: 0, w: r - l, h: e.h, tipX: p.x, tipY: p.y - 4, index: i, label, value: values[i], row: data[i], tooltip: `${label}: ${this._fmt(values[i])}` });
|
|
2467
2692
|
});
|
|
2468
2693
|
if (this._hover) {
|
|
2469
2694
|
const p = points[this._hover._id];
|
|
@@ -2473,23 +2698,34 @@ class Sparkline extends Chart {
|
|
|
2473
2698
|
}
|
|
2474
2699
|
|
|
2475
2700
|
// ── src/charts/realtime.js
|
|
2476
|
-
// RealtimeChart — streaming lines over a rolling window
|
|
2477
|
-
|
|
2701
|
+
// RealtimeChart — streaming lines over a rolling window.
|
|
2702
|
+
// data: { series: [{ name, color? }] } then chart.push([v1, v2, …]) per sample
|
|
2478
2703
|
|
|
2479
2704
|
|
|
2480
2705
|
class RealtimeChart extends Chart {
|
|
2481
|
-
get
|
|
2706
|
+
static get meta() {
|
|
2707
|
+
return { id: 'realtime', name: 'RealtimeChart', aria: 'Realtime chart', data: 'stream', options: ['min', 'max', 'window'] };
|
|
2708
|
+
}
|
|
2482
2709
|
constructor(container, options = {}) {
|
|
2483
2710
|
super(container, options);
|
|
2484
|
-
this.
|
|
2485
|
-
this.
|
|
2711
|
+
this._series = [];
|
|
2712
|
+
this._sync();
|
|
2486
2713
|
this._follow = true;
|
|
2487
|
-
|
|
2714
|
+
}
|
|
2715
|
+
// (Re)build the series from data, keeping samples already received.
|
|
2716
|
+
_sync() {
|
|
2717
|
+
const list = this.options.data?.series || [{ name: 'Series' }];
|
|
2718
|
+
this._series = list.map((s, i) => ({ name: s.name || 'Series ' + (i + 1), color: s.color, values: this._series[i]?.values || [] }));
|
|
2719
|
+
}
|
|
2720
|
+
update(patch = {}) {
|
|
2721
|
+
super.update(patch);
|
|
2722
|
+
if ('data' in patch) this._sync();
|
|
2723
|
+
return this;
|
|
2488
2724
|
}
|
|
2489
2725
|
_legend() { return this._series.length > 1 ? this._seriesLegend(this._series) : null; }
|
|
2490
2726
|
push(values) {
|
|
2491
2727
|
const arr = Array.isArray(values) ? values : [values];
|
|
2492
|
-
const N = this.options.
|
|
2728
|
+
const N = this.options.window || 200;
|
|
2493
2729
|
for (let i = 0; i < this._series.length; i++) {
|
|
2494
2730
|
const v = this._series[i].values;
|
|
2495
2731
|
v.push(arr[i] != null ? +arr[i] : NaN);
|
|
@@ -2498,13 +2734,13 @@ class RealtimeChart extends Chart {
|
|
|
2498
2734
|
if (this._auto) { this._state = null; this._auto = false; }
|
|
2499
2735
|
this._dirty = true;
|
|
2500
2736
|
this._anim.progress = 1;
|
|
2501
|
-
|
|
2737
|
+
schedule(this);
|
|
2502
2738
|
return this;
|
|
2503
2739
|
}
|
|
2504
2740
|
clear() {
|
|
2505
2741
|
for (const s of this._series) s.values.length = 0;
|
|
2506
2742
|
this._dirty = true;
|
|
2507
|
-
|
|
2743
|
+
schedule(this);
|
|
2508
2744
|
return this;
|
|
2509
2745
|
}
|
|
2510
2746
|
_render() {
|
|
@@ -2514,16 +2750,16 @@ class RealtimeChart extends Chart {
|
|
|
2514
2750
|
const on = this._series.map((s, i) => i).filter((i) => !this._isOff(i));
|
|
2515
2751
|
const hasData = this._series.some((s) => s.values.length > 0);
|
|
2516
2752
|
if (!hasData) {
|
|
2517
|
-
const ax = drawCartesian(this, { yMin: 0, yMax:
|
|
2518
|
-
e.text(o.
|
|
2753
|
+
const ax = drawCartesian(this, { yMin: o.min ?? 0, yMax: o.max ?? 100 });
|
|
2754
|
+
e.text(o.emptyText || 'Waiting for data…', ax.px + ax.pw / 2, ax.py + ax.ph / 2, {
|
|
2519
2755
|
fill: t.muted, font: `${t.fontSize}px ${t.font}`, align: 'center', baseline: 'middle',
|
|
2520
2756
|
});
|
|
2521
2757
|
return;
|
|
2522
2758
|
}
|
|
2523
|
-
const [lo, hi] = range(on.map((s) => this._series[s].values.filter((v) => v === v)),
|
|
2524
|
-
const [yLo, yHi] = o
|
|
2525
|
-
const N = o.
|
|
2526
|
-
const ax = drawCartesian(this, { yMin: yLo, yMax: yHi
|
|
2759
|
+
const [lo, hi] = range(on.map((s) => this._series[s].values.filter((v) => v === v)), 0);
|
|
2760
|
+
const [yLo, yHi] = valueDomain(o, lo, hi);
|
|
2761
|
+
const N = o.window || 200;
|
|
2762
|
+
const ax = drawCartesian(this, { yMin: yLo, yMax: yHi });
|
|
2527
2763
|
const xAt = (i, len) => ax.px + ((N - len + i) / (N - 1)) * ax.pw; // newest sample on the right edge
|
|
2528
2764
|
const len = this._series[0].values.length;
|
|
2529
2765
|
this._steps = [];
|
|
@@ -2556,41 +2792,45 @@ class RealtimeChart extends Chart {
|
|
|
2556
2792
|
const x = this._steps[k];
|
|
2557
2793
|
e.poly([{ x, y: ax.py }, { x, y: ax.py + ax.ph }], { stroke: t.muted, lineWidth: 1, dash: [3, 3] });
|
|
2558
2794
|
this._tipNow = { title: `#${k + 1 - len}`, rows };
|
|
2795
|
+
this._hover = { _id: k, index: k, label: k + 1 - len };
|
|
2559
2796
|
this.tooltip.show(x, ax.py + 8, this._tipNow, t);
|
|
2560
2797
|
}
|
|
2561
2798
|
}
|
|
2562
2799
|
}
|
|
2563
2800
|
|
|
2564
2801
|
// ── src/charts/waterfall.js
|
|
2565
|
-
// WaterfallChart — running total
|
|
2566
|
-
|
|
2802
|
+
// WaterfallChart — running total. A row with total: true is drawn from zero to
|
|
2803
|
+
// its value (a subtotal or the end result). Colors: { up, down, total }.
|
|
2567
2804
|
|
|
2568
2805
|
|
|
2569
2806
|
class WaterfallChart extends Chart {
|
|
2570
|
-
get
|
|
2807
|
+
static get meta() {
|
|
2808
|
+
return { id: 'waterfall', name: 'WaterfallChart', aria: 'Waterfall chart', data: 'steps', options: ['labelFormat', 'min', 'max'], colors: 'an object like { up, down, total }' };
|
|
2809
|
+
}
|
|
2571
2810
|
|
|
2572
2811
|
_render() {
|
|
2573
2812
|
const o = this.options;
|
|
2574
|
-
const data =
|
|
2813
|
+
const data = Array.isArray(o.data) ? o.data : [];
|
|
2575
2814
|
if (!data.length) return this._empty();
|
|
2576
2815
|
const e = this.engine;
|
|
2577
2816
|
const t = this.theme;
|
|
2578
2817
|
let cum = 0;
|
|
2579
2818
|
const segs = data.map((d) => {
|
|
2580
2819
|
const v = +d.value || 0;
|
|
2581
|
-
const seg = d.
|
|
2820
|
+
const seg = d.total ? { from: 0, to: v } : { from: cum, to: cum + v };
|
|
2582
2821
|
cum = seg.to;
|
|
2583
|
-
return { ...seg, value: v,
|
|
2822
|
+
return { ...seg, value: v, total: !!d.total, label: d.label };
|
|
2584
2823
|
});
|
|
2585
2824
|
const [lo, hi] = range([segs.map((s) => s.from), segs.map((s) => s.to)], 0, 0);
|
|
2586
|
-
const [yLo, yHi] =
|
|
2587
|
-
const ax = drawCartesian(this, { yMin: yLo, yMax: yHi, xLabels: segs.map((s) => s.label)
|
|
2825
|
+
const [yLo, yHi] = valueDomain(o, lo, hi);
|
|
2826
|
+
const ax = drawCartesian(this, { yMin: yLo, yMax: yHi, xLabels: segs.map((s) => s.label) });
|
|
2588
2827
|
const slot = ax.pw / segs.length;
|
|
2589
2828
|
const barW = Math.max(1, Math.min(60, slot * 0.7));
|
|
2590
2829
|
const prog = this._anim.progress;
|
|
2591
|
-
const
|
|
2592
|
-
const
|
|
2593
|
-
const
|
|
2830
|
+
const c = o.colors || {};
|
|
2831
|
+
const up = c.up || '#10b981';
|
|
2832
|
+
const down = c.down || '#ef4444';
|
|
2833
|
+
const total = c.total || '#6366f1';
|
|
2594
2834
|
|
|
2595
2835
|
for (let i = 0; i < segs.length; i++) {
|
|
2596
2836
|
const s = segs[i];
|
|
@@ -2598,15 +2838,15 @@ class WaterfallChart extends Chart {
|
|
|
2598
2838
|
const y0 = ax.yToPx(s.from);
|
|
2599
2839
|
const h = Math.abs(ax.yToPx(s.to) - y0) * prog;
|
|
2600
2840
|
const top = s.to >= s.from ? y0 - h : y0;
|
|
2601
|
-
e.rect(x, top, barW, Math.max(h, 1), { fill: s.
|
|
2841
|
+
e.rect(x, top, barW, Math.max(h, 1), { fill: s.total ? total : s.value >= 0 ? up : down, radius: t.barRadius });
|
|
2602
2842
|
if (i < segs.length - 1 && prog === 1) {
|
|
2603
2843
|
const yEnd = ax.yToPx(s.to);
|
|
2604
2844
|
e.poly([{ x: x + barW, y: yEnd }, { x: x + slot, y: yEnd }], { stroke: t.muted, lineWidth: 1, dash: [3, 3] });
|
|
2605
2845
|
}
|
|
2606
2846
|
this.hits.add({
|
|
2607
2847
|
_id: i, x, y: top, w: barW, h: Math.max(h, 4), tipX: x + barW / 2, tipY: top,
|
|
2608
|
-
|
|
2609
|
-
tooltip: `${s.label}: ${s.value > 0 && !s.
|
|
2848
|
+
index: i, label: s.label, value: s.value, row: data[i],
|
|
2849
|
+
tooltip: `${this._lab(s.label)}: ${s.value > 0 && !s.total ? '+' : ''}${this._fmt(s.value)}`,
|
|
2610
2850
|
});
|
|
2611
2851
|
}
|
|
2612
2852
|
if (this._hover) {
|
|
@@ -2617,39 +2857,47 @@ class WaterfallChart extends Chart {
|
|
|
2617
2857
|
}
|
|
2618
2858
|
|
|
2619
2859
|
// ── src/charts/composite.js
|
|
2620
|
-
// CompositeChart — bars, areas, lines and
|
|
2621
|
-
//
|
|
2860
|
+
// CompositeChart — bars, areas, lines and dots on one category axis, with an
|
|
2861
|
+
// optional right y axis.
|
|
2622
2862
|
//
|
|
2623
|
-
//
|
|
2863
|
+
// data: { categories, series: [{ type: 'bar'|'line'|'area'|'scatter', name,
|
|
2864
|
+
// values, axis?: 'right', format?, color? }] }
|
|
2865
|
+
// scatter values line up with categories like the others (null = no dot).
|
|
2866
|
+
|
|
2624
2867
|
|
|
2625
2868
|
|
|
2869
|
+
const TYPES = ['bar', 'line', 'area', 'scatter'];
|
|
2626
2870
|
|
|
2627
2871
|
class CompositeChart extends Chart {
|
|
2628
|
-
get
|
|
2872
|
+
static get meta() {
|
|
2873
|
+
return { id: 'composite', name: 'CompositeChart', aria: 'Composite chart', data: 'series', options: ['labelFormat', 'min', 'max', 'xScale'], enums: { xScale: ['category', 'time'] } };
|
|
2874
|
+
}
|
|
2629
2875
|
constructor(container, options) {
|
|
2630
2876
|
super(container, options);
|
|
2631
2877
|
this._follow = true;
|
|
2632
2878
|
}
|
|
2633
2879
|
_legend() {
|
|
2634
|
-
return (this.options.
|
|
2880
|
+
return (this.options.data?.series || []).map((l, i) => ({
|
|
2635
2881
|
label: (l.name || l.type) + (l.axis === 'right' ? ' (R)' : ''),
|
|
2636
2882
|
color: this._colorOf(i, l),
|
|
2637
2883
|
}));
|
|
2638
2884
|
}
|
|
2639
2885
|
_render() {
|
|
2640
2886
|
const o = this.options;
|
|
2641
|
-
const all = o.
|
|
2642
|
-
const cats = o.categories || [];
|
|
2887
|
+
const { categories: cats = [], series: all = [] } = o.data || {};
|
|
2643
2888
|
if (!all.length || !cats.length) return this._empty();
|
|
2644
2889
|
const e = this.engine;
|
|
2645
2890
|
const t = this.theme;
|
|
2891
|
+
all.forEach((l, i) => {
|
|
2892
|
+
if (!TYPES.includes(l.type)) warn(`CompositeChart: series[${i}] type must be ${TYPES.map((x) => `'${x}'`).join(' | ')} — got ${JSON.stringify(l.type)}.`);
|
|
2893
|
+
});
|
|
2646
2894
|
const layers = all.map((l, i) => ({ ...l, i, color: this._colorOf(i, l) })).filter((l) => !this._isOff(l.i));
|
|
2647
|
-
const yOf = (l, v) => (l.type === 'scatter' ? v.y : +v);
|
|
2648
2895
|
|
|
2649
2896
|
const domain = (ll) => {
|
|
2650
2897
|
let mn = 0, mx = -Infinity;
|
|
2651
2898
|
for (const l of ll) for (const v of l.values) {
|
|
2652
|
-
|
|
2899
|
+
if (v == null) continue;
|
|
2900
|
+
const y = +v;
|
|
2653
2901
|
if (y < mn) mn = y;
|
|
2654
2902
|
if (y > mx) mx = y;
|
|
2655
2903
|
}
|
|
@@ -2657,15 +2905,18 @@ class CompositeChart extends Chart {
|
|
|
2657
2905
|
};
|
|
2658
2906
|
const left = layers.filter((l) => l.axis !== 'right');
|
|
2659
2907
|
const rightL = layers.filter((l) => l.axis === 'right');
|
|
2660
|
-
|
|
2908
|
+
// min / max set the left axis; each axis uses the format of its first
|
|
2909
|
+
// series that has one.
|
|
2910
|
+
let [lLo, lHi] = domain(left);
|
|
2911
|
+
if (o.min != null || o.max != null) [lLo, lHi] = valueDomain(o, o.min ?? lLo, o.max ?? lHi);
|
|
2912
|
+
const fmtOf = (ll) => ll.find((l) => l.format)?.format;
|
|
2661
2913
|
const useTime = o.xScale === 'time';
|
|
2662
2914
|
const ax = drawCartesian(this, {
|
|
2663
2915
|
yMin: lLo, yMax: lHi,
|
|
2664
|
-
|
|
2665
|
-
right: rightL.length ? (([a, b]) => ({ yMin: a, yMax: b,
|
|
2916
|
+
yFormat: fmtOf(left),
|
|
2917
|
+
right: rightL.length ? (([a, b]) => ({ yMin: a, yMax: b, yFormat: fmtOf(rightL) }))(domain(rightL)) : null,
|
|
2666
2918
|
xLabels: useTime ? null : cats,
|
|
2667
2919
|
xTime: useTime ? { min: +cats[0], max: +cats[cats.length - 1] } : null,
|
|
2668
|
-
xTickFormat: o.xTickFormat,
|
|
2669
2920
|
});
|
|
2670
2921
|
const yPx = (l) => (l.axis === 'right' && ax.yRToPx ? ax.yRToPx : ax.yToPx);
|
|
2671
2922
|
const xAt = useTime
|
|
@@ -2710,8 +2961,9 @@ class CompositeChart extends Chart {
|
|
|
2710
2961
|
e.unclip();
|
|
2711
2962
|
for (const l of layers.filter((x) => x.type === 'scatter')) {
|
|
2712
2963
|
const toPx = yPx(l);
|
|
2713
|
-
const items =
|
|
2714
|
-
|
|
2964
|
+
const items = [];
|
|
2965
|
+
l.values.forEach((v, i) => { if (v != null) items.push({ x: xAt(i), y: toPx(+v) }); });
|
|
2966
|
+
e.withAlpha(0.8, () => e.circles(items, l.color, t.pointRadius + 1));
|
|
2715
2967
|
}
|
|
2716
2968
|
|
|
2717
2969
|
// Shared tooltip for the category under the pointer.
|
|
@@ -2724,16 +2976,12 @@ class CompositeChart extends Chart {
|
|
|
2724
2976
|
e.withAlpha(0.06, () => e.rect(x - slot / 2, ax.py, slot, ax.ph, { fill: t.text }));
|
|
2725
2977
|
const rows = [];
|
|
2726
2978
|
for (const l of layers) {
|
|
2727
|
-
|
|
2728
|
-
if (
|
|
2729
|
-
|
|
2730
|
-
if (!p) continue;
|
|
2731
|
-
v = p.y;
|
|
2732
|
-
} else v = +l.values[idx];
|
|
2733
|
-
rows.push({ color: l.color, name: l.name || l.type, value: this._fmt(v) });
|
|
2979
|
+
const v = l.values[idx];
|
|
2980
|
+
if (v == null) continue;
|
|
2981
|
+
rows.push({ color: l.color, name: l.name || l.type, value: M.formatNumber(+v, l.format ?? o.format) });
|
|
2734
2982
|
}
|
|
2735
|
-
this._tipNow = { title:
|
|
2736
|
-
this._hover = { _id: idx, index: idx };
|
|
2983
|
+
this._tipNow = { title: this._lab(cats[idx], useTime), rows };
|
|
2984
|
+
this._hover = { _id: idx, index: idx, label: cats[idx] };
|
|
2737
2985
|
this.tooltip.show(x, ax.py + 8, this._tipNow, t);
|
|
2738
2986
|
} else if (this._cursor) {
|
|
2739
2987
|
this.tooltip.hide();
|
|
@@ -2742,8 +2990,7 @@ class CompositeChart extends Chart {
|
|
|
2742
2990
|
}
|
|
2743
2991
|
|
|
2744
2992
|
// ── src/index.js
|
|
2745
|
-
|
|
2746
|
-
// @aeriz/chart — public API (1.x compatible).
|
|
2993
|
+
// @aeriz/chart 2.0 — public API.
|
|
2747
2994
|
//
|
|
2748
2995
|
// Every chart type is its own module, so bundlers keep only the ones you
|
|
2749
2996
|
// import. Without a bundler, import one type directly:
|
|
@@ -2754,42 +3001,50 @@ const math = { TAU, clamp, lerp, extent, niceTicks, niceDomain, timeNiceTicks, t
|
|
|
2754
3001
|
// import $ from 'jquery';
|
|
2755
3002
|
// import { BarChart } from '@aeriz/chart/bar';
|
|
2756
3003
|
// import { install } from '@aeriz/chart/jquery';
|
|
2757
|
-
// install(
|
|
3004
|
+
// install($, { BarChart });
|
|
2758
3005
|
//
|
|
2759
|
-
// $('#chart').aerizChart(
|
|
2760
|
-
// $('#chart').aerizChart('
|
|
2761
|
-
// $('#chart').aerizChart('instance');
|
|
2762
|
-
// $('#chart').aerizChart('destroy');
|
|
3006
|
+
// $('#chart').aerizChart('bar', { data: rows }); // create (chart id)
|
|
3007
|
+
// $('#chart').aerizChart('update', { data: next }); // update · setState · render · destroy
|
|
3008
|
+
// $('#chart').aerizChart('instance'); // the chart object
|
|
2763
3009
|
//
|
|
2764
|
-
//
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
3010
|
+
// The CDN script (window.Aeriz) installs every chart when jQuery is on the page.
|
|
3011
|
+
|
|
3012
|
+
const METHODS = ['update', 'setState', 'render', 'destroy', 'push', 'clear'];
|
|
3013
|
+
|
|
3014
|
+
function install($, charts) {
|
|
3015
|
+
if (!$ || !$.fn) throw new Error('aeriz: install($, { BarChart }) needs jQuery as the first argument');
|
|
3016
|
+
const types = $.fn.aerizChart?.types || {};
|
|
3017
|
+
for (const T of Object.values(charts || {})) if (T && T.meta) types[T.meta.id] = T;
|
|
3018
|
+
|
|
3019
|
+
function aerizChart(cmd, ...args) {
|
|
3020
|
+
if (cmd === 'instance') return this.first().data('aeriz') || null;
|
|
3021
|
+
if (METHODS.includes(cmd)) {
|
|
2774
3022
|
return this.each(function () {
|
|
2775
3023
|
const c = $(this).data('aeriz');
|
|
2776
|
-
if (!c)
|
|
2777
|
-
c[
|
|
2778
|
-
|
|
3024
|
+
if (!c) throw new Error(`aeriz: .aerizChart('${cmd}') — there is no chart here. Create one first, e.g. .aerizChart('bar', { data })`);
|
|
3025
|
+
if (typeof c[cmd] !== 'function') throw new Error(`aeriz: ${c.constructor.meta.name} has no ${cmd}()`);
|
|
3026
|
+
c[cmd](...args);
|
|
3027
|
+
if (cmd === 'destroy') $(this).removeData('aeriz');
|
|
2779
3028
|
});
|
|
2780
3029
|
}
|
|
2781
|
-
const T =
|
|
2782
|
-
if (
|
|
3030
|
+
const T = types[cmd];
|
|
3031
|
+
if (!T) {
|
|
3032
|
+
const ids = Object.keys(types);
|
|
3033
|
+
throw new Error(typeof cmd === 'function'
|
|
3034
|
+
? `aeriz 2.0: pass the chart id, not the class — install($, { ${cmd.meta?.name || 'BarChart'} }) then .aerizChart('${cmd.meta?.id || 'bar'}', options)`
|
|
3035
|
+
: `aeriz: unknown chart '${cmd}'. ${ids.length ? `Installed: ${ids.join(', ')}` : 'Call install($, { BarChart }) first'}.`);
|
|
3036
|
+
}
|
|
2783
3037
|
return this.each(function () {
|
|
2784
3038
|
$(this).data('aeriz')?.destroy();
|
|
2785
|
-
$(this).data('aeriz', new T(this,
|
|
3039
|
+
$(this).data('aeriz', new T(this, args[0] || {}));
|
|
2786
3040
|
});
|
|
2787
|
-
}
|
|
3041
|
+
}
|
|
3042
|
+
aerizChart.types = types;
|
|
3043
|
+
$.fn.aerizChart = aerizChart;
|
|
2788
3044
|
return $;
|
|
2789
3045
|
}
|
|
2790
3046
|
|
|
2791
|
-
|
|
2792
|
-
const Aeriz = { Chart, getFPS, getActiveCount, scheduler, toRows, Engine, drawCartesian, math, PALETTE, BarChart, HBarChart, LineChart, AreaChart, StackedBarChart, GroupedBarChart, MultiLineChart, ScatterChart, BubbleChart, PieChart, RadarChart, PolarChart, GaugeChart, HeatmapChart, BoxplotChart, FunnelChart, TreemapChart, Sparkline, RealtimeChart, WaterfallChart, CompositeChart, jquery: ($) => install($, Aeriz) };
|
|
3047
|
+
const Aeriz = { Chart, PALETTE, BarChart, HBarChart, LineChart, AreaChart, StackedBarChart, GroupedBarChart, MultiLineChart, ScatterChart, BubbleChart, PieChart, RadarChart, PolarChart, GaugeChart, HeatmapChart, BoxplotChart, FunnelChart, TreemapChart, SparklineChart, RealtimeChart, WaterfallChart, CompositeChart };
|
|
2793
3048
|
if (typeof window !== 'undefined' && window.jQuery) install(window.jQuery, Aeriz);
|
|
2794
3049
|
return Aeriz;
|
|
2795
3050
|
})();
|