@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.
Files changed (45) hide show
  1. package/README.md +60 -34
  2. package/dist/aeriz.cjs +609 -354
  3. package/dist/aeriz.esm.js +578 -331
  4. package/dist/aeriz.esm.min.js +119 -105
  5. package/dist/aeriz.js +609 -354
  6. package/dist/aeriz.min.cjs +123 -107
  7. package/dist/aeriz.min.js +123 -107
  8. package/dist/esm/base.js +33 -22
  9. package/dist/esm/cartesian.js +7 -5
  10. package/dist/esm/charts/area.js +6 -6
  11. package/dist/esm/charts/bar.js +3 -3
  12. package/dist/esm/charts/boxplot.js +3 -3
  13. package/dist/esm/charts/bubble.js +3 -3
  14. package/dist/esm/charts/composite.js +8 -8
  15. package/dist/esm/charts/funnel.js +3 -3
  16. package/dist/esm/charts/gauge.js +5 -5
  17. package/dist/esm/charts/grouped-bar.js +4 -4
  18. package/dist/esm/charts/hbar.js +5 -5
  19. package/dist/esm/charts/heatmap.js +6 -6
  20. package/dist/esm/charts/line.js +6 -6
  21. package/dist/esm/charts/multi-line.js +5 -5
  22. package/dist/esm/charts/pie.js +6 -6
  23. package/dist/esm/charts/polar.js +3 -3
  24. package/dist/esm/charts/radar.js +6 -6
  25. package/dist/esm/charts/realtime.js +11 -9
  26. package/dist/esm/charts/scatter.js +4 -4
  27. package/dist/esm/charts/sparkline.js +4 -4
  28. package/dist/esm/charts/stacked-bar.js +5 -5
  29. package/dist/esm/charts/treemap.js +4 -4
  30. package/dist/esm/charts/waterfall.js +4 -4
  31. package/dist/esm/engine.js +1 -1
  32. package/dist/esm/index.js +2 -2
  33. package/dist/esm/jquery.js +5 -4
  34. package/dist/esm/math.js +1 -1
  35. package/dist/esm/react.js +4 -4
  36. package/dist/esm/shallow.js +4 -2
  37. package/dist/esm/svelte.js +4 -4
  38. package/dist/esm/vue.js +4 -2
  39. package/dist/index.d.ts +228 -552
  40. package/dist/jquery.d.ts +19 -8
  41. package/dist/react.d.ts +55 -18
  42. package/dist/svelte.d.ts +53 -14
  43. package/dist/types/sparkline.d.ts +1 -1
  44. package/dist/vue.d.ts +60 -10
  45. package/package.json +2 -2
