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