package/dist/aeriz.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/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.invalidate(); });
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
- // Plain arrays and objects work wherever `{ label, value }` rows are expected:
737
- // [3, 7, 4] → labels 1, 2, 3
738
- // { Jan: 3, Feb: 7 } → labels Jan, Feb
739
- // [['Jan', 3], …] → labels Jan, …
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 (!data) return [];
742
- if (!Array.isArray(data)) {
743
- return typeof data === 'object' ? Object.keys(data).map((k) => ({ label: k, value: +data[k] })) : [];
744
- }
745
- const first = data[0];
746
- if (typeof first === 'number') return data.map((v, i) => ({ label: String(i + 1), value: v }));
747
- if (Array.isArray(first)) return data.map((d) => ({ label: d[0], value: +d[1] }));
748
- return data;
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
- // Re-read theme + palette (after changing CSS variables) and redraw.
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 ('palette' in patch) this.palette = patch.palette || readPalette(this.container);
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
- setData(data) { return this.update({ data }); }
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
- setLoading(msg) { return this._setState('loading', msg || ''); }
834
- setEmpty(msg) { return this._setState('empty', msg || 'No data'); }
835
- setError(msg) { return this._setState('error', msg || 'Error'); }
836
- clearState() { return this._setState(null); }
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
- _setState(kind, msg) {
854
- this._state = kind;
855
- this._stateMsg = msg;
856
- this._auto = false;
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 setEmpty() it
863
- // clears itself on the next update()/setData().
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
- const o = this.options;
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
- const hit = this.hits.find(p.x, p.y) || (this._follow ? this._hover : null);
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.options.onClick?.(this._hover, this._cursor, 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', yTickFormat?,
1129
- // xLabels?: string[] | xTime?: {min,max} | xLinear?: {min,max}, xTickFormat?,
1130
- // right?: { yMin, yMax, yTickFormat? }, pad?: {l,r,t,b} }
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.yTickFormat || ((v) => chart._tickFmt(v));
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.yTickFormat || ((v) => chart._tickFmt(v));
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 = spec.xTickFormat || localTimeFormat(step);
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 = spec.xTickFormat || ((v) => chart._tickFmt(v));
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 = spec.xTickFormat || ((i) => String(spec.xLabels[i]));
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, 0), font), e.measure(xFmt(n - 1, n - 1), font)) + 6;
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, 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 _kind() { return 'Bar chart'; } // aria-label default (bundlers minify class names)
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 = o.label || ((d) => d.label);
1267
- const valueOf = o.value || ((d) => d.value);
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] = M.niceDomain(lo, hi, 5);
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: data.map((d, i) => labelOf(d, i)),
1274
- yTickFormat: o.yTickFormat,
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(o.color ? 0 : i, data[i].color ? data[i] : { color: o.color });
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: `${labelOf(data[i], i)}: ${this._fmt(v)}`,
1299
- data: data[i], index: i, value: v,
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 _kind() { return 'Horizontal bar chart'; } // aria-label default (bundlers minify class names)
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 = o.label || ((d) => d.label);
1332
- const valueOf = o.value || ((d) => d.value);
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 labels = data.map((d, i) => String(labelOf(d, i)));
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] = M.niceDomain(lo, hi, 5);
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(xFmt(v), xToPx(v), py + ph + 6, { fill: t.muted, font, align: 'center', baseline: 'top' });
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(o.color ? 0 : i, data[i].color ? data[i] : { color: o.color });
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
- data: data[i], index: i, value: v,
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 _kind() { return 'Line chart'; } // aria-label default (bundlers minify class names)
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 = o.label || LABEL;
1412
- const valueOf = o.value || VALUE;
1413
- const n = data.length;
1592
+ const labelOf = this._fn('label') || LABEL;
1593
+ const valueOf = this._fn('value') || VALUE;
1414
1594
 
1415
- // Values are cached per data array so hover redraws don't re-map 1M rows.
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 values = new Float64Array(n);
1419
- for (let i = 0; i < n; i++) values[i] = +valueOf(data[i], i);
1420
- c = this._cache = { data: o.data, valueOf, labelOf, values, idx: null, idxW: 0, xs: null, labels: null, range: null };
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 [lo, hi] = (c.range ||= range([values], o.yScale === 'log' ? Infinity : 0));
1424
- const [yLo, yHi] = o.yScale === 'log' ? [lo, hi] : M.niceDomain(lo, hi, 5);
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
- yMin: yLo, yMax: yHi,
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 ||= data.map((d, i) => labelOf(d, i));
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 = o.color || this.palette[0];
1438
- const interp = o.interpolation || 'smooth';
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 if (interp.startsWith('step')) {
1474
- e.step(points, interp === 'step' ? 'before' : interp.split('-')[1], { stroke: color, lineWidth: lineW });
1660
+ } else {
1661
+ e.step(points, 'before', { stroke: color, lineWidth: lineW });
1475
1662
  }
1476
1663
  e.unclip();
1477
1664
 
1478
- const tip = (i) => `${useTime ? new Date(xs[i]).toLocaleString() : labelOf(data[i], i)}: ${this._fmt(values[i])}`;
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), data: data[p.i], index: p.i, value: values[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), data: data[p.i], index: p.i, value: values[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 _kind() { return 'Area chart'; } // aria-label default (bundlers minify class names)
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.series || [];
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] = M.niceDomain(yMin, yMax, 5);
1541
- const ax = drawCartesian(this, { yMin: yLo, yMax: yHi, xLabels: cats, yTickFormat: o.yTickFormat, xTickFormat: o.xTickFormat });
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: String(cats[i]),
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 _kind() { return 'Stacked bar chart'; } // aria-label default (bundlers minify class names)
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.series || [];
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] = M.niceDomain(lo, hi, 5);
1623
- const ax = drawCartesian(this, { yMin: yLo, yMax: yHi, xLabels: cats, yTickFormat: o.yTickFormat, xTickFormat: o.xTickFormat });
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: String(cats[i]), rows: [{ color, name: series[s].name || 'Series ' + (s + 1), value: this._fmt(v) }] },
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 _kind() { return 'Grouped bar chart'; } // aria-label default (bundlers minify class names)
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.series || [];
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] = M.niceDomain(lo, hi, 5);
1688
- const ax = drawCartesian(this, { yMin: yLo, yMax: yHi, xLabels: cats, yTickFormat: o.yTickFormat, xTickFormat: o.xTickFormat });
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: String(cats[i]), rows: [{ color, name: series[s].name || 'Series ' + (s + 1), value: this._fmt(v) }] },
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 _kind() { return 'Multi line chart'; } // aria-label default (bundlers minify class names)
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.series || [];
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.interpolation || 'smooth';
1744
- const on = series.map((s, i) => i).filter((i) => !this._isOff(i) && !series[i].hidden);
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] = M.niceDomain(lo, hi, 5);
1937
+ const [yLo, yHi] = valueDomain(o, lo, hi);
1748
1938
  const useTime = o.xScale === 'time';
1749
- const spec = { yMin: yLo, yMax: yHi, yTickFormat: o.yTickFormat, xTickFormat: o.xTickFormat };
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, interp === 'step' ? 'before' : interp.split('-')[1], { stroke: color, lineWidth: lineW });
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 = useTime ? new Date(+cats[idx]).toLocaleString() : String(cats[idx]);
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 batched per color/radius; `group` picks the color.
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 _kind() { return 'Scatter chart'; } // aria-label default (bundlers minify class names)
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] = M.niceDomain(yMin, yMax, 5);
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.r || defaultR) * prog;
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, data: d, index: i,
1851
- tooltip: `(${this._fmt(d.x)}, ${this._fmt(d.y)})${d.label ? ` · ${d.label}` : ''}`,
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 `z` sets the bubble area.
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 _kind() { return 'Bubble chart'; } // aria-label default (bundlers minify class names)
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.z || 0) > zMax) zMax = d.z;
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] = M.niceDomain(yMin - py, yMax + py, 5);
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.maxRadius || 32, minR = o.minRadius || 4;
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.z || 0) / Math.max(1e-9, zMax)) * (maxR - minR)) * prog;
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, data: d, index: i,
1906
- tooltip: `${d.label ? d.label + ' ' : ''}(${this._fmt(d.x)}, ${this._fmt(d.y)}) · z=${this._fmt(d.z)}`,
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 a center label.
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 _kind() { return 'Pie chart'; } // aria-label default (bundlers minify class names)
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 = o.label || ((d) => d.label);
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 = o.label || ((d) => d.label);
1944
- const valueOf = o.value || ((d) => d.value);
1945
- const vals = data.map((d, i) => (this._isOff(i) ? 0 : Math.max(0, +valueOf(d, i) || 0)));
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
- const innerR = ir == null || ir === false ? 0 : typeof ir === 'number' ? (ir <= 1 ? R * ir : ir) : R * 0.55;
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
- data: data[i], index: i, value: v,
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.centerLabel !== false) {
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.totalLabel || this._fmt(total);
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 _kind() { return 'Radar chart'; } // aria-label default (bundlers minify class names)
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.series || [];
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
- for (const a of axes) labelW = Math.max(labelW, e.measure(String(a), font));
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(String(axes[i]), cx + Math.cos(a) * (R + 10), cy + Math.sin(a) * (R + 10), {
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: String(axes[i]), rows: [{ color, name: ser.name || 'Series ' + (s + 1), value: this._fmt(v) }] },
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 _kind() { return 'Polar chart'; } // aria-label default (bundlers minify class names)
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 = o.label || ((d) => d.label);
2088
- const valueOf = o.value || ((d) => d.value);
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
- data: data[i], index: i, value: v,
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; setValue(v) tweens from the current value.
2332
+ // GaugeChart — animated arc; update({ data }) tweens from the shown value.
2128
2333
 
2129
2334
  class GaugeChart extends Chart {
2130
- get _kind() { return 'Gauge chart'; } // aria-label default (bundlers minify class names)
2131
- setValue(v) {
2132
- this._from = this._shown ?? 0;
2133
- return this.update({ value: v });
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 || 0;
2140
- const max = o.max != null ? o.max : 100;
2141
- const target = Math.max(min, Math.min(max, +o.value || 0));
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 = o.color || this.palette[0];
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.label) {
2160
- e.text(o.label, cx, cy + R * 0.22, { fill: t.muted, font: `${t.fontSize}px ${t.font}`, align: 'center', baseline: 'top' });
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.ariaLabel || o.label || o.title || 'Gauge'}: ${txt}`);
2372
+ this.canvas.setAttribute('aria-label', `${o.title || 'Gauge'}: ${txt}`);
2163
2373
  }
2164
2374
  }
2165
2375
 
2166
2376
  // ── src/charts/heatmap.js
2167
- // HeatmapChart — cells colored between colorLow and colorHigh.
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 _kind() { return 'Heatmap chart'; } // aria-label default (bundlers minify class names)
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 data = o.data || []; // [{ x, y, value }]
2176
- const xLabels = o.xLabels || [];
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
- for (const s of yLabels) labelW = Math.max(labelW, e.measure(String(s), font));
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 d of data) {
2190
- if (d.value > vMax) vMax = d.value;
2191
- if (d.value < vMin) vMin = d.value;
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 lo = o.colorLow || M.colorAlpha(this.palette[0], 0.08);
2196
- const hi = o.colorHigh || this.palette[0];
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
- const xi = new Map(xLabels.map((l, i) => [l, i]));
2200
- const yi = new Map(yLabels.map((l, i) => [l, i]));
2201
-
2202
- for (let k = 0; k < data.length; k++) {
2203
- const d = data[k];
2204
- const cx = typeof d.x === 'number' ? d.x : xi.get(d.x) ?? -1;
2205
- const cy = typeof d.y === 'number' ? d.y : yi.get(d.y) ?? -1;
2206
- if (cx < 0 || cy < 0) continue;
2207
- const x = px + cx * cellW;
2208
- const y = py + cy * cellH;
2209
- const n = vMax > vMin ? (d.value - vMin) / (vMax - vMin) : 0.5;
2210
- e.rect(x + gap, y + gap, cellW - gap * 2, cellH - gap * 2, { fill: M.colorMix(lo, hi, n * prog), radius: gap ? 2 : 0 });
2211
- this.hits.add({
2212
- _id: k, x, y, w: cellW, h: cellH, tipX: x + cellW / 2, tipY: y, data: d, index: k, value: d.value,
2213
- tooltip: `${xLabels[cx]} · ${yLabels[cy]}: ${this._fmt(d.value)}`,
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 = String(xLabels[i]);
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(String(yLabels[i]), px - 6, py + (i + 0.5) * cellH, { fill: t.muted, font, align: 'right', baseline: 'middle' });
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 _kind() { return 'Boxplot chart'; } // aria-label default (bundlers minify class names)
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] = M.niceDomain(yMin, yMax, 5);
2259
- const ax = drawCartesian(this, { yMin: yLo, yMax: yHi, xLabels: data.map((d) => d.label), yTickFormat: o.yTickFormat });
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), data: d, index: i,
2498
+ tipX: cx, tipY: ax.yToPx(d.q3), index: i, label: d.label, value: d.median, row: d,
2283
2499
  tooltip: {
2284
- title: String(d.label),
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 _kind() { return 'Funnel chart'; } // aria-label default (bundlers minify class names)
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 = o.label || ((d) => d.label);
2306
- const valueOf = o.value || ((d) => d.value);
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, data: data[i], index: i, value: v,
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 _kind() { return 'Treemap chart'; } // aria-label default (bundlers minify class names)
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 = o.label || ((d) => d.label);
2395
- const valueOf = o.value || ((d) => d.value);
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, data: it.data, index: it._id, value: it.value,
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
- // Sparkline — tiny trend line, no axes. Accepts numbers or rows.
2645
+ // SparklineChart — tiny trend line, no axes. Same data as LineChart.
2426
2646
 
2427
2647
 
2428
2648
 
2429
- class Sparkline extends Chart {
2430
- get _kind() { return 'Sparkline'; } // aria-label default (bundlers minify class names)
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 = o.value || ((v) => (typeof v === 'object' && v ? +v.value : +v));
2439
- const values = data.map((d, i) => valueOf(d, i));
2440
- const [yMin, yMax] = range([values]);
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 = o.color || this.palette[0];
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
- this.hits.add({ _id: i, x: l, y: 0, w: r - l, h: e.h, tipX: p.x, tipY: p.y - 4, index: i, value: values[i], tooltip: this._fmt(values[i]) });
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: push() a sample.
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 _kind() { return 'Realtime chart'; } // aria-label default (bundlers minify class names)
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.options.windowSize = options.windowSize || 200;
2485
- this._series = (options.series || [{ name: 'Series' }]).map((s) => ({ name: s.name || 'Series', color: s.color, values: [] }));
2711
+ this._series = [];
2712
+ this._sync();
2486
2713
  this._follow = true;
2487
- this._streaming = true;
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.windowSize;
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
- scheduler.schedule(this);
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
- scheduler.schedule(this);
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: 100, yTickFormat: o.yTickFormat });
2518
- e.text(o.waitingText || 'streaming…', ax.px + ax.pw / 2, ax.py + ax.ph / 2, {
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)), o.yMin != null ? o.yMin : 0);
2524
- const [yLo, yHi] = o.yMax != null ? [o.yMin || 0, o.yMax] : M.niceDomain(lo, hi, 5);
2525
- const N = o.windowSize;
2526
- const ax = drawCartesian(this, { yMin: yLo, yMax: yHi, yTickFormat: o.yTickFormat });
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; rows with isTotal reset the bar to zero.
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 _kind() { return 'Waterfall chart'; } // aria-label default (bundlers minify class names)
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 = toRows(o.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.isTotal ? { from: 0, to: v } : { from: cum, to: cum + v };
2820
+ const seg = d.total ? { from: 0, to: v } : { from: cum, to: cum + v };
2582
2821
  cum = seg.to;
2583
- return { ...seg, value: v, isTotal: !!d.isTotal, label: d.label };
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] = M.niceDomain(lo, hi, 5);
2587
- const ax = drawCartesian(this, { yMin: yLo, yMax: yHi, xLabels: segs.map((s) => s.label), yTickFormat: o.yTickFormat });
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 up = o.colorUp || '#10b981';
2592
- const down = o.colorDown || '#ef4444';
2593
- const total = o.colorTotal || '#6366f1';
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.isTotal ? total : s.value >= 0 ? up : down, radius: t.barRadius });
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
- data: data[i], index: i, value: s.value,
2609
- tooltip: `${s.label}: ${s.value > 0 && !s.isTotal ? '+' : ''}${this._fmt(s.value)}`,
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 scatter on one category axis,
2621
- // with an optional right y axis (layer.axis = 'right').
2860
+ // CompositeChart — bars, areas, lines and dots on one category axis, with an
2861
+ // optional right y axis.
2622
2862
  //
2623
- // Layer: { type: 'bar'|'line'|'area'|'scatter', name, values, axis?, color?, radius? }
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 _kind() { return 'Composite chart'; } // aria-label default (bundlers minify class names)
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.layers || []).map((l, i) => ({
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.layers || [];
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
- const y = yOf(l, v);
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
- const [lLo, lHi] = domain(left);
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
- yTickFormat: o.yLeftTickFormat || o.yTickFormat,
2665
- right: rightL.length ? (([a, b]) => ({ yMin: a, yMax: b, yTickFormat: o.yRightTickFormat }))(domain(rightL)) : null,
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 = l.values.map((p) => ({ x: xAt(typeof p.x === 'number' ? p.x : cats.indexOf(p.x)), y: toPx(p.y) }));
2714
- e.withAlpha(0.8, () => e.circles(items, l.color, l.radius || t.pointRadius + 1));
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
- let v;
2728
- if (l.type === 'scatter') {
2729
- const p = l.values.find((p) => (typeof p.x === 'number' ? p.x : cats.indexOf(p.x)) === idx);
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: useTime ? new Date(+cats[idx]).toLocaleString() : String(cats[idx]), rows };
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
- const math = { TAU, clamp, lerp, extent, niceTicks, niceDomain, timeNiceTicks, timeFormat, minMaxDecimate, minMaxDecimateIdx, shouldDecimate, bezierControlPoints, springStep, colorParse, colorAlpha, colorMix, PALETTE, formatNumber };
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(BarChart, { data: rows }); // create
2760
- // $('#chart').aerizChart('setData', rows); // call any method
2761
- // $('#chart').aerizChart('instance'); // the chart object
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
- // With the CDN script (window.Aeriz) the plugin installs itself when jQuery
2765
- // is on the page, and types can be given by name: .aerizChart('BarChart', …)
2766
-
2767
- function install($, registry) {
2768
- if (!$ || !$.fn) throw new Error('aeriz: install($) needs jQuery');
2769
- $.fn.aerizChart = function (type, options) {
2770
- const first = this.first().data('aeriz');
2771
- if (type === 'instance') return first;
2772
- if (typeof type === 'string' && first && typeof first[type] === 'function') {
2773
- const args = [].slice.call(arguments, 1);
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) return;
2777
- c[type](...args);
2778
- if (type === 'destroy') $(this).removeData('aeriz');
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 = typeof type === 'string' ? registry && registry[type] : type;
2782
- if (typeof T !== 'function') throw new Error(`aeriz: unknown chart type ${type}`);
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, options || {}));
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
  })();