@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
@@ -1,5 +1,5 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */const TAU=Math.PI*2;const clamp=(v,lo,hi)=>v<lo?lo:v>hi?hi:v;const lerp=(a,b,t)=>a+(b-a)*t;function extent(arr,fn=(x)=>x){let mn=Infinity,mx=-Infinity;for(let i=0;i<arr.length;i++){const v=fn(arr[i],i);if(v<mn)mn=v;if(v>mx)mx=v;}
1
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */const TAU=Math.PI*2;const clamp=(v,lo,hi)=>v<lo?lo:v>hi?hi:v;const lerp=(a,b,t)=>a+(b-a)*t;function extent(arr,fn=(x)=>x){let mn=Infinity,mx=-Infinity;for(let i=0;i<arr.length;i++){const v=fn(arr[i],i);if(v<mn)mn=v;if(v>mx)mx=v;}
3
3
  return[mn===Infinity?0:mn,mx===-Infinity?1:mx];}
4
4
  function niceTicks(min,max,count=5){if(min===max){min-=0.5;max+=0.5;}
5
5
  const span=max-min;const step0=span/Math.max(1,count);const mag=Math.pow(10,Math.floor(Math.log10(step0)));const norm=step0/mag;let step;if(norm<1.5)step=mag;else if(norm<3)step=2*mag;else if(norm<7)step=5*mag;else step=10*mag;const lo=Math.ceil(min/step-1e-9)*step;const hi=Math.floor(max/step+1e-9)*step;const ticks=[];for(let i=0,v=lo;v<=hi+step/2;v=lo+ ++i*step){ticks.push(+v.toPrecision(12));}
@@ -62,13 +62,10 @@ this._gradCache.set(key,g);return g;}
62
62
  clip(x,y,w,h){const ctx=this.ctx;ctx.save();ctx.beginPath();ctx.rect(x,y,w,h);ctx.clip();}
63
63
  unclip(){this.ctx.restore();this._font=null;}
64
64
  withAlpha(a,fn){const ctx=this.ctx;const prev=ctx.globalAlpha;ctx.globalAlpha=prev*a;fn();ctx.globalAlpha=prev;}}
65
- const M={TAU,clamp,lerp,extent,niceTicks,niceDomain,timeNiceTicks,timeFormat,minMaxDecimate,minMaxDecimateIdx,shouldDecimate,bezierControlPoints,springStep,colorParse,colorAlpha,colorMix,PALETTE,formatNumber};const charts=new Set();let rafId=null;let fps=60;let frames=0;let fpsT=0;function tick(now){if(now-fpsT>500){fps=Math.round((1000*frames)/(now-fpsT));frames=0;fpsT=now;}
66
- frames++;let active=false;for(const c of charts){try{if(c._tick(now))active=true;}catch(e){console.error(e);}}
65
+ const M={TAU,clamp,lerp,extent,niceTicks,niceDomain,timeNiceTicks,timeFormat,minMaxDecimate,minMaxDecimateIdx,shouldDecimate,bezierControlPoints,springStep,colorParse,colorAlpha,colorMix,PALETTE,formatNumber};const charts=new Set();let rafId=null;function tick(now){let active=false;for(const c of charts){try{if(c._tick(now))active=true;}catch(e){console.error(e);}}
67
66
  rafId=charts.size>0&&active?requestAnimationFrame(tick):null;}
68
- function schedule(chart){if(chart._destroyed)return;charts.add(chart);if(rafId===null){fpsT=performance.now();rafId=requestAnimationFrame(tick);}}
69
- const scheduler={schedule,fps:()=>fps,count:()=>charts.size};function getFPS(){return fps;}
70
- function getActiveCount(){return charts.size;}
71
- const mq=(q)=>(typeof matchMedia==='function'?matchMedia(q):null);const reducedMotion=mq('(prefers-reduced-motion: reduce)');const darkScheme=mq('(prefers-color-scheme: dark)');darkScheme?.addEventListener?.('change',()=>{for(const c of charts)c.invalidate();});const TOKENS=['bg','surface','text','muted','grid','axis','accent','font','fontSize','lineWidth','pointRadius','barRadius','tooltipBg','tooltipFg'];const cssVar=(k)=>'--aeriz-'+k.replace(/[A-Z]/g,(c)=>'-'+c.toLowerCase());function backgroundOf(el){for(let n=el;n&&n.nodeType===1;n=n.parentElement){const c=getComputedStyle(n).backgroundColor;if(c&&M.colorParse(c)[3]>0.5)return c;}
67
+ function schedule(chart){if(chart._destroyed)return;charts.add(chart);if(rafId===null)rafId=requestAnimationFrame(tick);}
68
+ const mq=(q)=>(typeof matchMedia==='function'?matchMedia(q):null);const reducedMotion=mq('(prefers-reduced-motion: reduce)');const darkScheme=mq('(prefers-color-scheme: dark)');darkScheme?.addEventListener?.('change',()=>{for(const c of charts){c._theme();schedule(c);}});const TOKENS=['bg','surface','text','muted','grid','axis','accent','font','fontSize','lineWidth','pointRadius','barRadius','tooltipBg','tooltipFg'];const cssVar=(k)=>'--aeriz-'+k.replace(/[A-Z]/g,(c)=>'-'+c.toLowerCase());function backgroundOf(el){for(let n=el;n&&n.nodeType===1;n=n.parentElement){const c=getComputedStyle(n).backgroundColor;if(c&&M.colorParse(c)[3]>0.5)return c;}
72
69
  return'#ffffff';}
73
70
  function resolveTheme(el){const cs=getComputedStyle(el);const v={};for(const k of TOKENS)v[k]=cs.getPropertyValue(cssVar(k)).trim();const text=v.text||cs.color||'#222';const num=(s,d)=>(s&&!isNaN(parseFloat(s))?parseFloat(s):d);return{bg:v.bg||backgroundOf(el),surface:v.surface||'transparent',text,muted:v.muted||M.colorAlpha(text,0.62),grid:v.grid||M.colorAlpha(text,0.09),axis:v.axis||M.colorAlpha(text,0.3),accent:v.accent||'#64c8ff',font:v.font||cs.fontFamily||'system-ui, sans-serif',fontSize:num(v.fontSize,11),lineWidth:num(v.lineWidth,2),pointRadius:num(v.pointRadius,3),barRadius:num(v.barRadius,3),tooltipBg:v.tooltipBg||'rgba(20,22,30,0.94)',tooltipFg:v.tooltipFg||'#fff',};}
74
71
  function readPalette(el){const cs=getComputedStyle(el);const out=[];for(let i=1;i<=12;i++){const v=(cs.getPropertyValue(`--aeriz-color-${i}`)||'').trim();if(v)out.push(v);}
@@ -90,25 +87,38 @@ clear(){this.regions.length=0;}
90
87
  add(r){this.regions.push(r);}
91
88
  find(x,y){for(let i=this.regions.length-1;i>=0;i--){const r=this.regions[i];if(r.test?r.test(x,y):x>=r.x&&x<=r.x+r.w&&y>=r.y&&y<=r.y+r.h)return r;}
92
89
  return null;}}
93
- function toRows(data){if(!data)return[];if(!Array.isArray(data)){return typeof data==='object'?Object.keys(data).map((k)=>({label:k,value:+data[k]})):[];}
94
- const first=data[0];if(typeof first==='number')return data.map((v,i)=>({label:String(i+1),value:v}));if(Array.isArray(first))return data.map((d)=>({label:d[0],value:+d[1]}));return data;}
95
- class Chart{constructor(container,options={}){const el=typeof container==='string'?document.querySelector(container):container;if(!el)throw new Error(`aeriz: container not found${typeof container === 'string' ? ': ' + container : ''}`);this.container=el;this.options=options;this._restore={position:el.style.position,height:el.style.height};el.classList.add('aeriz');if(getComputedStyle(el).position==='static')el.style.position='relative';if(!el.style.height&&el.offsetHeight===0)el.style.height='320px';this.wrap=div(el,'display:flex;flex-direction:column;width:100%;height:100%');this.legendEl=div(this.wrap,'display:none;flex:none;flex-wrap:wrap;gap:2px 12px;padding:0 0 6px;font:inherit;font-size:11px');this.plot=div(this.wrap,'position:relative;flex:1 1 auto;min-height:0');this.canvas=document.createElement('canvas');this.canvas.style.cssText='position:absolute;inset:0;display:block;width:100%;height:100%;touch-action:pan-y;outline:none';this.canvas.tabIndex=0;this.canvas.setAttribute('role','img');this.plot.appendChild(this.canvas);this.live=div(this.plot,'position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap');this.live.setAttribute('aria-live','polite');this.engine=new Engine(this.canvas);this.tooltip=new Tooltip(this.plot);this.overlay=new Overlay(this.plot);this.hits=new HitIndex();this.theme=resolveTheme(el);this.palette=options.palette||readPalette(el);this._off=new Set();this._legendKey='';this._cursor=null;this._hover=null;this._dirty=true;this._destroyed=false;this._visible=typeof IntersectionObserver==='undefined';this._anim={progress:0,start:0,duration:600};this._state=null;this._auto=false;this._label();this._setupEvents();this._observe();this._resize();if(options.animate===false||reducedMotion?.matches)this._anim.progress=1;schedule(this);}
96
- invalidate(){this.theme=resolveTheme(this.container);this.palette=this.options.palette||readPalette(this.container);this.engine._gradCache.clear();this._legendKey='';this._dirty=true;schedule(this);}
97
- update(patch={}){Object.assign(this.options,patch);this._cache=null;if('palette'in patch)this.palette=patch.palette||readPalette(this.container);if(this._auto){this._state=null;this._auto=false;}
90
+ const isList=(d)=>Array.isArray(d)||ArrayBuffer.isView(d);const isObj=(d)=>d!=null&&typeof d==='object'&&!isList(d);function toRows(data){if(isList(data)){if(!data.length||Array.isArray(data[0]))return[];return typeof data[0]==='number'?Array.from(data,(v,i)=>({label:String(i+1),value:v})):data;}
91
+ return isObj(data)?Object.keys(data).map((k)=>({label:k,value:+data[k]})):[];}
92
+ const said=new Set();function warn(msg){if(said.has(msg))return;said.add(msg);console.warn('[aeriz] '+msg);}
93
+ const COMMON=['data','title','colors','format','legend','animate','emptyText','onClick','onLegend'];const SHAPES={rows:'an array of numbers, an object like { Jan: 64 }, or rows like [{ label, value }]',series:'{ categories: [...], series: [{ name, values: [...] }] }',stream:'{ series: [{ name }] } (values arrive with chart.push())',points:'points like [{ x, y }]',matrix:'{ x: [...], y: [...], values: [[...], ...] } with values[y][x]',boxes:'rows like [{ label, min, q1, median, q3, max }]',steps:'rows like [{ label, value }] (add total: true for a total bar)',number:'a number',};const DATA='data: { categories, series }';const RENAMED={ariaLabel:'title',palette:'colors',color:'colors',xTickFormat:'labelFormat (category axis) or format (value axis)',yTickFormat:'format',yLeftTickFormat:'format on the series',yRightTickFormat:'format on the series',categories:DATA,series:DATA,layers:DATA,axes:DATA,xLabels:'data: { x, y, values }',yLabels:'data: { x, y, values }',interpolation:'curve',windowSize:'window',yMin:'min',yMax:'max',waitingText:'emptyText',colorLow:'colors: { low, high }',colorHigh:'colors: { low, high }',colorUp:'colors: { up, down, total }',colorDown:'colors: { up, down, total }',colorTotal:'colors: { up, down, total }',minRadius:'radius: [min, max]',maxRadius:'radius: [min, max]',centerLabel:'center',totalLabel:'center',type:'the chart class itself',};const FUNCS=['label','value','labelFormat','onClick','onLegend'];function distance(a,b){const d=Array.from({length:b.length+1},(_,j)=>j);for(let i=1;i<=a.length;i++){let p=d[0];d[0]=i;for(let j=1;j<=b.length;j++){const t=d[j];d[j]=Math.min(d[j]+1,d[j-1]+1,p+(a[i-1]===b[j-1]?0:1));p=t;}}
94
+ return d[b.length];}
95
+ function checkOptions(meta,o){const name=meta.name;const allowed=[...COMMON,...meta.options];for(const k of Object.keys(o)){const v=o[k];if(!allowed.includes(k)){const hint=meta.renamed?.[k]||RENAMED[k];if(hint){warn(`${name}: "${k}" is a 1.x option. In 2.0 use ${hint}.`);continue;}
96
+ let best=null,bd=3;for(const a of allowed){const dd=a.toLowerCase()===k.toLowerCase()?0:distance(k,a);if(dd<bd){bd=dd;best=a;}}
97
+ warn(`${name}: unknown option "${k}"${best ? ` — did you mean "${best}"?` : `. Options: ${allowed.join(', ')}`}`);continue;}
98
+ if(v==null)continue;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'}` : ''}.`);const en=meta.enums?.[k];if(en&&!en.includes(v))warn(`${name}: ${k} must be ${en.map((x) => `'${x}'`).join(' | ')}, got ${JSON.stringify(v)}.`);if(k==='format'&&typeof v!=='function'&&!isObj(v))warn(`${name}: format must be Intl.NumberFormat options like { style: 'percent' } or a function (v) => string.`);if(k==='colors'&&(meta.colors?!isObj(v):!Array.isArray(v)))warn(`${name}: colors must be ${meta.colors || 'an array like [\'#64c8ff\', \'#10b981\']'}.`);}
99
+ if('data'in o)checkData(meta,o.data,o);}
100
+ function checkData(meta,d,o){if(d==null)return;const kind=meta.data;const bad=(why)=>warn(`${meta.name}: data must be ${SHAPES[kind]}${why ? ` — ${why}` : ''}.`);const first=isList(d)?d[0]:undefined;switch(kind){case'rows':if(isList(d)){if(Array.isArray(first))return bad(`pairs like ['Jan', 64] were removed in 2.0`);if(first!=null&&typeof first!=='number'&&!isObj(first))return bad(`got ${typeof first} items`);if(isObj(first)&&!o.value&&!('value'in first))warn(`${meta.name}: rows need a "value" field, or pass value: (row) => row.amount.`);if(isObj(first)&&!o.label&&!('label'in first))warn(`${meta.name}: rows need a "label" field, or pass label: (row) => row.name.`);}else if(!isObj(d))bad(`got ${typeof d}`);return;case'series':case'stream':{if(!isObj(d)||!Array.isArray(d.series)||(kind==='series'&&!isList(d.categories)))return bad(isList(d)?'got an array':'');const n=kind==='series'?d.categories.length:0;d.series.forEach((s,i)=>{const who=`series[${i}]${s?.name ? ` "${s.name}"` : ''}`;if(kind==='stream')return;if(!s||!isList(s.values))return warn(`${meta.name}: ${who} needs values: [...].`);if(s.values.length!==n)warn(`${meta.name}: ${who} has ${s.values.length} values but there are ${n} categories.`);});return;}
101
+ case'matrix':if(!isObj(d)||!isList(d.x)||!isList(d.y)||!Array.isArray(d.values))return bad(isList(d)?'got an array':'');if(d.values.length!==d.y.length)warn(`${meta.name}: values has ${d.values.length} rows but y has ${d.y.length} labels.`);return;case'number':if(typeof d!=='number')bad(`got ${typeof d}`);return;default:{if(!Array.isArray(d))return bad(`got ${isObj(d) ? 'an object' : typeof d}`);if(!first)return;const need={points:['x','y'],boxes:['q1','median','q3'],steps:['value']}[kind];const miss=need.filter((k)=>!(k in first));if(miss.length)bad(`the first item has no ${miss.join(', ')}`);if(kind==='steps'&&'isTotal'in first)warn(`${meta.name}: "isTotal" is 1.x. In 2.0 use total: true.`);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.`);}}}
102
+ const STATES=['loading','empty','error'];class Chart{static get meta(){return null;}
103
+ constructor(container,options={}){const meta=this.constructor.meta;const el=typeof container==='string'?document.querySelector(container):container;if(!el)throw new Error(`aeriz: ${meta ? meta.name + ': ' : ''}container not found${typeof container === 'string' ? ': ' + container : ''}`);this.container=el;this.options={...options};if(meta)checkOptions(meta,this.options);this._restore={position:el.style.position,height:el.style.height};el.classList.add('aeriz');if(getComputedStyle(el).position==='static')el.style.position='relative';if(!el.style.height&&el.offsetHeight===0)el.style.height='320px';this.wrap=div(el,'display:flex;flex-direction:column;width:100%;height:100%');this.legendEl=div(this.wrap,'display:none;flex:none;flex-wrap:wrap;gap:2px 12px;padding:0 0 6px;font:inherit;font-size:11px');this.plot=div(this.wrap,'position:relative;flex:1 1 auto;min-height:0');this.canvas=document.createElement('canvas');this.canvas.style.cssText='position:absolute;inset:0;display:block;width:100%;height:100%;touch-action:pan-y;outline:none';this.canvas.tabIndex=0;this.canvas.setAttribute('role','img');this.plot.appendChild(this.canvas);this.live=div(this.plot,'position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap');this.live.setAttribute('aria-live','polite');this.engine=new Engine(this.canvas);this.tooltip=new Tooltip(this.plot);this.overlay=new Overlay(this.plot);this.hits=new HitIndex();this._off=new Set();this._legendKey='';this._cursor=null;this._hover=null;this._dirty=true;this._destroyed=false;this._visible=typeof IntersectionObserver==='undefined';this._anim={progress:0,start:0,duration:600};this._state=null;this._auto=false;this._theme();this._label();this._setupEvents();this._observe();this._resize();if(options.animate===false||reducedMotion?.matches)this._anim.progress=1;schedule(this);}
104
+ update(patch={}){const meta=this.constructor.meta;if(meta)checkOptions(meta,patch);Object.assign(this.options,patch);this._cache=null;if('colors'in patch)this._theme();if(this._auto){this._state=null;this._auto=false;}
98
105
  this._label();this._restart();return this;}
99
- setData(data){return this.update({data});}
100
- setLoading(msg){return this._setState('loading',msg||'');}
101
- setEmpty(msg){return this._setState('empty',msg||'No data');}
102
- setError(msg){return this._setState('error',msg||'Error');}
103
- clearState(){return this._setState(null);}
106
+ setState(state,text){if(state!=null&&!STATES.includes(state)){throw new Error(`aeriz: setState() takes 'loading', 'empty', 'error' or null — got ${JSON.stringify(state)}`);}
107
+ this._state=state||null;this._stateMsg=text??(state==='empty'?this.options.emptyText||'No data':state==='error'?'Error':'');this._auto=false;this._dirty=true;schedule(this);return this;}
108
+ render(){if(this._destroyed)return this;this._theme();this._anim.progress=1;this._resize();this._dirty=false;this._draw();return this;}
104
109
  destroy(){if(this._destroyed)return;this._destroyed=true;charts.delete(this);this._ro?.disconnect();this._io?.disconnect();this._removeEvents();this.wrap.remove();const el=this.container;el.classList.remove('aeriz');el.style.position=this._restore.position;el.style.height=this._restore.height;}
105
- _setState(kind,msg){this._state=kind;this._stateMsg=msg;this._auto=false;this._dirty=true;schedule(this);return this;}
110
+ _theme(){const o=this.options;this.theme=resolveTheme(this.container);this.palette=Array.isArray(o.colors)&&o.colors.length?o.colors:readPalette(this.container);this.engine._gradCache.clear();this._legendKey='';this._dirty=true;}
106
111
  _empty(msg=this.options.emptyText||'No data'){this._state='empty';this._stateMsg=msg;this._auto=true;this.overlay.show('empty',msg,this.theme);}
107
112
  _restart(){const instant=this.options.animate===false||reducedMotion?.matches;this._anim.start=0;this._anim.progress=instant?1:0;this._dirty=true;schedule(this);}
108
- _label(){const o=this.options;this.canvas.setAttribute('aria-label',o.ariaLabel||o.title||this._kind||'Chart');}
113
+ _label(){this.canvas.setAttribute('aria-label',this.options.title||this.constructor.meta?.aria||'Chart');}
109
114
  _fmt(v){return M.formatNumber(v,this.options.format);}
110
115
  _tickFmt(v){return M.formatNumber(v,this.options.format,true);}
116
+ _lab(l,time){const f=this._fn('labelFormat');return f?String(f(l)):time?new Date(+l).toLocaleString():String(l);}
117
+ _xFmt(v){const f=this._fn('labelFormat');return f?String(f(v)):this._fmt(v);}
111
118
  _isOff(i){return this._off.has(i);}
119
+ _fn(k){const f=this.options[k];return typeof f==='function'?f:null;}
120
+ _hit(h){if(!h)return null;const out={index:h.index,label:h.label};if('value'in h)out.value=h.value;if('series'in h)out.series=h.series;if('row'in h)out.row=h.row;return out;}
121
+ _click(h){const hit=this._hit(h);if(hit)this.options.onClick?.(hit,this);}
112
122
  _resize(){const w=this.plot.clientWidth;const h=this.plot.clientHeight;if(w===this.engine.w&&h===this.engine.h)return false;this.engine.resize(Math.max(50,w),Math.max(50,h));this._dirty=true;return true;}
113
123
  _observe(){if(typeof ResizeObserver!=='undefined'){this._ro=new ResizeObserver(()=>{if(this._resize())schedule(this);});this._ro.observe(this.plot);}
114
124
  if(typeof IntersectionObserver!=='undefined'){this._io=new IntersectionObserver((entries)=>{this._visible=entries[entries.length-1].isIntersecting;if(this._visible&&this._dirty)schedule(this);});this._io.observe(this.container);}}
@@ -121,8 +131,8 @@ if(changed||this._follow){this._dirty=true;schedule(this);}
121
131
  if(typeof this._onMoveX==='function')this._onMoveX(p);return changed;}
122
132
  _onMove(e){const p=this._ptr(e);this._cursor=p;this._setHover(this.hits.find(p.x,p.y),p);}
123
133
  _onLeave(){this._cursor=null;this._hover=null;this._key=-1;this.tooltip.hide();this._dirty=true;schedule(this);}
124
- _onClick(e){const p=this._ptr(e);const hit=this.hits.find(p.x,p.y)||(this._follow?this._hover:null);this.options.onClick?.(hit,p,this);}
125
- _onKey(e){const k=e.key;const fwd=k==='ArrowRight'||k==='ArrowDown';const back=k==='ArrowLeft'||k==='ArrowUp';if(k==='Escape')return this._onLeave();if(k==='Enter'&&this._hover)return this.options.onClick?.(this._hover,this._cursor,this);if(!fwd&&!back&&k!=='Home'&&k!=='End')return;const steps=this._steps?.length?this._steps.map((x)=>({x,y:this.engine.h/2})):this.hits.regions.filter((r)=>r.tooltip);if(!steps.length)return;e.preventDefault();const n=steps.length;const cur=this._key??-1;this._key=k==='Home'?0:k==='End'?n-1:cur<0?(fwd?0:n-1):(cur+(fwd?1:n-1))%n;const s=steps[this._key];if(this._steps?.length){this._cursor={x:s.x,y:s.y};this._setHover(null,this._cursor);this._dirty=true;this._tick(performance.now());this.live.textContent=this._tipNow?tipText(this._tipNow):'';}else{this._cursor={x:s.tipX??s.x+s.w/2,y:s.tipY??s.y};this._setHover(s,this._cursor);this.live.textContent=tipText(s.tooltip);}}
134
+ _onClick(e){const p=this._ptr(e);this._click(this.hits.find(p.x,p.y)||(this._follow?this._hover:null));}
135
+ _onKey(e){const k=e.key;const fwd=k==='ArrowRight'||k==='ArrowDown';const back=k==='ArrowLeft'||k==='ArrowUp';if(k==='Escape')return this._onLeave();if(k==='Enter'&&this._hover)return this._click(this._hover);if(!fwd&&!back&&k!=='Home'&&k!=='End')return;const steps=this._steps?.length?this._steps.map((x)=>({x,y:this.engine.h/2})):this.hits.regions.filter((r)=>r.tooltip);if(!steps.length)return;e.preventDefault();const n=steps.length;const cur=this._key??-1;this._key=k==='Home'?0:k==='End'?n-1:cur<0?(fwd?0:n-1):(cur+(fwd?1:n-1))%n;const s=steps[this._key];if(this._steps?.length){this._cursor={x:s.x,y:s.y};this._setHover(null,this._cursor);this._dirty=true;this._tick(performance.now());this.live.textContent=this._tipNow?tipText(this._tipNow):'';}else{this._cursor={x:s.tipX??s.x+s.w/2,y:s.tipY??s.y};this._setHover(s,this._cursor);this.live.textContent=tipText(s.tooltip);}}
126
136
  _tick(now){if(this._destroyed)return false;if(!this._visible){if(this._anim.progress<1)this._anim.start=0;return false;}
127
137
  let active=false;const a=this._anim;if(a.progress<1){if(!a.start)a.start=now;const t=Math.min(1,(now-a.start)/a.duration);a.progress=t===1?1:1-Math.pow(1-t,3);this._dirty=true;if(t<1)active=true;}
128
138
  if(this._dirty){this._dirty=false;this._draw();}
@@ -134,131 +144,135 @@ _legend(){return null;}
134
144
  _renderLegend(){const items=this.options.legend===false?null:this._legend();const key=items?items.map((it,i)=>`${it.label}|${it.color}|${this._off.has(i)}`).join(','):'';if(key===this._legendKey)return;this._legendKey=key;const el=this.legendEl;el.textContent='';el.style.display=items&&items.length?'flex':'none';if(!items)return;el.style.color=this.theme.text;items.forEach((it,i)=>{const off=this._off.has(i);const b=div(el,'display:inline-flex;align-items:center;gap:6px;padding:2px 0;border:0;background:none;color:inherit;'+`font:inherit;letter-spacing:normal;text-transform:none;border-radius:0;box-shadow:none;min-width:0;min-height:0;cursor:pointer;opacity:${off ? 0.4 : 1}`,'button');b.type='button';b.dataset.i=i;b.setAttribute('aria-pressed',String(!off));div(b,`width:10px;height:10px;border-radius:2px;background:${it.color}`,'span');b.appendChild(document.createTextNode(it.label));});}
135
145
  _seriesLegend(series){return series.map((s,i)=>({label:s.name||'Series '+(i+1),color:this._colorOf(i,s)}));}
136
146
  _colorOf(idx,item){if(item&&item.color)return item.color;return this.palette[idx%this.palette.length];}}
147
+ const GONE={setData:'update({ data })',setLoading:"setState('loading', text)",setEmpty:"setState('empty', text)",setError:"setState('error', text)",clearState:'setState(null)',invalidate:'render()',setValue:'update({ data: value })',};for(const k in GONE){Chart.prototype[k]=function(){throw new Error(`aeriz 2.0: chart.${k}() was removed. Use chart.${GONE[k]}.`);};}
137
148
  const DTF=new Map();function dateFmt(opts){const k=JSON.stringify(opts);let f=DTF.get(k);if(!f)DTF.set(k,(f=new Intl.DateTimeFormat(undefined,opts)));return f;}
138
149
  function localTimeFormat(step){const o=step<6e4?{hour:'2-digit',minute:'2-digit',second:'2-digit',hour12:false}:step<864e5?{hour:'2-digit',minute:'2-digit',hour12:false}:step<2.5e9?{month:'short',day:'numeric'}:step<3e10?{year:'2-digit',month:'short'}:{year:'numeric'};const f=dateFmt(o);return(t)=>f.format(t);}
139
150
  function drawCartesian(chart,spec){const e=chart.engine;const t=chart.theme;const font=`${t.fontSize}px ${t.font}`;const yMin=spec.yMin!=null?spec.yMin:0;const yMax=spec.yMax;const yScale=spec.yScale||'linear';let yTicks,yNorm;if(yScale==='log'){const lo=Math.max(1e-10,spec.yMin||1);const lLo=Math.log10(lo),lHi=Math.log10(Math.max(lo*10,yMax));yNorm=(v)=>(Math.log10(Math.max(lo,v))-lLo)/(lHi-lLo);yTicks=[];for(let p=Math.floor(lLo);p<=Math.ceil(lHi);p++){const v=Math.pow(10,p);if(v>=lo&&v<=yMax)yTicks.push(v);}}else{yTicks=M.niceTicks(yMin,yMax,5);yNorm=(v)=>(v-yMin)/(yMax-yMin||1);}
140
- const yFmt=spec.yTickFormat||((v)=>chart._tickFmt(v));const yText=yTicks.map((v)=>yFmt(v));let maxW=0;for(const s of yText)maxW=Math.max(maxW,e.measure(s,font));const right=spec.right;let rTicks=[],rText=[],rNorm=null,rMaxW=0;if(right){rTicks=M.niceTicks(right.yMin,right.yMax,5);rNorm=(v)=>(v-right.yMin)/(right.yMax-right.yMin||1);const rf=right.yTickFormat||((v)=>chart._tickFmt(v));rText=rTicks.map((v)=>rf(v));for(const s of rText)rMaxW=Math.max(rMaxW,e.measure(s,font));}
151
+ const yFmt=spec.yFormat?(v)=>M.formatNumber(v,spec.yFormat,true):(v)=>chart._tickFmt(v);const yText=yTicks.map((v)=>yFmt(v));let maxW=0;for(const s of yText)maxW=Math.max(maxW,e.measure(s,font));const right=spec.right;let rTicks=[],rText=[],rNorm=null,rMaxW=0;if(right){rTicks=M.niceTicks(right.yMin,right.yMax,5);rNorm=(v)=>(v-right.yMin)/(right.yMax-right.yMin||1);const rf=right.yFormat?(v)=>M.formatNumber(v,right.yFormat,true):(v)=>chart._tickFmt(v);rText=rTicks.map((v)=>rf(v));for(const s of rText)rMaxW=Math.max(rMaxW,e.measure(s,font));}
141
152
  const p=spec.pad||{};const pad={l:p.l!=null?p.l:Math.ceil(maxW)+12,r:p.r!=null?p.r:right?Math.ceil(rMaxW)+12:16,t:p.t!=null?p.t:12,b:p.b!=null?p.b:t.fontSize+16,};const px=pad.l,py=pad.t;const pw=Math.max(1,e.w-pad.l-pad.r);const ph=Math.max(1,e.h-pad.t-pad.b);const yToPx=(v)=>py+ph*(1-yNorm(v));const yRToPx=rNorm?(v)=>py+ph*(1-rNorm(v)):null;const ctx=e.ctx;ctx.strokeStyle=t.grid;ctx.lineWidth=1;ctx.beginPath();for(const v of yTicks){const y=Math.round(yToPx(v))+0.5;ctx.moveTo(px,y);ctx.lineTo(px+pw,y);}
142
- ctx.stroke();yTicks.forEach((v,i)=>e.text(yText[i],px-6,yToPx(v),{fill:t.muted,font,align:'right',baseline:'middle'}));rTicks.forEach((v,i)=>e.text(rText[i],px+pw+6,yRToPx(v),{fill:t.muted,font,align:'left',baseline:'middle'}));let xToPx,xTicks,xFmt;if(spec.xTime){const{min,max}=spec.xTime;xToPx=(v)=>px+(pw*(v-min))/(max-min||1);xTicks=M.timeNiceTicks(min,max,Math.max(2,Math.floor(pw/90)));const step=xTicks.length>1?xTicks[1]-xTicks[0]:max-min;xFmt=spec.xTickFormat||localTimeFormat(step);}else if(spec.xLinear){const{min,max}=spec.xLinear;xToPx=(v)=>px+(pw*(v-min))/(max-min||1);xTicks=M.niceTicks(min,max,6);xFmt=spec.xTickFormat||((v)=>chart._tickFmt(v));}else if(spec.xLabels){const n=spec.xLabels.length||1;xToPx=(i)=>px+(i+0.5)*(pw/n);xFmt=spec.xTickFormat||((i)=>String(spec.xLabels[i]));const guess=Math.max(e.measure(xFmt(0,0),font),e.measure(xFmt(n-1,n-1),font))+6;const k=Math.max(1,Math.ceil((n*guess)/pw));xTicks=[];for(let i=0;i<n;i+=k)xTicks.push(i);}
143
- if(xToPx){let lastEnd=-Infinity;for(const v of xTicks){const txt=xFmt(v,v);const w=e.measure(txt,font);const x=Math.max(w/2,Math.min(e.w-w/2,xToPx(v)));if(x-w/2<lastEnd+6)continue;lastEnd=x+w/2;e.text(txt,x,py+ph+6,{fill:t.muted,font,align:'center',baseline:'top'});}}
153
+ ctx.stroke();yTicks.forEach((v,i)=>e.text(yText[i],px-6,yToPx(v),{fill:t.muted,font,align:'right',baseline:'middle'}));rTicks.forEach((v,i)=>e.text(rText[i],px+pw+6,yRToPx(v),{fill:t.muted,font,align:'left',baseline:'middle'}));let xToPx,xTicks,xFmt;const lf=chart._fn('labelFormat');if(spec.xTime){const{min,max}=spec.xTime;xToPx=(v)=>px+(pw*(v-min))/(max-min||1);xTicks=M.timeNiceTicks(min,max,Math.max(2,Math.floor(pw/90)));const step=xTicks.length>1?xTicks[1]-xTicks[0]:max-min;xFmt=lf?(v)=>String(lf(v)):localTimeFormat(step);}else if(spec.xLinear){const{min,max}=spec.xLinear;xToPx=(v)=>px+(pw*(v-min))/(max-min||1);xTicks=M.niceTicks(min,max,6);xFmt=lf?(v)=>String(lf(v)):(v)=>chart._tickFmt(v);}else if(spec.xLabels){const n=spec.xLabels.length||1;xToPx=(i)=>px+(i+0.5)*(pw/n);xFmt=(i)=>chart._lab(spec.xLabels[i]);const guess=Math.max(e.measure(xFmt(0),font),e.measure(xFmt(n-1),font))+6;const k=Math.max(1,Math.ceil((n*guess)/pw));xTicks=[];for(let i=0;i<n;i+=k)xTicks.push(i);}
154
+ if(xToPx){let lastEnd=-Infinity;for(const v of xTicks){const txt=xFmt(v);const w=e.measure(txt,font);const x=Math.max(w/2,Math.min(e.w-w/2,xToPx(v)));if(x-w/2<lastEnd+6)continue;lastEnd=x+w/2;e.text(txt,x,py+ph+6,{fill:t.muted,font,align:'center',baseline:'top'});}}
144
155
  e.poly([{x:px,y:py+ph},{x:px+pw,y:py+ph}],{stroke:t.axis,lineWidth:1});return{px,py,pw,ph,yToPx,yRToPx,xToPx,yTicks,xTicks,yMin,yMax};}
145
156
  function range(arrays,lo=Infinity,hi=-Infinity){for(const arr of arrays){for(let i=0;i<arr.length;i++){const v=+arr[i];if(v<lo)lo=v;if(v>hi)hi=v;}}
146
157
  if(lo===Infinity)return[0,1];return[lo,hi];}
147
- class BarChart extends Chart{get _kind(){return'Bar chart';}
148
- _render(){const data=toRows(this.options.data);if(!data.length)return this._empty();const e=this.engine;const t=this.theme;const o=this.options;const labelOf=o.label||((d)=>d.label);const valueOf=o.value||((d)=>d.value);const values=data.map((d,i)=>+valueOf(d,i)||0);const[lo,hi]=range([values],0,0);const[yLo,yHi]=M.niceDomain(lo,hi,5);const ax=drawCartesian(this,{yMin:yLo,yMax:yHi,xLabels:data.map((d,i)=>labelOf(d,i)),yTickFormat:o.yTickFormat,xTickFormat:o.xTickFormat,pad:o.dataLabels?{t:t.fontSize+10}:undefined,});const slot=ax.pw/data.length;const barW=Math.max(1,Math.min(60,slot*0.8,slot-2));const prog=this._anim.progress;const y0=ax.yToPx(0);const groups=new Map();const dl=o.dataLabels;const font=`${t.fontSize}px ${t.font}`;for(let i=0;i<data.length;i++){const v=values[i];const x=ax.px+(i+0.5)*slot-barW/2;const h=Math.abs(ax.yToPx(v)-y0)*prog;const top=v>=0?y0-h:y0;const color=this._colorOf(o.color?0:i,data[i].color?data[i]:{color:o.color});if(!groups.has(color))groups.set(color,[]);groups.get(color).push({x,y:top,w:barW,h,radius:t.barRadius});this.hits.add({_id:i,x,y:Math.min(top,y0-2),w:barW,h:Math.max(h,4),tipX:x+barW/2,tipY:top,tooltip:`${labelOf(data[i], i)}: ${this._fmt(v)}`,data:data[i],index:i,value:v,});if(dl&&prog>0.95){const txt=typeof dl==='function'?dl(data[i],i):this._fmt(v);e.text(txt,x+barW/2,v>=0?top-4:top+h+4,{fill:t.text,font,align:'center',baseline:v>=0?'bottom':'top',});}}
158
+ function valueDomain(o,lo,hi){const[a,b]=M.niceDomain(o.min??lo,o.max??hi,5);return[o.min??a,o.max??b];}
159
+ const AXIS=['labelFormat','min','max'];class BarChart extends Chart{static get meta(){return{id:'bar',name:'BarChart',aria:'Bar chart',data:'rows',options:['label','value','labelFormat','min','max','dataLabels']};}
160
+ _render(){const data=toRows(this.options.data);if(!data.length)return this._empty();const e=this.engine;const t=this.theme;const o=this.options;const labelOf=this._fn('label')||((d)=>d.label);const valueOf=this._fn('value')||((d)=>d.value);const values=data.map((d,i)=>+valueOf(d,i)||0);const labels=data.map((d,i)=>labelOf(d,i));const[lo,hi]=range([values],0,0);const[yLo,yHi]=valueDomain(o,lo,hi);const dl=o.dataLabels;const ax=drawCartesian(this,{yMin:yLo,yMax:yHi,xLabels:labels,pad:dl?{t:t.fontSize+10}:undefined,});const slot=ax.pw/data.length;const barW=Math.max(1,Math.min(60,slot*0.8,slot-2));const prog=this._anim.progress;const y0=ax.yToPx(Math.max(yLo,0));const groups=new Map();const font=`${t.fontSize}px ${t.font}`;for(let i=0;i<data.length;i++){const v=values[i];const x=ax.px+(i+0.5)*slot-barW/2;const h=Math.abs(ax.yToPx(v)-y0)*prog;const top=v>=0?y0-h:y0;const color=this._colorOf(i,data[i]);if(!groups.has(color))groups.set(color,[]);groups.get(color).push({x,y:top,w:barW,h,radius:t.barRadius});const label=this._lab(labels[i]);this.hits.add({_id:i,x,y:Math.min(top,y0-2),w:barW,h:Math.max(h,4),tipX:x+barW/2,tipY:top,tooltip:`${label}: ${this._fmt(v)}`,index:i,label:labels[i],value:v,row:data[i],});if(dl&&prog>0.95){const txt=typeof dl==='function'?dl(v,data[i],i):this._fmt(v);e.text(txt,x+barW/2,v>=0?top-4:top+h+4,{fill:t.text,font,align:'center',baseline:v>=0?'bottom':'top',});}}
149
161
  for(const[color,items]of groups)e.rects(items,color);if(this._hover){const r=this._hover;e.rect(r.x,r.y,r.w,r.h,{stroke:t.text,lineWidth:1.5});}}}
150
- class HBarChart extends Chart{get _kind(){return'Horizontal bar chart';}
151
- _render(){const data=toRows(this.options.data);if(!data.length)return this._empty();const e=this.engine;const t=this.theme;const o=this.options;const labelOf=o.label||((d)=>d.label);const valueOf=o.value||((d)=>d.value);const values=data.map((d,i)=>+valueOf(d,i)||0);const labels=data.map((d,i)=>String(labelOf(d,i)));const[lo,hi]=range([values],0,0);const[xLo,xHi]=M.niceDomain(lo,hi,5);const font=`${t.fontSize}px ${t.font}`;let labelW=0;for(const s of labels)labelW=Math.max(labelW,e.measure(s,font));const px=Math.min(Math.ceil(labelW)+12,e.w*0.4),py=12;const pw=Math.max(1,e.w-px-20);const ph=Math.max(1,e.h-py-t.fontSize-16);const xTicks=M.niceTicks(xLo,xHi,Math.max(2,Math.min(6,Math.floor(pw/70))));const xToPx=(v)=>px+(pw*(v-xLo))/(xHi-xLo||1);const xFmt=o.xTickFormat||((v)=>this._tickFmt(v));e.ctx.strokeStyle=t.grid;e.ctx.lineWidth=1;e.ctx.beginPath();for(const v of xTicks){const x=Math.round(xToPx(v))+0.5;e.ctx.moveTo(x,py);e.ctx.lineTo(x,py+ph);}
152
- e.ctx.stroke();for(const v of xTicks){e.text(xFmt(v),xToPx(v),py+ph+6,{fill:t.muted,font,align:'center',baseline:'top'});}
153
- e.poly([{x:px,y:py+ph},{x:px+pw,y:py+ph}],{stroke:t.axis,lineWidth:1});const slot=ph/data.length;const barH=Math.max(1,Math.min(40,slot*0.75,slot-2));const prog=this._anim.progress;const x0=xToPx(0);const groups=new Map();for(let i=0;i<data.length;i++){const v=values[i];const yMid=py+(i+0.5)*slot;const y=yMid-barH/2;const w=Math.abs(xToPx(v)-x0)*prog;const left=v>=0?x0:x0-w;const color=this._colorOf(o.color?0:i,data[i].color?data[i]:{color:o.color});if(!groups.has(color))groups.set(color,[]);groups.get(color).push({x:left,y,w,h:barH,radius:t.barRadius});this.hits.add({_id:i,x:Math.min(left,x0-2),y,w:Math.max(w,4),h:barH,tipX:left+w/2,tipY:y,tooltip:`${labels[i]}: ${this._fmt(v)}`,data:data[i],index:i,value:v,});e.text(labels[i],px-6,yMid,{fill:t.muted,font,align:'right',baseline:'middle'});}
162
+ class HBarChart extends Chart{static get meta(){return{id:'hbar',name:'HBarChart',aria:'Horizontal bar chart',data:'rows',options:['label','value','labelFormat','min','max']};}
163
+ _render(){const data=toRows(this.options.data);if(!data.length)return this._empty();const e=this.engine;const t=this.theme;const o=this.options;const labelOf=this._fn('label')||((d)=>d.label);const valueOf=this._fn('value')||((d)=>d.value);const values=data.map((d,i)=>+valueOf(d,i)||0);const raw=data.map((d,i)=>labelOf(d,i));const labels=raw.map((l)=>this._lab(l));const[lo,hi]=range([values],0,0);const[xLo,xHi]=valueDomain(o,lo,hi);const font=`${t.fontSize}px ${t.font}`;let labelW=0;for(const s of labels)labelW=Math.max(labelW,e.measure(s,font));const px=Math.min(Math.ceil(labelW)+12,e.w*0.4),py=12;const pw=Math.max(1,e.w-px-20);const ph=Math.max(1,e.h-py-t.fontSize-16);const xTicks=M.niceTicks(xLo,xHi,Math.max(2,Math.min(6,Math.floor(pw/70))));const xToPx=(v)=>px+(pw*(v-xLo))/(xHi-xLo||1);e.ctx.strokeStyle=t.grid;e.ctx.lineWidth=1;e.ctx.beginPath();for(const v of xTicks){const x=Math.round(xToPx(v))+0.5;e.ctx.moveTo(x,py);e.ctx.lineTo(x,py+ph);}
164
+ e.ctx.stroke();for(const v of xTicks){e.text(this._tickFmt(v),xToPx(v),py+ph+6,{fill:t.muted,font,align:'center',baseline:'top'});}
165
+ e.poly([{x:px,y:py+ph},{x:px+pw,y:py+ph}],{stroke:t.axis,lineWidth:1});const slot=ph/data.length;const barH=Math.max(1,Math.min(40,slot*0.75,slot-2));const prog=this._anim.progress;const x0=xToPx(Math.max(xLo,0));const groups=new Map();for(let i=0;i<data.length;i++){const v=values[i];const yMid=py+(i+0.5)*slot;const y=yMid-barH/2;const w=Math.abs(xToPx(v)-x0)*prog;const left=v>=0?x0:x0-w;const color=this._colorOf(i,data[i]);if(!groups.has(color))groups.set(color,[]);groups.get(color).push({x:left,y,w,h:barH,radius:t.barRadius});this.hits.add({_id:i,x:Math.min(left,x0-2),y,w:Math.max(w,4),h:barH,tipX:left+w/2,tipY:y,tooltip:`${labels[i]}: ${this._fmt(v)}`,index:i,label:raw[i],value:v,row:data[i],});e.text(labels[i],px-6,yMid,{fill:t.muted,font,align:'right',baseline:'middle'});}
154
166
  for(const[color,items]of groups)e.rects(items,color);if(this._hover){const r=this._hover;e.rect(r.x,r.y,r.w,r.h,{stroke:t.text,lineWidth:1.5});}}}
155
- const LABEL=(d)=>d.label;const VALUE=(d)=>d.value;class LineChart extends Chart{get _kind(){return'Line chart';}
156
- _render(){const data=toRows(this.options.data);if(!data.length)return this._empty();const e=this.engine;const t=this.theme;const o=this.options;const labelOf=o.label||LABEL;const valueOf=o.value||VALUE;const n=data.length;let c=this._cache;if(!c||c.data!==o.data||c.valueOf!==valueOf||c.labelOf!==labelOf){const values=new Float64Array(n);for(let i=0;i<n;i++)values[i]=+valueOf(data[i],i);c=this._cache={data:o.data,valueOf,labelOf,values,idx:null,idxW:0,xs:null,labels:null,range:null};}
157
- const values=c.values;const[lo,hi]=(c.range||=range([values],o.yScale==='log'?Infinity:0));const[yLo,yHi]=o.yScale==='log'?[lo,hi]:M.niceDomain(lo,hi,5);const useTime=o.xScale==='time';const spec={yMin:yLo,yMax:yHi,yScale:o.yScale,yTickFormat:o.yTickFormat,xTickFormat:o.xTickFormat,};const xs=useTime?(c.xs||=data.map((d,i)=>+labelOf(d,i))):null;if(useTime)spec.xTime={min:xs[0],max:xs[n-1]};else spec.xLabels=c.labels||=data.map((d,i)=>labelOf(d,i));const ax=drawCartesian(this,spec);const color=o.color||this.palette[0];const interp=o.interpolation||'smooth';const lineW=t.lineWidth;const prog=this._anim.progress;const xAt=useTime?(i)=>ax.xToPx(xs[i]):(i)=>ax.px+((i+0.5)/n)*ax.pw;const decimated=M.shouldDecimate(n,ax.pw);if(decimated&&(!c.idx||c.idxW!==Math.floor(ax.pw))){c.idxW=Math.floor(ax.pw);c.idx=M.minMaxDecimateIdx(n,c.idxW,(i)=>values[i]);}
167
+ const LABEL=(d)=>d.label;const VALUE=(d)=>d.value;class LineChart extends Chart{static get meta(){return{id:'line',name:'LineChart',aria:'Line chart',data:'rows',options:['label','value','labelFormat','min','max','curve','fill','points','xScale','yScale'],enums:{curve:['smooth','linear','step'],xScale:['category','time'],yScale:['linear','log']},};}
168
+ _render(){const o=this.options;const labelOf=this._fn('label')||LABEL;const valueOf=this._fn('value')||VALUE;let c=this._cache;if(!c||c.data!==o.data||c.valueOf!==valueOf||c.labelOf!==labelOf){const d=o.data;const nums=d&&typeof d[0]==='number'&&(Array.isArray(d)||ArrayBuffer.isView(d))&&!this._fn('value')&&!this._fn('label');const rows=nums?d:toRows(d);const values=nums?Float64Array.from(d):new Float64Array(rows.length);if(!nums)for(let i=0;i<rows.length;i++)values[i]=+valueOf(rows[i],i);const labels=nums?Array.from({length:d.length},(_,i)=>i+1):null;c=this._cache={data:o.data,rows,valueOf,labelOf,values,idx:null,idxW:0,xs:null,labels,range:null};}
169
+ const data=c.rows;if(!data.length)return this._empty();const e=this.engine;const t=this.theme;const n=data.length;const values=c.values;const log=o.yScale==='log';const[lo,hi]=(c.range||=range([values],log?Infinity:0));const[yLo,yHi]=log?[o.min??lo,o.max??hi]:valueDomain(o,lo,hi);const useTime=o.xScale==='time';const spec={yMin:yLo,yMax:yHi,yScale:o.yScale};const xs=useTime?(c.xs||=Array.from(data,(d,i)=>+labelOf(d,i))):null;if(useTime)spec.xTime={min:xs[0],max:xs[n-1]};else spec.xLabels=c.labels||=Array.from(data,(d,i)=>labelOf(d,i));const ax=drawCartesian(this,spec);const color=this.palette[0];const interp=o.curve||'smooth';const lineW=t.lineWidth;const prog=this._anim.progress;const xAt=useTime?(i)=>ax.xToPx(xs[i]):(i)=>ax.px+((i+0.5)/n)*ax.pw;const decimated=M.shouldDecimate(n,ax.pw);if(decimated&&(!c.idx||c.idxW!==Math.floor(ax.pw))){c.idxW=Math.floor(ax.pw);c.idx=M.minMaxDecimateIdx(n,c.idxW,(i)=>values[i]);}
158
170
  const idx=decimated?c.idx:null;const count=idx?idx.length:n;const points=new Array(count);for(let k=0;k<count;k++){const i=idx?idx[k]:k;points[k]={x:xAt(i),y:ax.yToPx(values[i]),i};}
159
171
  e.clip(ax.px-lineW,ax.py-lineW,(ax.pw+lineW*2)*prog,ax.ph+lineW*2);const smooth=interp==='smooth'&&!decimated;const base=ax.py+ax.ph;if(interp==='smooth'||interp==='linear'){const cps=smooth?M.bezierControlPoints(points):null;if(o.fill!==false){const grad=e.vGradient(`area-${color}`,ax.px,ax.py,base,[[0,M.colorAlpha(color,0.25)],[1,M.colorAlpha(color,0)],]);if(cps)e.smoothArea(points,cps,base,grad);else e.poly(points.concat([{x:points[count-1].x,y:base},{x:points[0].x,y:base}]),{fill:grad});}
160
- if(cps)e.smooth(points,cps,{stroke:color,lineWidth:lineW});else e.poly(points,{stroke:color,lineWidth:lineW,cap:'round',join:'round'});}else if(interp.startsWith('step')){e.step(points,interp==='step'?'before':interp.split('-')[1],{stroke:color,lineWidth:lineW});}
161
- e.unclip();const tip=(i)=>`${useTime ? new Date(xs[i]).toLocaleString() : labelOf(data[i], i)}: ${this._fmt(values[i])}`;if(!decimated&&n<=50){if(o.points!==false)e.circles(points,color,t.pointRadius);for(const p of points){this.hits.add({_id:p.i,x:p.x-8,y:p.y-8,w:16,h:16,tipX:p.x,tipY:p.y-t.pointRadius,tooltip:tip(p.i),data:data[p.i],index:p.i,value:values[p.i],});}}else{for(let k=0;k<count;k++){const p=points[k];const l=k?(points[k-1].x+p.x)/2:ax.px;const r=k<count-1?(points[k+1].x+p.x)/2:ax.px+ax.pw;if(r<=l&&k<count-1)continue;this.hits.add({_id:p.i,x:l,y:ax.py,w:Math.max(1,r-l),h:ax.ph,tipX:p.x,tipY:p.y-8,tooltip:tip(p.i),data:data[p.i],index:p.i,value:values[p.i],});}}
172
+ if(cps)e.smooth(points,cps,{stroke:color,lineWidth:lineW});else e.poly(points,{stroke:color,lineWidth:lineW,cap:'round',join:'round'});}else{e.step(points,'before',{stroke:color,lineWidth:lineW});}
173
+ e.unclip();const labelAt=(i)=>(useTime?xs[i]:c.labels[i]);const tip=(i)=>`${this._lab(labelAt(i), useTime)}: ${this._fmt(values[i])}`;if(!decimated&&n<=50){if(o.points!==false)e.circles(points,color,t.pointRadius);for(const p of points){this.hits.add({_id:p.i,x:p.x-8,y:p.y-8,w:16,h:16,tipX:p.x,tipY:p.y-t.pointRadius,tooltip:tip(p.i),index:p.i,label:labelAt(p.i),value:values[p.i],row:data[p.i],});}}else{for(let k=0;k<count;k++){const p=points[k];const l=k?(points[k-1].x+p.x)/2:ax.px;const r=k<count-1?(points[k+1].x+p.x)/2:ax.px+ax.pw;if(r<=l&&k<count-1)continue;this.hits.add({_id:p.i,x:l,y:ax.py,w:Math.max(1,r-l),h:ax.ph,tipX:p.x,tipY:p.y-8,tooltip:tip(p.i),index:p.i,label:labelAt(p.i),value:values[p.i],row:data[p.i],});}}
162
174
  if(this._hover){const p=points.find((pp)=>pp.i===this._hover._id);if(p)e.circle(p.x,p.y,t.pointRadius+3,{stroke:color,lineWidth:2,fill:t.bg});}}}
163
- class AreaChart extends Chart{get _kind(){return'Area chart';}
164
- _legend(){return this._seriesLegend(this.options.series||[]);}
165
- _render(){const o=this.options;const series=o.series||[];const cats=o.categories||[];if(!series.length||!cats.length)return this._empty();const e=this.engine;const t=this.theme;const stacked=o.stacked===true;const on=series.map((s,i)=>i).filter((i)=>!this._isOff(i));let yMin=0,yMax;if(stacked){yMax=0;for(let i=0;i<cats.length;i++){let sum=0;for(const s of on)sum+=+series[s].values[i]||0;if(sum>yMax)yMax=sum;}}else{[yMin,yMax]=range(on.map((s)=>series[s].values),0,0);}
166
- const[yLo,yHi]=M.niceDomain(yMin,yMax,5);const ax=drawCartesian(this,{yMin:yLo,yMax:yHi,xLabels:cats,yTickFormat:o.yTickFormat,xTickFormat:o.xTickFormat});const lineW=t.lineWidth;const prog=this._anim.progress;const xAt=(i)=>ax.px+((i+0.5)/cats.length)*ax.pw;const ctx=e.ctx;e.clip(ax.px,ax.py-lineW,ax.pw*prog,ax.ph+lineW*2);const cum=new Array(cats.length).fill(0);for(const s of on){const ser=series[s];const color=this._colorOf(s,ser);const top=[],bot=[];for(let i=0;i<cats.length;i++){const v=+ser.values[i]||0;bot.push({x:xAt(i),y:ax.yToPx(stacked?cum[i]:Math.max(yLo,0))});top.push({x:xAt(i),y:ax.yToPx(stacked?cum[i]+v:v)});if(stacked)cum[i]+=v;}
175
+ class AreaChart extends Chart{static get meta(){return{id:'area',name:'AreaChart',aria:'Area chart',data:'series',options:['labelFormat','min','max','stacked']};}
176
+ _legend(){return this._seriesLegend(this.options.data?.series||[]);}
177
+ _render(){const o=this.options;const{categories:cats=[],series=[]}=o.data||{};if(!series.length||!cats.length)return this._empty();const e=this.engine;const t=this.theme;const stacked=o.stacked===true;const on=series.map((s,i)=>i).filter((i)=>!this._isOff(i));let yMin=0,yMax;if(stacked){yMax=0;for(let i=0;i<cats.length;i++){let sum=0;for(const s of on)sum+=+series[s].values[i]||0;if(sum>yMax)yMax=sum;}}else{[yMin,yMax]=range(on.map((s)=>series[s].values),0,0);}
178
+ const[yLo,yHi]=valueDomain(o,yMin,yMax);const ax=drawCartesian(this,{yMin:yLo,yMax:yHi,xLabels:cats});const lineW=t.lineWidth;const prog=this._anim.progress;const xAt=(i)=>ax.px+((i+0.5)/cats.length)*ax.pw;const ctx=e.ctx;e.clip(ax.px,ax.py-lineW,ax.pw*prog,ax.ph+lineW*2);const cum=new Array(cats.length).fill(0);for(const s of on){const ser=series[s];const color=this._colorOf(s,ser);const top=[],bot=[];for(let i=0;i<cats.length;i++){const v=+ser.values[i]||0;bot.push({x:xAt(i),y:ax.yToPx(stacked?cum[i]:Math.max(yLo,0))});top.push({x:xAt(i),y:ax.yToPx(stacked?cum[i]+v:v)});if(stacked)cum[i]+=v;}
167
179
  const cps=M.bezierControlPoints(top);ctx.beginPath();ctx.moveTo(top[0].x,top[0].y);for(let i=0;i<cps.length;i++){ctx.bezierCurveTo(cps[i].c1x,cps[i].c1y,cps[i].c2x,cps[i].c2y,top[i+1].x,top[i+1].y);}
168
180
  for(let i=bot.length-1;i>=0;i--)ctx.lineTo(bot[i].x,bot[i].y);ctx.closePath();ctx.fillStyle=stacked?M.colorAlpha(color,0.7):e.vGradient(`area-${s}-${color}`,ax.px,ax.py,ax.py+ax.ph,[[0,M.colorAlpha(color,0.4)],[1,M.colorAlpha(color,0)]]);ctx.fill();e.smooth(top,cps,{stroke:color,lineWidth:lineW});}
169
- e.unclip();const slot=ax.pw/cats.length;for(let i=0;i<cats.length;i++){this.hits.add({_id:i,x:ax.px+i*slot,y:ax.py,w:slot,h:ax.ph,tipX:xAt(i),tipY:ax.py+8,index:i,tooltip:{title:String(cats[i]),rows:on.map((s)=>({color:this._colorOf(s,series[s]),name:series[s].name||'Series '+(s+1),value:this._fmt(+series[s].values[i])})),},});}
181
+ e.unclip();const slot=ax.pw/cats.length;for(let i=0;i<cats.length;i++){this.hits.add({_id:i,x:ax.px+i*slot,y:ax.py,w:slot,h:ax.ph,tipX:xAt(i),tipY:ax.py+8,index:i,label:cats[i],tooltip:{title:this._lab(cats[i]),rows:on.map((s)=>({color:this._colorOf(s,series[s]),name:series[s].name||'Series '+(s+1),value:this._fmt(+series[s].values[i])})),},});}
170
182
  if(this._hover){const x=xAt(this._hover._id);e.poly([{x,y:ax.py},{x,y:ax.py+ax.ph}],{stroke:t.muted,lineWidth:1,dash:[3,3]});}}}
171
- class StackedBarChart extends Chart{get _kind(){return'Stacked bar chart';}
172
- _legend(){return this._seriesLegend(this.options.series||[]);}
173
- _render(){const o=this.options;const series=o.series||[];const cats=o.categories||[];if(!series.length||!cats.length)return this._empty();const e=this.engine;const t=this.theme;const on=series.map((s,i)=>i).filter((i)=>!this._isOff(i));let lo=0,hi=0;for(let i=0;i<cats.length;i++){let pos=0,neg=0;for(const s of on){const v=+series[s].values[i]||0;if(v>=0)pos+=v;else neg+=v;}
183
+ class StackedBarChart extends Chart{static get meta(){return{id:'stacked-bar',name:'StackedBarChart',aria:'Stacked bar chart',data:'series',options:['labelFormat','min','max']};}
184
+ _legend(){return this._seriesLegend(this.options.data?.series||[]);}
185
+ _render(){const o=this.options;const{categories:cats=[],series=[]}=o.data||{};if(!series.length||!cats.length)return this._empty();const e=this.engine;const t=this.theme;const on=series.map((s,i)=>i).filter((i)=>!this._isOff(i));let lo=0,hi=0;for(let i=0;i<cats.length;i++){let pos=0,neg=0;for(const s of on){const v=+series[s].values[i]||0;if(v>=0)pos+=v;else neg+=v;}
174
186
  if(pos>hi)hi=pos;if(neg<lo)lo=neg;}
175
- const[yLo,yHi]=M.niceDomain(lo,hi,5);const ax=drawCartesian(this,{yMin:yLo,yMax:yHi,xLabels:cats,yTickFormat:o.yTickFormat,xTickFormat:o.xTickFormat});const slot=ax.pw/cats.length;const barW=Math.max(1,Math.min(50,slot*0.8,slot-2));const prog=this._anim.progress;const groups=new Map();const y0=ax.yToPx(0);const lastPos=[],lastNeg=[];for(let i=0;i<cats.length;i++){let pos=0,neg=0;const x=ax.px+(i+0.5)*slot-barW/2;for(const s of on){const v=+series[s].values[i]||0;if(!v)continue;const from=v>0?pos:neg;const to=from+v;if(v>0)pos=to;else neg=to;const ya=y0+(ax.yToPx(from)-y0)*prog;const yb=y0+(ax.yToPx(to)-y0)*prog;const y=Math.min(ya,yb),h=Math.abs(yb-ya);const color=this._colorOf(s,series[s]);if(!groups.has(color))groups.set(color,[]);const item={x,y,w:barW,h,radius:0};groups.get(color).push(item);(v>0?lastPos:lastNeg)[i]=item;this.hits.add({_id:`${i}-${s}`,x,y,w:barW,h,tipX:x+barW/2,tipY:y,index:i,series:s,value:v,tooltip:{title:String(cats[i]),rows:[{color,name:series[s].name||'Series '+(s+1),value:this._fmt(v)}]},});}}
187
+ const[yLo,yHi]=valueDomain(o,lo,hi);const ax=drawCartesian(this,{yMin:yLo,yMax:yHi,xLabels:cats});const slot=ax.pw/cats.length;const barW=Math.max(1,Math.min(50,slot*0.8,slot-2));const prog=this._anim.progress;const groups=new Map();const y0=ax.yToPx(0);const lastPos=[],lastNeg=[];for(let i=0;i<cats.length;i++){let pos=0,neg=0;const x=ax.px+(i+0.5)*slot-barW/2;for(const s of on){const v=+series[s].values[i]||0;if(!v)continue;const from=v>0?pos:neg;const to=from+v;if(v>0)pos=to;else neg=to;const ya=y0+(ax.yToPx(from)-y0)*prog;const yb=y0+(ax.yToPx(to)-y0)*prog;const y=Math.min(ya,yb),h=Math.abs(yb-ya);const color=this._colorOf(s,series[s]);if(!groups.has(color))groups.set(color,[]);const item={x,y,w:barW,h,radius:0};groups.get(color).push(item);(v>0?lastPos:lastNeg)[i]=item;this.hits.add({_id:`${i}-${s}`,x,y,w:barW,h,tipX:x+barW/2,tipY:y,index:i,label:cats[i],series:s,value:v,row:series[s],tooltip:{title:this._lab(cats[i]),rows:[{color,name:series[s].name||'Series '+(s+1),value:this._fmt(v)}]},});}}
176
188
  for(const it of lastPos)if(it)it.radius=t.barRadius;for(const it of lastNeg)if(it)it.radius=t.barRadius;for(const[color,items]of groups)e.rects(items,color);if(this._hover){const r=this._hover;e.rect(r.x,r.y,r.w,r.h,{stroke:t.text,lineWidth:1.5});}}}
177
- class GroupedBarChart extends Chart{get _kind(){return'Grouped bar chart';}
178
- _legend(){return this._seriesLegend(this.options.series||[]);}
179
- _render(){const o=this.options;const series=o.series||[];const cats=o.categories||[];if(!series.length||!cats.length)return this._empty();const e=this.engine;const t=this.theme;const on=series.map((s,i)=>i).filter((i)=>!this._isOff(i));const[lo,hi]=range(on.map((s)=>series[s].values),0,0);const[yLo,yHi]=M.niceDomain(lo,hi,5);const ax=drawCartesian(this,{yMin:yLo,yMax:yHi,xLabels:cats,yTickFormat:o.yTickFormat,xTickFormat:o.xTickFormat});const slot=ax.pw/cats.length;const inner=slot*0.8;const barW=Math.max(1,inner/on.length);const prog=this._anim.progress;const y0=ax.yToPx(0);const groups=new Map();for(let i=0;i<cats.length;i++){const left=ax.px+(i+0.5)*slot-inner/2;on.forEach((s,k)=>{const v=+series[s].values[i]||0;const x=left+k*barW;const h=Math.abs(ax.yToPx(v)-y0)*prog;const y=v>=0?y0-h:y0;const color=this._colorOf(s,series[s]);if(!groups.has(color))groups.set(color,[]);const w=Math.max(1,barW-(barW>4?1:0));groups.get(color).push({x,y,w,h,radius:t.barRadius});this.hits.add({_id:`${i}-${s}`,x,y:Math.min(y,y0-2),w,h:Math.max(h,4),tipX:x+barW/2,tipY:y,index:i,series:s,value:v,tooltip:{title:String(cats[i]),rows:[{color,name:series[s].name||'Series '+(s+1),value:this._fmt(v)}]},});});}
189
+ class GroupedBarChart extends Chart{static get meta(){return{id:'grouped-bar',name:'GroupedBarChart',aria:'Grouped bar chart',data:'series',options:['labelFormat','min','max']};}
190
+ _legend(){return this._seriesLegend(this.options.data?.series||[]);}
191
+ _render(){const o=this.options;const{categories:cats=[],series=[]}=o.data||{};if(!series.length||!cats.length)return this._empty();const e=this.engine;const t=this.theme;const on=series.map((s,i)=>i).filter((i)=>!this._isOff(i));const[lo,hi]=range(on.map((s)=>series[s].values),0,0);const[yLo,yHi]=valueDomain(o,lo,hi);const ax=drawCartesian(this,{yMin:yLo,yMax:yHi,xLabels:cats});const slot=ax.pw/cats.length;const inner=slot*0.8;const barW=Math.max(1,inner/on.length);const prog=this._anim.progress;const y0=ax.yToPx(0);const groups=new Map();for(let i=0;i<cats.length;i++){const left=ax.px+(i+0.5)*slot-inner/2;on.forEach((s,k)=>{const v=+series[s].values[i]||0;const x=left+k*barW;const h=Math.abs(ax.yToPx(v)-y0)*prog;const y=v>=0?y0-h:y0;const color=this._colorOf(s,series[s]);if(!groups.has(color))groups.set(color,[]);const w=Math.max(1,barW-(barW>4?1:0));groups.get(color).push({x,y,w,h,radius:t.barRadius});this.hits.add({_id:`${i}-${s}`,x,y:Math.min(y,y0-2),w,h:Math.max(h,4),tipX:x+barW/2,tipY:y,index:i,label:cats[i],series:s,value:v,row:series[s],tooltip:{title:this._lab(cats[i]),rows:[{color,name:series[s].name||'Series '+(s+1),value:this._fmt(v)}]},});});}
180
192
  for(const[color,items]of groups)e.rects(items,color);if(this._hover){const r=this._hover;e.rect(r.x,r.y,r.w,r.h,{stroke:t.text,lineWidth:1.5});}}}
181
- class MultiLineChart extends Chart{get _kind(){return'Multi line chart';}
193
+ class MultiLineChart extends Chart{static get meta(){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']}};}
182
194
  constructor(container,options){super(container,options);this._follow=true;}
183
- _legend(){return this._seriesLegend(this.options.series||[]);}
184
- _render(){const o=this.options;const series=o.series||[];const cats=o.categories||[];if(!series.length||!cats.length)return this._empty();const e=this.engine;const t=this.theme;const interp=o.interpolation||'smooth';const on=series.map((s,i)=>i).filter((i)=>!this._isOff(i)&&!series[i].hidden);const[lo,hi]=range(on.map((s)=>series[s].values),0);const[yLo,yHi]=M.niceDomain(lo,hi,5);const useTime=o.xScale==='time';const spec={yMin:yLo,yMax:yHi,yTickFormat:o.yTickFormat,xTickFormat:o.xTickFormat};if(useTime)spec.xTime={min:+cats[0],max:+cats[cats.length-1]};else spec.xLabels=cats;const ax=drawCartesian(this,spec);const lineW=t.lineWidth;const prog=this._anim.progress;const xAt=useTime?(i)=>ax.xToPx(+cats[i]):(i)=>ax.px+((i+0.5)/cats.length)*ax.pw;this._steps=cats.map((_,i)=>xAt(i));e.clip(ax.px-lineW,ax.py-lineW,(ax.pw+lineW*2)*prog,ax.ph+lineW*2);const all=[];for(const s of on){const ser=series[s];const color=this._colorOf(s,ser);const pts=ser.values.map((v,i)=>({x:xAt(i),y:ax.yToPx(+v)}));all.push({pts,color,s,ser});if(interp==='smooth')e.smooth(pts,M.bezierControlPoints(pts),{stroke:color,lineWidth:lineW});else if(interp==='linear')e.poly(pts,{stroke:color,lineWidth:lineW,cap:'round',join:'round'});else e.step(pts,interp==='step'?'before':interp.split('-')[1],{stroke:color,lineWidth:lineW});if(pts.length<=50)e.circles(pts,color,t.pointRadius);}
195
+ _legend(){return this._seriesLegend(this.options.data?.series||[]);}
196
+ _render(){const o=this.options;const{categories:cats=[],series=[]}=o.data||{};if(!series.length||!cats.length)return this._empty();const e=this.engine;const t=this.theme;const interp=o.curve||'smooth';const on=series.map((s,i)=>i).filter((i)=>!this._isOff(i));const[lo,hi]=range(on.map((s)=>series[s].values),0);const[yLo,yHi]=valueDomain(o,lo,hi);const useTime=o.xScale==='time';const spec={yMin:yLo,yMax:yHi};if(useTime)spec.xTime={min:+cats[0],max:+cats[cats.length-1]};else spec.xLabels=cats;const ax=drawCartesian(this,spec);const lineW=t.lineWidth;const prog=this._anim.progress;const xAt=useTime?(i)=>ax.xToPx(+cats[i]):(i)=>ax.px+((i+0.5)/cats.length)*ax.pw;this._steps=cats.map((_,i)=>xAt(i));e.clip(ax.px-lineW,ax.py-lineW,(ax.pw+lineW*2)*prog,ax.ph+lineW*2);const all=[];for(const s of on){const ser=series[s];const color=this._colorOf(s,ser);const pts=ser.values.map((v,i)=>({x:xAt(i),y:ax.yToPx(+v)}));all.push({pts,color,s,ser});if(interp==='smooth')e.smooth(pts,M.bezierControlPoints(pts),{stroke:color,lineWidth:lineW});else if(interp==='linear')e.poly(pts,{stroke:color,lineWidth:lineW,cap:'round',join:'round'});else e.step(pts,'before',{stroke:color,lineWidth:lineW});if(pts.length<=50)e.circles(pts,color,t.pointRadius);}
185
197
  e.unclip();this._tipNow=null;const cx=this._cursor?.x;if(cx!=null&&cx>=ax.px-4&&cx<=ax.px+ax.pw+4&&all.length){let idx=0,best=Infinity;for(let i=0;i<cats.length;i++){const d=Math.abs(this._steps[i]-cx);if(d<best){best=d;idx=i;}}
186
198
  const xPx=this._steps[idx];e.poly([{x:xPx,y:ax.py},{x:xPx,y:ax.py+ax.ph}],{stroke:t.muted,lineWidth:1,dash:[3,3]});const rows=[];for(const sp of all){const p=sp.pts[idx];if(!p)continue;e.circle(p.x,p.y,t.pointRadius+2,{fill:t.bg,stroke:sp.color,lineWidth:2});rows.push({color:sp.color,name:sp.ser.name||'Series '+(sp.s+1),value:this._fmt(+sp.ser.values[idx])});}
187
- const title=useTime?new Date(+cats[idx]).toLocaleString():String(cats[idx]);this._tipNow={title,rows};this._hover={_id:idx,index:idx};this.tooltip.show(xPx,ax.py+8,this._tipNow,t);}else if(this._cursor){this.tooltip.hide();}}}
199
+ const title=this._lab(cats[idx],useTime);this._tipNow={title,rows};this._hover={_id:idx,index:idx,label:cats[idx]};this.tooltip.show(xPx,ax.py+8,this._tipNow,t);}else if(this._cursor){this.tooltip.hide();}}}
188
200
  function xyExtent(data){let xMin=Infinity,xMax=-Infinity,yMin=Infinity,yMax=-Infinity;for(const d of data){if(d.x<xMin)xMin=d.x;if(d.x>xMax)xMax=d.x;if(d.y<yMin)yMin=d.y;if(d.y>yMax)yMax=d.y;}
189
201
  return[xMin,xMax,yMin,yMax];}
190
- class ScatterChart extends Chart{get _kind(){return'Scatter chart';}
191
- _render(){const o=this.options;const data=o.data||[];if(!data.length)return this._empty();const e=this.engine;const t=this.theme;const[xMin,xMax,yMin,yMax]=xyExtent(data);const[xLo,xHi]=M.niceDomain(xMin,xMax,6);const[yLo,yHi]=M.niceDomain(yMin,yMax,5);const ax=drawCartesian(this,{yMin:yLo,yMax:yHi,xLinear:{min:xLo,max:xHi},yTickFormat:o.yTickFormat,xTickFormat:o.xTickFormat,});const prog=this._anim.progress;const groups=new Map();const defaultR=t.pointRadius+1;const hitsOn=data.length<=20000;for(let i=0;i<data.length;i++){const d=data[i];const cx=ax.xToPx(d.x);const cy=ax.yToPx(d.y);const r=(d.r||defaultR)*prog;const color=d.color||this._colorOf(d.group!=null?d.group:0);const key=color+':'+r.toFixed(1);if(!groups.has(key))groups.set(key,{color,r,items:[]});groups.get(key).items.push({x:cx,y:cy});if(hitsOn){this.hits.add({_id:i,x:cx-r-2,y:cy-r-2,w:r*2+4,h:r*2+4,tipX:cx,tipY:cy-r,data:d,index:i,tooltip:`(${this._fmt(d.x)}, ${this._fmt(d.y)})${d.label ? ` · ${d.label}` : ''}`,});}}
202
+ class ScatterChart extends Chart{static get meta(){return{id:'scatter',name:'ScatterChart',aria:'Scatter chart',data:'points',options:['labelFormat','min','max']};}
203
+ _render(){const o=this.options;const data=o.data||[];if(!data.length)return this._empty();const e=this.engine;const t=this.theme;const[xMin,xMax,yMin,yMax]=xyExtent(data);const[xLo,xHi]=M.niceDomain(xMin,xMax,6);const[yLo,yHi]=valueDomain(o,yMin,yMax);const ax=drawCartesian(this,{yMin:yLo,yMax:yHi,xLinear:{min:xLo,max:xHi},});const prog=this._anim.progress;const groups=new Map();const defaultR=t.pointRadius+1;const hitsOn=data.length<=20000;for(let i=0;i<data.length;i++){const d=data[i];const cx=ax.xToPx(d.x);const cy=ax.yToPx(d.y);const r=(d.size||defaultR)*prog;const color=d.color||this._colorOf(d.group!=null?d.group:0);const key=color+':'+r.toFixed(1);if(!groups.has(key))groups.set(key,{color,r,items:[]});groups.get(key).items.push({x:cx,y:cy});if(hitsOn){this.hits.add({_id:i,x:cx-r-2,y:cy-r-2,w:r*2+4,h:r*2+4,tipX:cx,tipY:cy-r,index:i,label:d.label,value:d.y,row:d,tooltip:`${d.label ? d.label + ' ' : ''}(${this._xFmt(d.x)}, ${this._fmt(d.y)})`,});}}
192
204
  e.withAlpha(0.7,()=>{for(const{color,r,items}of groups.values())e.circles(items,color,r);});if(this._hover){const h=this._hover;e.circle(h.tipX,h.y+h.h/2,h.w/2,{stroke:t.text,lineWidth:1.5});}}}
193
- class BubbleChart extends Chart{get _kind(){return'Bubble chart';}
194
- _render(){const o=this.options;const data=o.data||[];if(!data.length)return this._empty();const e=this.engine;const t=this.theme;const[xMin,xMax,yMin,yMax]=xyExtent(data);let zMax=0;for(const d of data)if((d.z||0)>zMax)zMax=d.z;const px=(xMax-xMin||1)*0.08,py=(yMax-yMin||1)*0.1;const[xLo,xHi]=M.niceDomain(xMin-px,xMax+px,6);const[yLo,yHi]=M.niceDomain(yMin-py,yMax+py,5);const ax=drawCartesian(this,{yMin:yLo,yMax:yHi,xLinear:{min:xLo,max:xHi},yTickFormat:o.yTickFormat,xTickFormat:o.xTickFormat,});const prog=this._anim.progress;const maxR=o.maxRadius||32,minR=o.minRadius||4;for(let i=0;i<data.length;i++){const d=data[i];const cx=ax.xToPx(d.x);const cy=ax.yToPx(d.y);const r=(minR+Math.sqrt((d.z||0)/Math.max(1e-9,zMax))*(maxR-minR))*prog;const color=d.color||this._colorOf(d.group!=null?d.group:i);const hover=this._hover&&this._hover._id===i;e.circle(cx,cy,r,{fill:M.colorAlpha(color,hover?0.75:0.5),stroke:color,lineWidth:1.5});this.hits.add({_id:i,x:cx-r,y:cy-r,w:r*2,h:r*2,test:(x,y)=>(x-cx)**2+(y-cy)**2<=r*r,tipX:cx,tipY:cy-r,data:d,index:i,tooltip:`${d.label ? d.label + ' ' : ''}(${this._fmt(d.x)}, ${this._fmt(d.y)}) · z=${this._fmt(d.z)}`,});}}}
205
+ class BubbleChart extends Chart{static get meta(){return{id:'bubble',name:'BubbleChart',aria:'Bubble chart',data:'points',options:['labelFormat','min','max','radius']};}
206
+ _render(){const o=this.options;const data=o.data||[];if(!data.length)return this._empty();const e=this.engine;const t=this.theme;const[xMin,xMax,yMin,yMax]=xyExtent(data);let zMax=0;for(const d of data)if((d.size||0)>zMax)zMax=d.size;const px=(xMax-xMin||1)*0.08,py=(yMax-yMin||1)*0.1;const[xLo,xHi]=M.niceDomain(xMin-px,xMax+px,6);const[yLo,yHi]=valueDomain(o,yMin-py,yMax+py);const ax=drawCartesian(this,{yMin:yLo,yMax:yHi,xLinear:{min:xLo,max:xHi},});const prog=this._anim.progress;const[minR,maxR]=Array.isArray(o.radius)?o.radius:[4,32];for(let i=0;i<data.length;i++){const d=data[i];const cx=ax.xToPx(d.x);const cy=ax.yToPx(d.y);const r=(minR+Math.sqrt((d.size||0)/Math.max(1e-9,zMax))*(maxR-minR))*prog;const color=d.color||this._colorOf(d.group!=null?d.group:i);const hover=this._hover&&this._hover._id===i;e.circle(cx,cy,r,{fill:M.colorAlpha(color,hover?0.75:0.5),stroke:color,lineWidth:1.5});this.hits.add({_id:i,x:cx-r,y:cy-r,w:r*2,h:r*2,test:(x,y)=>(x-cx)**2+(y-cy)**2<=r*r,tipX:cx,tipY:cy-r,index:i,label:d.label,value:d.y,row:d,tooltip:`${d.label ? d.label + ' ' : ''}(${this._xFmt(d.x)}, ${this._fmt(d.y)}) · ${this._fmt(d.size)}`,});}}}
195
207
  function inSector(cx,cy,rIn,rOut,a0,a1){return(x,y)=>{const dx=x-cx,dy=y-cy;const d=dx*dx+dy*dy;if(d<rIn*rIn||d>rOut*rOut)return false;let a=Math.atan2(dy,dx);while(a<a0)a+=TAU;return a<=a1;};}
196
- class PieChart extends Chart{get _kind(){return'Pie chart';}
197
- _legend(){const o=this.options;const labelOf=o.label||((d)=>d.label);return toRows(o.data).map((d,i)=>({label:String(labelOf(d,i)),color:this._colorOf(i,d)}));}
198
- _render(){const o=this.options;const data=toRows(o.data);if(!data.length)return this._empty();const e=this.engine;const t=this.theme;const labelOf=o.label||((d)=>d.label);const valueOf=o.value||((d)=>d.value);const vals=data.map((d,i)=>(this._isOff(i)?0:Math.max(0,+valueOf(d,i)||0)));const total=vals.reduce((a,v)=>a+v,0);if(total<=0)return this._empty();const cx=e.w/2,cy=e.h/2;const R=Math.max(4,Math.min(e.w,e.h)/2-12);const ir=o.innerRadius;const innerR=ir==null||ir===false?0:typeof ir==='number'?(ir<=1?R*ir:ir):R*0.55;const prog=this._anim.progress;let a0=-Math.PI/2;for(let i=0;i<data.length;i++){const v=vals[i];if(!v)continue;const a1=a0+(v/total)*TAU*prog;const color=this._colorOf(i,data[i]);const hover=this._hover&&this._hover._id===i;const mid=(a0+a1)/2;const off=hover?6:0;e.arc(cx+Math.cos(mid)*off,cy+Math.sin(mid)*off,innerR,R,a0,a1,{fill:color,stroke:data.length>1?t.bg:null,lineWidth:1.5,});const midR=(innerR+R)/2;this.hits.add({_id:i,x:cx-R,y:cy-R,w:R*2,h:R*2,test:inSector(cx,cy,innerR,R+off,a0,a1),tipX:cx+Math.cos(mid)*midR,tipY:cy+Math.sin(mid)*midR-8,data:data[i],index:i,value:v,tooltip:`${labelOf(data[i], i)}: ${this._fmt(v)} (${((v / total) * 100).toFixed(1)}%)`,});a0=a1;}
199
- if(innerR>0&&o.centerLabel!==false){const h=this._hover;const center=h?`${((vals[h._id] / total) * 100).toFixed(0)}%`:o.totalLabel||this._fmt(total);e.text(center,cx,cy,{fill:t.text,font:`600 ${Math.max(12, Math.min(t.fontSize * 1.8, innerR / 2.2))}px ${t.font}`,align:'center',baseline:'middle',});}}}
200
- class RadarChart extends Chart{get _kind(){return'Radar chart';}
201
- _legend(){return this._seriesLegend(this.options.series||[]);}
202
- _render(){const o=this.options;const series=o.series||[];const axes=o.axes||[];if(!series.length||!axes.length)return this._empty();const e=this.engine;const t=this.theme;const font=`${t.fontSize}px ${t.font}`;let labelW=0;for(const a of axes)labelW=Math.max(labelW,e.measure(String(a),font));const cx=e.w/2;const cy=e.h/2;const R=Math.max(10,Math.min(e.w/2-labelW-18,e.h/2-t.fontSize-16));const N=axes.length;const angle=(i)=>-Math.PI/2+(i/N)*TAU;const on=series.map((s,i)=>i).filter((i)=>!this._isOff(i));const max=range(on.map((s)=>series[s].values),0)[1];const niceMax=M.niceDomain(0,max,5)[1]||1;const ctx=e.ctx;ctx.strokeStyle=t.grid;ctx.lineWidth=1;for(const v of M.niceTicks(0,niceMax,4)){if(v<=0)continue;const r=(v/niceMax)*R;ctx.beginPath();for(let i=0;i<=N;i++){const a=angle(i%N);if(i===0)ctx.moveTo(cx+Math.cos(a)*r,cy+Math.sin(a)*r);else ctx.lineTo(cx+Math.cos(a)*r,cy+Math.sin(a)*r);}
208
+ class PieChart extends Chart{static get meta(){return{id:'pie',name:'PieChart',aria:'Pie chart',data:'rows',options:['label','value','innerRadius','center']};}
209
+ _legend(){const o=this.options;const labelOf=this._fn('label')||((d)=>d.label);return toRows(o.data).map((d,i)=>({label:String(labelOf(d,i)),color:this._colorOf(i,d)}));}
210
+ _render(){const o=this.options;const data=toRows(o.data);if(!data.length)return this._empty();const e=this.engine;const t=this.theme;const labelOf=this._fn('label')||((d)=>d.label);const valueOf=this._fn('value')||((d)=>d.value);const raw=data.map((d,i)=>+valueOf(d,i)||0);const neg=raw.findIndex((v)=>v<0);if(neg>=0)warn(`PieChart: values can't be negative — "${labelOf(data[neg], neg)}" is ${raw[neg]}. It is drawn as 0.`);const vals=raw.map((v,i)=>(this._isOff(i)?0:Math.max(0,v)));const total=vals.reduce((a,v)=>a+v,0);if(total<=0)return this._empty();const cx=e.w/2,cy=e.h/2;const R=Math.max(4,Math.min(e.w,e.h)/2-12);const ir=+o.innerRadius||0;if(ir>0.9)warn(`PieChart: innerRadius is a ratio from 0 to 0.9 (0.6 = donut) — got ${ir}.`);const innerR=R*Math.min(0.9,Math.max(0,ir));const prog=this._anim.progress;let a0=-Math.PI/2;for(let i=0;i<data.length;i++){const v=vals[i];if(!v)continue;const a1=a0+(v/total)*TAU*prog;const color=this._colorOf(i,data[i]);const hover=this._hover&&this._hover._id===i;const mid=(a0+a1)/2;const off=hover?6:0;e.arc(cx+Math.cos(mid)*off,cy+Math.sin(mid)*off,innerR,R,a0,a1,{fill:color,stroke:data.length>1?t.bg:null,lineWidth:1.5,});const midR=(innerR+R)/2;this.hits.add({_id:i,x:cx-R,y:cy-R,w:R*2,h:R*2,test:inSector(cx,cy,innerR,R+off,a0,a1),tipX:cx+Math.cos(mid)*midR,tipY:cy+Math.sin(mid)*midR-8,index:i,label:labelOf(data[i],i),value:v,row:data[i],tooltip:`${this._lab(labelOf(data[i], i))}: ${this._fmt(v)} (${((v / total) * 100).toFixed(1)}%)`,});a0=a1;}
211
+ if(innerR>0&&o.center!==false){const h=this._hover;const center=h?`${((vals[h._id] / total) * 100).toFixed(0)}%`:o.center??this._fmt(total);e.text(center,cx,cy,{fill:t.text,font:`600 ${Math.max(12, Math.min(t.fontSize * 1.8, innerR / 2.2))}px ${t.font}`,align:'center',baseline:'middle',});}}}
212
+ class RadarChart extends Chart{static get meta(){return{id:'radar',name:'RadarChart',aria:'Radar chart',data:'series',options:['labelFormat','max']};}
213
+ _legend(){return this._seriesLegend(this.options.data?.series||[]);}
214
+ _render(){const o=this.options;const{categories:axes=[],series=[]}=o.data||{};if(!series.length||!axes.length)return this._empty();const e=this.engine;const t=this.theme;const font=`${t.fontSize}px ${t.font}`;let labelW=0;const names=axes.map((a)=>this._lab(a));for(const a of names)labelW=Math.max(labelW,e.measure(a,font));const cx=e.w/2;const cy=e.h/2;const R=Math.max(10,Math.min(e.w/2-labelW-18,e.h/2-t.fontSize-16));const N=axes.length;const angle=(i)=>-Math.PI/2+(i/N)*TAU;const on=series.map((s,i)=>i).filter((i)=>!this._isOff(i));const max=range(on.map((s)=>series[s].values),0)[1];const niceMax=o.max??(M.niceDomain(0,max,5)[1]||1);const ctx=e.ctx;ctx.strokeStyle=t.grid;ctx.lineWidth=1;for(const v of M.niceTicks(0,niceMax,4)){if(v<=0)continue;const r=(v/niceMax)*R;ctx.beginPath();for(let i=0;i<=N;i++){const a=angle(i%N);if(i===0)ctx.moveTo(cx+Math.cos(a)*r,cy+Math.sin(a)*r);else ctx.lineTo(cx+Math.cos(a)*r,cy+Math.sin(a)*r);}
203
215
  ctx.stroke();}
204
216
  ctx.beginPath();for(let i=0;i<N;i++){const a=angle(i);ctx.moveTo(cx,cy);ctx.lineTo(cx+Math.cos(a)*R,cy+Math.sin(a)*R);}
205
- ctx.stroke();for(let i=0;i<N;i++){const a=angle(i);e.text(String(axes[i]),cx+Math.cos(a)*(R+10),cy+Math.sin(a)*(R+10),{fill:t.muted,font,align:Math.cos(a)>0.3?'left':Math.cos(a)<-0.3?'right':'center',baseline:Math.sin(a)>0.3?'top':Math.sin(a)<-0.3?'bottom':'middle',});}
206
- const prog=this._anim.progress;for(const s of on){const ser=series[s];const color=this._colorOf(s,ser);const pts=[];for(let i=0;i<N;i++){const v=+ser.values[i]||0;const r=((v*prog)/niceMax)*R;const a=angle(i);const p={x:cx+Math.cos(a)*r,y:cy+Math.sin(a)*r};pts.push(p);this.hits.add({_id:`${s}-${i}`,x:p.x-7,y:p.y-7,w:14,h:14,tipX:p.x,tipY:p.y-4,index:i,series:s,value:v,tooltip:{title:String(axes[i]),rows:[{color,name:ser.name||'Series '+(s+1),value:this._fmt(v)}]},});}
217
+ ctx.stroke();for(let i=0;i<N;i++){const a=angle(i);e.text(names[i],cx+Math.cos(a)*(R+10),cy+Math.sin(a)*(R+10),{fill:t.muted,font,align:Math.cos(a)>0.3?'left':Math.cos(a)<-0.3?'right':'center',baseline:Math.sin(a)>0.3?'top':Math.sin(a)<-0.3?'bottom':'middle',});}
218
+ const prog=this._anim.progress;for(const s of on){const ser=series[s];const color=this._colorOf(s,ser);const pts=[];for(let i=0;i<N;i++){const v=+ser.values[i]||0;const r=Math.min(1,(v*prog)/niceMax)*R;const a=angle(i);const p={x:cx+Math.cos(a)*r,y:cy+Math.sin(a)*r};pts.push(p);this.hits.add({_id:`${s}-${i}`,x:p.x-7,y:p.y-7,w:14,h:14,tipX:p.x,tipY:p.y-4,index:i,label:axes[i],series:s,value:v,row:ser,tooltip:{title:names[i],rows:[{color,name:ser.name||'Series '+(s+1),value:this._fmt(v)}]},});}
207
219
  e.poly(pts,{fill:M.colorAlpha(color,0.2),stroke:color,lineWidth:2,close:true,join:'round'});e.circles(pts,color,t.pointRadius);}
208
220
  if(this._hover){const h=this._hover;e.circle(h.x+7,h.y+7,t.pointRadius+3,{stroke:t.text,lineWidth:1.5});}}}
209
- class PolarChart extends Chart{get _kind(){return'Polar chart';}
210
- _render(){const o=this.options;const data=toRows(o.data);if(!data.length)return this._empty();const e=this.engine;const t=this.theme;const labelOf=o.label||((d)=>d.label);const valueOf=o.value||((d)=>d.value);const N=data.length;const font=`${t.fontSize}px ${t.font}`;const cx=e.w/2,cy=e.h/2;const R=Math.max(10,Math.min(e.w,e.h)/2-t.fontSize-14);let max=0;for(let i=0;i<N;i++)max=Math.max(max,+valueOf(data[i],i)||0);const prog=this._anim.progress;const sqrt=o.scale!=='linear';e.circle(cx,cy,R,{stroke:t.grid,lineWidth:1});for(let i=0;i<N;i++){const v=Math.max(0,+valueOf(data[i],i)||0);const a0=-Math.PI/2+(i/N)*TAU;const a1=a0+(TAU/N)*0.92;const ratio=max?v/max:0;const r=(sqrt?Math.sqrt(ratio):ratio)*R*prog;const color=this._colorOf(i,data[i]);const hover=this._hover&&this._hover._id===i;e.arc(cx,cy,0,r+(hover?4:0),a0,a1,{fill:color});const mid=(a0+a1)/2;const c=Math.cos(mid),s=Math.sin(mid);e.text(String(labelOf(data[i],i)),cx+c*(R+8),cy+s*(R+8),{fill:t.muted,font,align:c>0.3?'left':c<-0.3?'right':'center',baseline:s>0.3?'top':s<-0.3?'bottom':'middle',});this.hits.add({_id:i,x:cx-R,y:cy-R,w:R*2,h:R*2,test:inSector(cx,cy,0,Math.max(r,12),a0,a1),tipX:cx+c*r*0.7,tipY:cy+s*r*0.7-6,data:data[i],index:i,value:v,tooltip:`${labelOf(data[i], i)}: ${this._fmt(v)}`,});}}}
211
- class GaugeChart extends Chart{get _kind(){return'Gauge chart';}
212
- setValue(v){this._from=this._shown??0;return this.update({value:v});}
213
- _render(){const e=this.engine;const t=this.theme;const o=this.options;const min=o.min||0;const max=o.max!=null?o.max:100;const target=Math.max(min,Math.min(max,+o.value||0));const from=this._from??min;const val=from+(target-from)*this._anim.progress;this._shown=val;const cx=e.w/2;const cy=e.h*0.68;const R=Math.max(10,Math.min(e.w/2-8,e.h*0.62));const r=R*0.8;const a0=Math.PI*1.1;const a1=Math.PI*1.9;const color=o.color||this.palette[0];e.arc(cx,cy,r,R,a0,a1,{fill:t.grid});const aEnd=a0+((a1-a0)*(val-min))/(max-min||1);if(aEnd>a0)e.arc(cx,cy,r,R,a0,aEnd,{fill:color});const txt=(o.format?this._fmt(val):String(Math.round(val)))+(o.unit||'');e.text(txt,cx,cy-R*0.08,{fill:t.text,font:`700 ${Math.max(12, R / 2.6)}px ${t.font}`,align:'center',baseline:'middle',});if(o.label){e.text(o.label,cx,cy+R*0.22,{fill:t.muted,font:`${t.fontSize}px ${t.font}`,align:'center',baseline:'top'});}
214
- this.canvas.setAttribute('aria-label',`${o.ariaLabel || o.label || o.title || 'Gauge'}: ${txt}`);}}
215
- class HeatmapChart extends Chart{get _kind(){return'Heatmap chart';}
216
- _render(){const o=this.options;const data=o.data||[];const xLabels=o.xLabels||[];const yLabels=o.yLabels||[];if(!data.length||!xLabels.length||!yLabels.length)return this._empty();const e=this.engine;const t=this.theme;const font=`${t.fontSize}px ${t.font}`;let labelW=0;for(const s of yLabels)labelW=Math.max(labelW,e.measure(String(s),font));const px=Math.ceil(labelW)+12,py=8;const pw=Math.max(1,e.w-px-8);const ph=Math.max(1,e.h-py-t.fontSize-14);let vMax=-Infinity,vMin=Infinity;for(const d of data){if(d.value>vMax)vMax=d.value;if(d.value<vMin)vMin=d.value;}
217
- const cellW=pw/xLabels.length;const cellH=ph/yLabels.length;const lo=o.colorLow||M.colorAlpha(this.palette[0],0.08);const hi=o.colorHigh||this.palette[0];const gap=cellW>6&&cellH>6?1:0;const prog=this._anim.progress;const xi=new Map(xLabels.map((l,i)=>[l,i]));const yi=new Map(yLabels.map((l,i)=>[l,i]));for(let k=0;k<data.length;k++){const d=data[k];const cx=typeof d.x==='number'?d.x:xi.get(d.x)??-1;const cy=typeof d.y==='number'?d.y:yi.get(d.y)??-1;if(cx<0||cy<0)continue;const x=px+cx*cellW;const y=py+cy*cellH;const n=vMax>vMin?(d.value-vMin)/(vMax-vMin):0.5;e.rect(x+gap,y+gap,cellW-gap*2,cellH-gap*2,{fill:M.colorMix(lo,hi,n*prog),radius:gap?2:0});this.hits.add({_id:k,x,y,w:cellW,h:cellH,tipX:x+cellW/2,tipY:y,data:d,index:k,value:d.value,tooltip:`${xLabels[cx]} · ${yLabels[cy]}: ${this._fmt(d.value)}`,});}
218
- let lastEnd=-Infinity;for(let i=0;i<xLabels.length;i++){const txt=String(xLabels[i]);const w=e.measure(txt,font);const x=px+(i+0.5)*cellW;if(x-w/2<lastEnd+4)continue;lastEnd=x+w/2;e.text(txt,x,py+ph+6,{fill:t.muted,font,align:'center',baseline:'top'});}
219
- const step=Math.ceil((t.fontSize+2)/cellH);for(let i=0;i<yLabels.length;i+=step){e.text(String(yLabels[i]),px-6,py+(i+0.5)*cellH,{fill:t.muted,font,align:'right',baseline:'middle'});}
221
+ class PolarChart extends Chart{static get meta(){return{id:'polar',name:'PolarChart',aria:'Polar chart',data:'rows',options:['label','value','scale'],enums:{scale:['sqrt','linear']}};}
222
+ _render(){const o=this.options;const data=toRows(o.data);if(!data.length)return this._empty();const e=this.engine;const t=this.theme;const labelOf=this._fn('label')||((d)=>d.label);const valueOf=this._fn('value')||((d)=>d.value);const N=data.length;const font=`${t.fontSize}px ${t.font}`;const cx=e.w/2,cy=e.h/2;const R=Math.max(10,Math.min(e.w,e.h)/2-t.fontSize-14);let max=0;for(let i=0;i<N;i++)max=Math.max(max,+valueOf(data[i],i)||0);const prog=this._anim.progress;const sqrt=o.scale!=='linear';e.circle(cx,cy,R,{stroke:t.grid,lineWidth:1});for(let i=0;i<N;i++){const v=Math.max(0,+valueOf(data[i],i)||0);const a0=-Math.PI/2+(i/N)*TAU;const a1=a0+(TAU/N)*0.92;const ratio=max?v/max:0;const r=(sqrt?Math.sqrt(ratio):ratio)*R*prog;const color=this._colorOf(i,data[i]);const hover=this._hover&&this._hover._id===i;e.arc(cx,cy,0,r+(hover?4:0),a0,a1,{fill:color});const mid=(a0+a1)/2;const c=Math.cos(mid),s=Math.sin(mid);e.text(String(labelOf(data[i],i)),cx+c*(R+8),cy+s*(R+8),{fill:t.muted,font,align:c>0.3?'left':c<-0.3?'right':'center',baseline:s>0.3?'top':s<-0.3?'bottom':'middle',});this.hits.add({_id:i,x:cx-R,y:cy-R,w:R*2,h:R*2,test:inSector(cx,cy,0,Math.max(r,12),a0,a1),tipX:cx+c*r*0.7,tipY:cy+s*r*0.7-6,index:i,label:labelOf(data[i],i),value:v,row:data[i],tooltip:`${labelOf(data[i], i)}: ${this._fmt(v)}`,});}}}
223
+ class GaugeChart extends Chart{static get meta(){return{id:'gauge',name:'GaugeChart',aria:'Gauge',data:'number',options:['min','max','unit'],renamed:{value:'data',label:'title'},};}
224
+ update(patch={}){if('data'in patch)this._from=this._shown;return super.update(patch);}
225
+ _render(){const e=this.engine;const t=this.theme;const o=this.options;const min=o.min??0;const max=o.max??100;const target=Math.max(min,Math.min(max,+o.data||0));const from=this._from??min;const val=from+(target-from)*this._anim.progress;this._shown=val;const cx=e.w/2;const cy=e.h*0.68;const R=Math.max(10,Math.min(e.w/2-8,e.h*0.62));const r=R*0.8;const a0=Math.PI*1.1;const a1=Math.PI*1.9;const color=this.palette[0];e.arc(cx,cy,r,R,a0,a1,{fill:t.grid});const aEnd=a0+((a1-a0)*(val-min))/(max-min||1);if(aEnd>a0)e.arc(cx,cy,r,R,a0,aEnd,{fill:color});const txt=(o.format?this._fmt(val):String(Math.round(val)))+(o.unit||'');e.text(txt,cx,cy-R*0.08,{fill:t.text,font:`700 ${Math.max(12, R / 2.6)}px ${t.font}`,align:'center',baseline:'middle',});if(o.title){e.text(o.title,cx,cy+R*0.22,{fill:t.muted,font:`${t.fontSize}px ${t.font}`,align:'center',baseline:'top'});}
226
+ this.canvas.setAttribute('aria-label',`${o.title || 'Gauge'}: ${txt}`);}}
227
+ class HeatmapChart extends Chart{static get meta(){return{id:'heatmap',name:'HeatmapChart',aria:'Heatmap',data:'matrix',options:['labelFormat'],colors:'an object like { low: \'#eef\', high: \'#36c\' }'};}
228
+ _render(){const o=this.options;const{x:xLabels=[],y:yLabels=[],values=[]}=o.data||{};if(!values.length||!xLabels.length||!yLabels.length)return this._empty();const e=this.engine;const t=this.theme;const font=`${t.fontSize}px ${t.font}`;let labelW=0;const yText=Array.from(yLabels,(l)=>this._lab(l));for(const s of yText)labelW=Math.max(labelW,e.measure(s,font));const px=Math.ceil(labelW)+12,py=8;const pw=Math.max(1,e.w-px-8);const ph=Math.max(1,e.h-py-t.fontSize-14);let vMax=-Infinity,vMin=Infinity;for(const row of values){for(const v of row||[]){if(v>vMax)vMax=v;if(v<vMin)vMin=v;}}
229
+ const cellW=pw/xLabels.length;const cellH=ph/yLabels.length;const c=o.colors||{};const hi=c.high||this.palette[0];const lo=c.low||M.colorAlpha(hi,0.08);const gap=cellW>6&&cellH>6?1:0;const prog=this._anim.progress;for(let cy=0;cy<yLabels.length;cy++){const row=values[cy]||[];for(let cx=0;cx<xLabels.length;cx++){const v=row[cx];if(v==null||v!==v)continue;const x=px+cx*cellW;const y=py+cy*cellH;const n=vMax>vMin?(v-vMin)/(vMax-vMin):0.5;e.rect(x+gap,y+gap,cellW-gap*2,cellH-gap*2,{fill:M.colorMix(lo,hi,n*prog),radius:gap?2:0});this.hits.add({_id:cy*xLabels.length+cx,x,y,w:cellW,h:cellH,tipX:x+cellW/2,tipY:y,index:cx,series:cy,label:xLabels[cx],value:v,row:{x:xLabels[cx],y:yLabels[cy],value:v},tooltip:`${this._lab(xLabels[cx])} · ${yText[cy]}: ${this._fmt(v)}`,});}}
230
+ let lastEnd=-Infinity;for(let i=0;i<xLabels.length;i++){const txt=this._lab(xLabels[i]);const w=e.measure(txt,font);const x=px+(i+0.5)*cellW;if(x-w/2<lastEnd+4)continue;lastEnd=x+w/2;e.text(txt,x,py+ph+6,{fill:t.muted,font,align:'center',baseline:'top'});}
231
+ const step=Math.ceil((t.fontSize+2)/cellH);for(let i=0;i<yLabels.length;i+=step){e.text(yText[i],px-6,py+(i+0.5)*cellH,{fill:t.muted,font,align:'right',baseline:'middle'});}
220
232
  if(this._hover){const r=this._hover;e.rect(r.x+0.5,r.y+0.5,r.w-1,r.h-1,{stroke:t.text,lineWidth:1.5});}}}
221
- class BoxplotChart extends Chart{get _kind(){return'Boxplot chart';}
233
+ class BoxplotChart extends Chart{static get meta(){return{id:'boxplot',name:'BoxplotChart',aria:'Box plot',data:'boxes',options:['labelFormat','min','max']};}
222
234
  _render(){const o=this.options;const data=o.data||[];if(!data.length)return this._empty();const e=this.engine;let yMax=-Infinity,yMin=Infinity;for(const d of data){if(d.max>yMax)yMax=d.max;if(d.min<yMin)yMin=d.min;if(d.outliers)for(const v of d.outliers){if(v>yMax)yMax=v;if(v<yMin)yMin=v;}}
223
- const[yLo,yHi]=M.niceDomain(yMin,yMax,5);const ax=drawCartesian(this,{yMin:yLo,yMax:yHi,xLabels:data.map((d)=>d.label),yTickFormat:o.yTickFormat});const slot=ax.pw/data.length;const boxW=Math.min(40,slot*0.5);const prog=this._anim.progress;for(let i=0;i<data.length;i++){const d=data[i];const cx=ax.px+(i+0.5)*slot;const med=ax.yToPx(d.median);const y=(v)=>med+(ax.yToPx(v)-med)*prog;const color=this._colorOf(i,d);const hover=this._hover&&this._hover._id===i;e.poly([{x:cx,y:y(d.max)},{x:cx,y:y(d.min)}],{stroke:color,lineWidth:1});e.poly([{x:cx-boxW/3,y:y(d.max)},{x:cx+boxW/3,y:y(d.max)}],{stroke:color,lineWidth:1});e.poly([{x:cx-boxW/3,y:y(d.min)},{x:cx+boxW/3,y:y(d.min)}],{stroke:color,lineWidth:1});e.rect(cx-boxW/2,y(d.q3),boxW,y(d.q1)-y(d.q3),{fill:M.colorAlpha(color,hover?0.6:0.4),stroke:color,lineWidth:1.5,});e.poly([{x:cx-boxW/2,y:med},{x:cx+boxW/2,y:med}],{stroke:color,lineWidth:2});if(d.outliers)e.circles(d.outliers.map((v)=>({x:cx,y:y(v)})),color,2);const top=ax.yToPx(d.max),bot=ax.yToPx(d.min);this.hits.add({_id:i,x:cx-boxW/2,y:top,w:boxW,h:Math.max(4,bot-top),tipX:cx,tipY:ax.yToPx(d.q3),data:d,index:i,tooltip:{title:String(d.label),rows:[['max',d.max],['Q3',d.q3],['median',d.median],['Q1',d.q1],['min',d.min]].map(([name,v])=>({name,value:this._fmt(v)})),},});}}}
224
- class FunnelChart extends Chart{get _kind(){return'Funnel chart';}
225
- _render(){const o=this.options;const data=toRows(o.data);if(!data.length)return this._empty();const e=this.engine;const t=this.theme;const labelOf=o.label||((d)=>d.label);const valueOf=o.value||((d)=>d.value);const N=data.length;const vals=data.map((d,i)=>Math.max(0,+valueOf(d,i)||0));const max=Math.max(1e-9,vals.reduce((a,v)=>(v>a?v:a),0));const first=vals[0]||max;const pad=12;const w=e.w-pad*2;const segH=(e.h-pad*2)/N;const cx=e.w/2;const prog=this._anim.progress;const font=`600 ${t.fontSize}px ${t.font}`;for(let i=0;i<N;i++){const v=vals[i];const wTop=(v/max)*w*prog;const wBot=i+1<N?(vals[i+1]/max)*w*prog:wTop*0.85;const yTop=pad+i*segH;const yBot=yTop+segH-2;const color=this._colorOf(i,data[i]);const hover=this._hover&&this._hover._id===i;e.withAlpha(hover?1:0.9,()=>e.poly([{x:cx-wTop/2,y:yTop},{x:cx+wTop/2,y:yTop},{x:cx+wBot/2,y:yBot},{x:cx-wBot/2,y:yBot},],{fill:color,close:true}));const txt=`${labelOf(data[i], i)} ${this._fmt(v)}`;const tw=e.measure(txt,font);const inside=tw+16<Math.min(wTop,wBot);e.text(txt,inside?cx:cx+Math.max(wTop,wBot)/2+8,yTop+segH/2,{fill:inside?o.labelColor||'#fff':t.text,font,align:inside?'center':'left',baseline:'middle',});this.hits.add({_id:i,x:cx-Math.max(wTop,8)/2,y:yTop,w:Math.max(wTop,8),h:segH,tipX:cx,tipY:yTop,data:data[i],index:i,value:v,tooltip:`${labelOf(data[i], i)}: ${this._fmt(v)} (${((v / first) * 100).toFixed(1)}%)`,});}}}
235
+ const[yLo,yHi]=valueDomain(o,yMin,yMax);const ax=drawCartesian(this,{yMin:yLo,yMax:yHi,xLabels:data.map((d)=>d.label)});const slot=ax.pw/data.length;const boxW=Math.min(40,slot*0.5);const prog=this._anim.progress;for(let i=0;i<data.length;i++){const d=data[i];const cx=ax.px+(i+0.5)*slot;const med=ax.yToPx(d.median);const y=(v)=>med+(ax.yToPx(v)-med)*prog;const color=this._colorOf(i,d);const hover=this._hover&&this._hover._id===i;e.poly([{x:cx,y:y(d.max)},{x:cx,y:y(d.min)}],{stroke:color,lineWidth:1});e.poly([{x:cx-boxW/3,y:y(d.max)},{x:cx+boxW/3,y:y(d.max)}],{stroke:color,lineWidth:1});e.poly([{x:cx-boxW/3,y:y(d.min)},{x:cx+boxW/3,y:y(d.min)}],{stroke:color,lineWidth:1});e.rect(cx-boxW/2,y(d.q3),boxW,y(d.q1)-y(d.q3),{fill:M.colorAlpha(color,hover?0.6:0.4),stroke:color,lineWidth:1.5,});e.poly([{x:cx-boxW/2,y:med},{x:cx+boxW/2,y:med}],{stroke:color,lineWidth:2});if(d.outliers)e.circles(d.outliers.map((v)=>({x:cx,y:y(v)})),color,2);const top=ax.yToPx(d.max),bot=ax.yToPx(d.min);this.hits.add({_id:i,x:cx-boxW/2,y:top,w:boxW,h:Math.max(4,bot-top),tipX:cx,tipY:ax.yToPx(d.q3),index:i,label:d.label,value:d.median,row:d,tooltip:{title:this._lab(d.label),rows:[['max',d.max],['Q3',d.q3],['median',d.median],['Q1',d.q1],['min',d.min]].map(([name,v])=>({name,value:this._fmt(v)})),},});}}}
236
+ class FunnelChart extends Chart{static get meta(){return{id:'funnel',name:'FunnelChart',aria:'Funnel chart',data:'rows',options:['label','value','labelColor']};}
237
+ _render(){const o=this.options;const data=toRows(o.data);if(!data.length)return this._empty();const e=this.engine;const t=this.theme;const labelOf=this._fn('label')||((d)=>d.label);const valueOf=this._fn('value')||((d)=>d.value);const N=data.length;const vals=data.map((d,i)=>Math.max(0,+valueOf(d,i)||0));const max=Math.max(1e-9,vals.reduce((a,v)=>(v>a?v:a),0));const first=vals[0]||max;const pad=12;const w=e.w-pad*2;const segH=(e.h-pad*2)/N;const cx=e.w/2;const prog=this._anim.progress;const font=`600 ${t.fontSize}px ${t.font}`;for(let i=0;i<N;i++){const v=vals[i];const wTop=(v/max)*w*prog;const wBot=i+1<N?(vals[i+1]/max)*w*prog:wTop*0.85;const yTop=pad+i*segH;const yBot=yTop+segH-2;const color=this._colorOf(i,data[i]);const hover=this._hover&&this._hover._id===i;e.withAlpha(hover?1:0.9,()=>e.poly([{x:cx-wTop/2,y:yTop},{x:cx+wTop/2,y:yTop},{x:cx+wBot/2,y:yBot},{x:cx-wBot/2,y:yBot},],{fill:color,close:true}));const txt=`${labelOf(data[i], i)} ${this._fmt(v)}`;const tw=e.measure(txt,font);const inside=tw+16<Math.min(wTop,wBot);e.text(txt,inside?cx:cx+Math.max(wTop,wBot)/2+8,yTop+segH/2,{fill:inside?o.labelColor||'#fff':t.text,font,align:inside?'center':'left',baseline:'middle',});this.hits.add({_id:i,x:cx-Math.max(wTop,8)/2,y:yTop,w:Math.max(wTop,8),h:segH,tipX:cx,tipY:yTop,index:i,label:labelOf(data[i],i),value:v,row:data[i],tooltip:`${labelOf(data[i], i)}: ${this._fmt(v)} (${((v / first) * 100).toFixed(1)}%)`,});}}}
226
238
  function squarify(items,rect,place){let i=0;let{x,y,w,h}=rect;let rest=items.reduce((a,it)=>a+it.value,0);while(i<items.length&&rest>0&&w>=1&&h>=1){const side=Math.min(w,h);const scale=(w*h)/rest;let sum=0,best=Infinity,j=i;for(;j<items.length;j++){const s=sum+items[j].value;const len=(s*scale)/side;let worst=0;for(let k=i;k<=j;k++){const l=(items[k].value*scale)/len;worst=Math.max(worst,len/l,l/len);}
227
239
  if(worst>best)break;best=worst;sum=s;}
228
240
  if(j===i){sum=items[i].value;j=i+1;}
229
241
  const thick=(sum*scale)/side;let pos=0;for(let k=i;k<j;k++){const l=(items[k].value*scale)/thick;place(items[k],w>=h?{x,y:y+pos,w:thick,h:l}:{x:x+pos,y,w:l,h:thick});pos+=l;}
230
242
  if(w>=h){x+=thick;w-=thick;}else{y+=thick;h-=thick;}
231
243
  rest-=sum;i=j;}}
232
- class TreemapChart extends Chart{get _kind(){return'Treemap chart';}
233
- _render(){const o=this.options;const data=toRows(o.data);if(!data.length)return this._empty();const e=this.engine;const t=this.theme;const labelOf=o.label||((d)=>d.label);const valueOf=o.value||((d)=>d.value);const items=data.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})).filter((it)=>it.value>0).sort((a,b)=>b.value-a.value);if(!items.length)return this._empty();const prog=this._anim.progress;const fg=o.labelColor||'#fff';squarify(items,{x:0,y:0,w:e.w,h:e.h},(it,r)=>{const gap=1;const w=(r.w-gap*2)*prog;const h=(r.h-gap*2)*prog;const x=r.x+gap+(r.w-gap*2-w)/2;const y=r.y+gap+(r.h-gap*2-h)/2;if(w<1||h<1)return;const hover=this._hover&&this._hover._id===it._id;e.withAlpha(hover?0.85:1,()=>e.rect(x,y,w,h,{fill:it.color,radius:2}));if(w>48&&h>t.fontSize*2+12){e.text(it.label,x+6,y+6,{fill:fg,font:`600 ${t.fontSize}px ${t.font}`,align:'left',baseline:'top'});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'}));}
234
- this.hits.add({_id:it._id,x,y,w,h,tipX:x+w/2,tipY:y,data:it.data,index:it._id,value:it.value,tooltip:`${it.label}: ${this._fmt(it.value)}`,});});}}
235
- class Sparkline extends Chart{get _kind(){return'Sparkline';}
236
- _render(){const o=this.options;const data=o.data||[];if(!data.length)return;const e=this.engine;const t=this.theme;const valueOf=o.value||((v)=>(typeof v==='object'&&v?+v.value:+v));const values=data.map((d,i)=>valueOf(d,i));const[yMin,yMax]=range([values]);const yRange=yMax-yMin||1;const pad=Math.max(2,t.pointRadius+1);const w=e.w-pad*2;const h=e.h-pad*2;const points=values.map((v,i)=>({x:pad+(i/(values.length-1||1))*w,y:pad+h-((v-yMin)/yRange)*h,}));const color=o.color||this.palette[0];const cps=M.bezierControlPoints(points);e.clip(0,0,e.w*this._anim.progress,e.h);if(o.fill!==false){const grad=e.vGradient(`spark-${color}`,0,pad,pad+h,[[0,M.colorAlpha(color,0.3)],[1,M.colorAlpha(color,0)]]);e.smoothArea(points,cps,pad+h,grad);}
244
+ class TreemapChart extends Chart{static get meta(){return{id:'treemap',name:'TreemapChart',aria:'Treemap',data:'rows',options:['label','value','labelColor']};}
245
+ _render(){const o=this.options;const data=toRows(o.data);if(!data.length)return this._empty();const e=this.engine;const t=this.theme;const labelOf=this._fn('label')||((d)=>d.label);const valueOf=this._fn('value')||((d)=>d.value);const items=data.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})).filter((it)=>it.value>0).sort((a,b)=>b.value-a.value);if(!items.length)return this._empty();const prog=this._anim.progress;const fg=o.labelColor||'#fff';squarify(items,{x:0,y:0,w:e.w,h:e.h},(it,r)=>{const gap=1;const w=(r.w-gap*2)*prog;const h=(r.h-gap*2)*prog;const x=r.x+gap+(r.w-gap*2-w)/2;const y=r.y+gap+(r.h-gap*2-h)/2;if(w<1||h<1)return;const hover=this._hover&&this._hover._id===it._id;e.withAlpha(hover?0.85:1,()=>e.rect(x,y,w,h,{fill:it.color,radius:2}));if(w>48&&h>t.fontSize*2+12){e.text(it.label,x+6,y+6,{fill:fg,font:`600 ${t.fontSize}px ${t.font}`,align:'left',baseline:'top'});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'}));}
246
+ this.hits.add({_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,tooltip:`${it.label}: ${this._fmt(it.value)}`,});});}}
247
+ class SparklineChart extends Chart{static get meta(){return{id:'sparkline',name:'SparklineChart',aria:'Sparkline',data:'rows',options:['label','value','min','max','fill','lastDot']};}
248
+ _render(){const o=this.options;const data=toRows(o.data);if(!data.length)return;const e=this.engine;const t=this.theme;const valueOf=this._fn('value')||((d)=>d.value);const labelOf=this._fn('label')||((d)=>d.label);const values=data.map((d,i)=>+valueOf(d,i));const[lo,hi]=range([values]);const yMin=o.min??lo,yMax=o.max??hi;const yRange=yMax-yMin||1;const pad=Math.max(2,t.pointRadius+1);const w=e.w-pad*2;const h=e.h-pad*2;const points=values.map((v,i)=>({x:pad+(i/(values.length-1||1))*w,y:pad+h-((v-yMin)/yRange)*h,}));const color=this.palette[0];const cps=M.bezierControlPoints(points);e.clip(0,0,e.w*this._anim.progress,e.h);if(o.fill!==false){const grad=e.vGradient(`spark-${color}`,0,pad,pad+h,[[0,M.colorAlpha(color,0.3)],[1,M.colorAlpha(color,0)]]);e.smoothArea(points,cps,pad+h,grad);}
237
249
  e.smooth(points,cps,{stroke:color,lineWidth:t.lineWidth});e.unclip();if(o.lastDot!==false){const last=points[points.length-1];e.circle(last.x,last.y,t.pointRadius+1,{fill:color});}
238
- const n=points.length;points.forEach((p,i)=>{const l=i?(points[i-1].x+p.x)/2:0;const r=i<n-1?(points[i+1].x+p.x)/2:e.w;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])});});if(this._hover){const p=points[this._hover._id];if(p)e.circle(p.x,p.y,t.pointRadius+1,{fill:t.bg,stroke:color,lineWidth:2});}}}
239
- class RealtimeChart extends Chart{get _kind(){return'Realtime chart';}
240
- constructor(container,options={}){super(container,options);this.options.windowSize=options.windowSize||200;this._series=(options.series||[{name:'Series'}]).map((s)=>({name:s.name||'Series',color:s.color,values:[]}));this._follow=true;this._streaming=true;}
250
+ const n=points.length;points.forEach((p,i)=>{const l=i?(points[i-1].x+p.x)/2:0;const r=i<n-1?(points[i+1].x+p.x)/2:e.w;const label=labelOf(data[i],i);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])}`});});if(this._hover){const p=points[this._hover._id];if(p)e.circle(p.x,p.y,t.pointRadius+1,{fill:t.bg,stroke:color,lineWidth:2});}}}
251
+ class RealtimeChart extends Chart{static get meta(){return{id:'realtime',name:'RealtimeChart',aria:'Realtime chart',data:'stream',options:['min','max','window']};}
252
+ constructor(container,options={}){super(container,options);this._series=[];this._sync();this._follow=true;}
253
+ _sync(){const list=this.options.data?.series||[{name:'Series'}];this._series=list.map((s,i)=>({name:s.name||'Series '+(i+1),color:s.color,values:this._series[i]?.values||[]}));}
254
+ update(patch={}){super.update(patch);if('data'in patch)this._sync();return this;}
241
255
  _legend(){return this._series.length>1?this._seriesLegend(this._series):null;}
242
- push(values){const arr=Array.isArray(values)?values:[values];const N=this.options.windowSize;for(let i=0;i<this._series.length;i++){const v=this._series[i].values;v.push(arr[i]!=null?+arr[i]:NaN);if(v.length>N)v.splice(0,v.length-N);}
256
+ push(values){const arr=Array.isArray(values)?values:[values];const N=this.options.window||200;for(let i=0;i<this._series.length;i++){const v=this._series[i].values;v.push(arr[i]!=null?+arr[i]:NaN);if(v.length>N)v.splice(0,v.length-N);}
243
257
  if(this._auto){this._state=null;this._auto=false;}
244
- this._dirty=true;this._anim.progress=1;scheduler.schedule(this);return this;}
245
- clear(){for(const s of this._series)s.values.length=0;this._dirty=true;scheduler.schedule(this);return this;}
246
- _render(){const e=this.engine;const t=this.theme;const o=this.options;const on=this._series.map((s,i)=>i).filter((i)=>!this._isOff(i));const hasData=this._series.some((s)=>s.values.length>0);if(!hasData){const ax=drawCartesian(this,{yMin:0,yMax:100,yTickFormat:o.yTickFormat});e.text(o.waitingText||'streaming…',ax.px+ax.pw/2,ax.py+ax.ph/2,{fill:t.muted,font:`${t.fontSize}px ${t.font}`,align:'center',baseline:'middle',});return;}
247
- const[lo,hi]=range(on.map((s)=>this._series[s].values.filter((v)=>v===v)),o.yMin!=null?o.yMin:0);const[yLo,yHi]=o.yMax!=null?[o.yMin||0,o.yMax]:M.niceDomain(lo,hi,5);const N=o.windowSize;const ax=drawCartesian(this,{yMin:yLo,yMax:yHi,yTickFormat:o.yTickFormat});const xAt=(i,len)=>ax.px+((N-len+i)/(N-1))*ax.pw;const len=this._series[0].values.length;this._steps=[];for(let i=0;i<len;i++)this._steps.push(xAt(i,len));e.clip(ax.px,ax.py-t.lineWidth,ax.pw+t.lineWidth,ax.ph+t.lineWidth*2);const rows=[];let k=-1;if(this._cursor){let best=Infinity;this._steps.forEach((x,i)=>{const d=Math.abs(x-this._cursor.x);if(d<best){best=d;k=i;}});}
258
+ this._dirty=true;this._anim.progress=1;schedule(this);return this;}
259
+ clear(){for(const s of this._series)s.values.length=0;this._dirty=true;schedule(this);return this;}
260
+ _render(){const e=this.engine;const t=this.theme;const o=this.options;const on=this._series.map((s,i)=>i).filter((i)=>!this._isOff(i));const hasData=this._series.some((s)=>s.values.length>0);if(!hasData){const ax=drawCartesian(this,{yMin:o.min??0,yMax:o.max??100});e.text(o.emptyText||'Waiting for data…',ax.px+ax.pw/2,ax.py+ax.ph/2,{fill:t.muted,font:`${t.fontSize}px ${t.font}`,align:'center',baseline:'middle',});return;}
261
+ const[lo,hi]=range(on.map((s)=>this._series[s].values.filter((v)=>v===v)),0);const[yLo,yHi]=valueDomain(o,lo,hi);const N=o.window||200;const ax=drawCartesian(this,{yMin:yLo,yMax:yHi});const xAt=(i,len)=>ax.px+((N-len+i)/(N-1))*ax.pw;const len=this._series[0].values.length;this._steps=[];for(let i=0;i<len;i++)this._steps.push(xAt(i,len));e.clip(ax.px,ax.py-t.lineWidth,ax.pw+t.lineWidth,ax.ph+t.lineWidth*2);const rows=[];let k=-1;if(this._cursor){let best=Infinity;this._steps.forEach((x,i)=>{const d=Math.abs(x-this._cursor.x);if(d<best){best=d;k=i;}});}
248
262
  for(const s of on){const ser=this._series[s];const color=this._colorOf(s,ser);const pts=ser.values.map((v,i)=>({x:xAt(i,ser.values.length),y:ax.yToPx(v)}));if(pts.length===1)e.circle(pts[0].x,pts[0].y,t.pointRadius+2,{fill:color});if(pts.length<2)continue;e.poly(pts,{stroke:color,lineWidth:t.lineWidth,cap:'round',join:'round'});const last=pts[pts.length-1];e.circle(last.x,last.y,t.pointRadius+1,{fill:color});if(k>=0&&pts[k]){e.circle(pts[k].x,pts[k].y,t.pointRadius+2,{fill:t.bg,stroke:color,lineWidth:2});rows.push({color,name:ser.name,value:this._fmt(ser.values[k])});}}
249
- e.unclip();this._tipNow=null;if(k>=0&&rows.length&&this._cursor.x>=ax.px-4){const x=this._steps[k];e.poly([{x,y:ax.py},{x,y:ax.py+ax.ph}],{stroke:t.muted,lineWidth:1,dash:[3,3]});this._tipNow={title:`#${k + 1 - len}`,rows};this.tooltip.show(x,ax.py+8,this._tipNow,t);}}}
250
- class WaterfallChart extends Chart{get _kind(){return'Waterfall chart';}
251
- _render(){const o=this.options;const data=toRows(o.data);if(!data.length)return this._empty();const e=this.engine;const t=this.theme;let cum=0;const segs=data.map((d)=>{const v=+d.value||0;const seg=d.isTotal?{from:0,to:v}:{from:cum,to:cum+v};cum=seg.to;return{...seg,value:v,isTotal:!!d.isTotal,label:d.label};});const[lo,hi]=range([segs.map((s)=>s.from),segs.map((s)=>s.to)],0,0);const[yLo,yHi]=M.niceDomain(lo,hi,5);const ax=drawCartesian(this,{yMin:yLo,yMax:yHi,xLabels:segs.map((s)=>s.label),yTickFormat:o.yTickFormat});const slot=ax.pw/segs.length;const barW=Math.max(1,Math.min(60,slot*0.7));const prog=this._anim.progress;const up=o.colorUp||'#10b981';const down=o.colorDown||'#ef4444';const total=o.colorTotal||'#6366f1';for(let i=0;i<segs.length;i++){const s=segs[i];const x=ax.px+(i+0.5)*slot-barW/2;const y0=ax.yToPx(s.from);const h=Math.abs(ax.yToPx(s.to)-y0)*prog;const top=s.to>=s.from?y0-h:y0;e.rect(x,top,barW,Math.max(h,1),{fill:s.isTotal?total:s.value>=0?up:down,radius:t.barRadius});if(i<segs.length-1&&prog===1){const yEnd=ax.yToPx(s.to);e.poly([{x:x+barW,y:yEnd},{x:x+slot,y:yEnd}],{stroke:t.muted,lineWidth:1,dash:[3,3]});}
252
- this.hits.add({_id:i,x,y:top,w:barW,h:Math.max(h,4),tipX:x+barW/2,tipY:top,data:data[i],index:i,value:s.value,tooltip:`${s.label}: ${s.value > 0 && !s.isTotal ? '+' : ''}${this._fmt(s.value)}`,});}
263
+ e.unclip();this._tipNow=null;if(k>=0&&rows.length&&this._cursor.x>=ax.px-4){const x=this._steps[k];e.poly([{x,y:ax.py},{x,y:ax.py+ax.ph}],{stroke:t.muted,lineWidth:1,dash:[3,3]});this._tipNow={title:`#${k + 1 - len}`,rows};this._hover={_id:k,index:k,label:k+1-len};this.tooltip.show(x,ax.py+8,this._tipNow,t);}}}
264
+ class WaterfallChart extends Chart{static get meta(){return{id:'waterfall',name:'WaterfallChart',aria:'Waterfall chart',data:'steps',options:['labelFormat','min','max'],colors:'an object like { up, down, total }'};}
265
+ _render(){const o=this.options;const data=Array.isArray(o.data)?o.data:[];if(!data.length)return this._empty();const e=this.engine;const t=this.theme;let cum=0;const segs=data.map((d)=>{const v=+d.value||0;const seg=d.total?{from:0,to:v}:{from:cum,to:cum+v};cum=seg.to;return{...seg,value:v,total:!!d.total,label:d.label};});const[lo,hi]=range([segs.map((s)=>s.from),segs.map((s)=>s.to)],0,0);const[yLo,yHi]=valueDomain(o,lo,hi);const ax=drawCartesian(this,{yMin:yLo,yMax:yHi,xLabels:segs.map((s)=>s.label)});const slot=ax.pw/segs.length;const barW=Math.max(1,Math.min(60,slot*0.7));const prog=this._anim.progress;const c=o.colors||{};const up=c.up||'#10b981';const down=c.down||'#ef4444';const total=c.total||'#6366f1';for(let i=0;i<segs.length;i++){const s=segs[i];const x=ax.px+(i+0.5)*slot-barW/2;const y0=ax.yToPx(s.from);const h=Math.abs(ax.yToPx(s.to)-y0)*prog;const top=s.to>=s.from?y0-h:y0;e.rect(x,top,barW,Math.max(h,1),{fill:s.total?total:s.value>=0?up:down,radius:t.barRadius});if(i<segs.length-1&&prog===1){const yEnd=ax.yToPx(s.to);e.poly([{x:x+barW,y:yEnd},{x:x+slot,y:yEnd}],{stroke:t.muted,lineWidth:1,dash:[3,3]});}
266
+ this.hits.add({_id:i,x,y:top,w:barW,h:Math.max(h,4),tipX:x+barW/2,tipY:top,index:i,label:s.label,value:s.value,row:data[i],tooltip:`${this._lab(s.label)}: ${s.value > 0 && !s.total ? '+' : ''}${this._fmt(s.value)}`,});}
253
267
  if(this._hover){const r=this._hover;e.rect(r.x,r.y,r.w,r.h,{stroke:t.text,lineWidth:1.5});}}}
254
- class CompositeChart extends Chart{get _kind(){return'Composite chart';}
268
+ const TYPES=['bar','line','area','scatter'];class CompositeChart extends Chart{static get meta(){return{id:'composite',name:'CompositeChart',aria:'Composite chart',data:'series',options:['labelFormat','min','max','xScale'],enums:{xScale:['category','time']}};}
255
269
  constructor(container,options){super(container,options);this._follow=true;}
256
- _legend(){return(this.options.layers||[]).map((l,i)=>({label:(l.name||l.type)+(l.axis==='right'?' (R)':''),color:this._colorOf(i,l),}));}
257
- _render(){const o=this.options;const all=o.layers||[];const cats=o.categories||[];if(!all.length||!cats.length)return this._empty();const e=this.engine;const t=this.theme;const layers=all.map((l,i)=>({...l,i,color:this._colorOf(i,l)})).filter((l)=>!this._isOff(l.i));const yOf=(l,v)=>(l.type==='scatter'?v.y:+v);const domain=(ll)=>{let mn=0,mx=-Infinity;for(const l of ll)for(const v of l.values){const y=yOf(l,v);if(y<mn)mn=y;if(y>mx)mx=y;}
258
- return mx===-Infinity?[0,1]:M.niceDomain(mn,mx,5);};const left=layers.filter((l)=>l.axis!=='right');const rightL=layers.filter((l)=>l.axis==='right');const[lLo,lHi]=domain(left);const useTime=o.xScale==='time';const ax=drawCartesian(this,{yMin:lLo,yMax:lHi,yTickFormat:o.yLeftTickFormat||o.yTickFormat,right:rightL.length?(([a,b])=>({yMin:a,yMax:b,yTickFormat:o.yRightTickFormat}))(domain(rightL)):null,xLabels:useTime?null:cats,xTime:useTime?{min:+cats[0],max:+cats[cats.length-1]}:null,xTickFormat:o.xTickFormat,});const yPx=(l)=>(l.axis==='right'&&ax.yRToPx?ax.yRToPx:ax.yToPx);const xAt=useTime?(i)=>ax.xToPx(+cats[i]):(i)=>ax.px+((i+0.5)/cats.length)*ax.pw;this._steps=cats.map((_,i)=>xAt(i));const prog=this._anim.progress;const lineW=t.lineWidth;const bars=layers.filter((l)=>l.type==='bar');const slot=ax.pw/cats.length;const inner=slot*0.7;const barW=bars.length?Math.max(1,inner/bars.length):0;bars.forEach((l,bi)=>{const toPx=yPx(l);const y0=toPx(0);const items=[];for(let i=0;i<cats.length;i++){const v=+l.values[i]||0;const h=Math.abs(toPx(v)-y0)*prog;items.push({x:xAt(i)-inner/2+bi*barW,y:v>=0?y0-h:y0,w:Math.max(1,barW-1),h,radius:t.barRadius});}
270
+ _legend(){return(this.options.data?.series||[]).map((l,i)=>({label:(l.name||l.type)+(l.axis==='right'?' (R)':''),color:this._colorOf(i,l),}));}
271
+ _render(){const o=this.options;const{categories:cats=[],series:all=[]}=o.data||{};if(!all.length||!cats.length)return this._empty();const e=this.engine;const t=this.theme;all.forEach((l,i)=>{if(!TYPES.includes(l.type))warn(`CompositeChart: series[${i}] type must be ${TYPES.map((x) => `'${x}'`).join(' | ')} — got ${JSON.stringify(l.type)}.`);});const layers=all.map((l,i)=>({...l,i,color:this._colorOf(i,l)})).filter((l)=>!this._isOff(l.i));const domain=(ll)=>{let mn=0,mx=-Infinity;for(const l of ll)for(const v of l.values){if(v==null)continue;const y=+v;if(y<mn)mn=y;if(y>mx)mx=y;}
272
+ return mx===-Infinity?[0,1]:M.niceDomain(mn,mx,5);};const left=layers.filter((l)=>l.axis!=='right');const rightL=layers.filter((l)=>l.axis==='right');let[lLo,lHi]=domain(left);if(o.min!=null||o.max!=null)[lLo,lHi]=valueDomain(o,o.min??lLo,o.max??lHi);const fmtOf=(ll)=>ll.find((l)=>l.format)?.format;const useTime=o.xScale==='time';const ax=drawCartesian(this,{yMin:lLo,yMax:lHi,yFormat:fmtOf(left),right:rightL.length?(([a,b])=>({yMin:a,yMax:b,yFormat:fmtOf(rightL)}))(domain(rightL)):null,xLabels:useTime?null:cats,xTime:useTime?{min:+cats[0],max:+cats[cats.length-1]}:null,});const yPx=(l)=>(l.axis==='right'&&ax.yRToPx?ax.yRToPx:ax.yToPx);const xAt=useTime?(i)=>ax.xToPx(+cats[i]):(i)=>ax.px+((i+0.5)/cats.length)*ax.pw;this._steps=cats.map((_,i)=>xAt(i));const prog=this._anim.progress;const lineW=t.lineWidth;const bars=layers.filter((l)=>l.type==='bar');const slot=ax.pw/cats.length;const inner=slot*0.7;const barW=bars.length?Math.max(1,inner/bars.length):0;bars.forEach((l,bi)=>{const toPx=yPx(l);const y0=toPx(0);const items=[];for(let i=0;i<cats.length;i++){const v=+l.values[i]||0;const h=Math.abs(toPx(v)-y0)*prog;items.push({x:xAt(i)-inner/2+bi*barW,y:v>=0?y0-h:y0,w:Math.max(1,barW-1),h,radius:t.barRadius});}
259
273
  e.rects(items,l.color);});e.clip(ax.px-lineW,ax.py-lineW,(ax.pw+lineW*2)*prog,ax.ph+lineW*2);for(const l of layers.filter((x)=>x.type==='area')){const toPx=yPx(l);const pts=l.values.map((v,i)=>({x:xAt(i),y:toPx(+v)}));const cps=M.bezierControlPoints(pts);const grad=e.vGradient(`area-${l.i}-${l.color}`,ax.px,ax.py,ax.py+ax.ph,[[0,M.colorAlpha(l.color,0.4)],[1,M.colorAlpha(l.color,0)]]);e.smoothArea(pts,cps,Math.min(ax.py+ax.ph,toPx(0)),grad);e.smooth(pts,cps,{stroke:l.color,lineWidth:lineW});}
260
274
  for(const l of layers.filter((x)=>x.type==='line')){const toPx=yPx(l);const pts=l.values.map((v,i)=>({x:xAt(i),y:toPx(+v)}));e.smooth(pts,M.bezierControlPoints(pts),{stroke:l.color,lineWidth:lineW});if(pts.length<=50)e.circles(pts,l.color,t.pointRadius);}
261
- e.unclip();for(const l of layers.filter((x)=>x.type==='scatter')){const toPx=yPx(l);const items=l.values.map((p)=>({x:xAt(typeof p.x==='number'?p.x:cats.indexOf(p.x)),y:toPx(p.y)}));e.withAlpha(0.8,()=>e.circles(items,l.color,l.radius||t.pointRadius+1));}
262
- this._tipNow=null;const cx=this._cursor?.x;if(cx!=null&&cx>=ax.px&&cx<=ax.px+ax.pw){let idx=0,best=Infinity;this._steps.forEach((x,i)=>{const d=Math.abs(x-cx);if(d<best){best=d;idx=i;}});const x=this._steps[idx];e.withAlpha(0.06,()=>e.rect(x-slot/2,ax.py,slot,ax.ph,{fill:t.text}));const rows=[];for(const l of layers){let v;if(l.type==='scatter'){const p=l.values.find((p)=>(typeof p.x==='number'?p.x:cats.indexOf(p.x))===idx);if(!p)continue;v=p.y;}else v=+l.values[idx];rows.push({color:l.color,name:l.name||l.type,value:this._fmt(v)});}
263
- this._tipNow={title:useTime?new Date(+cats[idx]).toLocaleString():String(cats[idx]),rows};this._hover={_id:idx,index:idx};this.tooltip.show(x,ax.py+8,this._tipNow,t);}else if(this._cursor){this.tooltip.hide();}}}
264
- const math={TAU,clamp,lerp,extent,niceTicks,niceDomain,timeNiceTicks,timeFormat,minMaxDecimate,minMaxDecimateIdx,shouldDecimate,bezierControlPoints,springStep,colorParse,colorAlpha,colorMix,PALETTE,formatNumber};export{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};
275
+ e.unclip();for(const l of layers.filter((x)=>x.type==='scatter')){const toPx=yPx(l);const items=[];l.values.forEach((v,i)=>{if(v!=null)items.push({x:xAt(i),y:toPx(+v)});});e.withAlpha(0.8,()=>e.circles(items,l.color,t.pointRadius+1));}
276
+ this._tipNow=null;const cx=this._cursor?.x;if(cx!=null&&cx>=ax.px&&cx<=ax.px+ax.pw){let idx=0,best=Infinity;this._steps.forEach((x,i)=>{const d=Math.abs(x-cx);if(d<best){best=d;idx=i;}});const x=this._steps[idx];e.withAlpha(0.06,()=>e.rect(x-slot/2,ax.py,slot,ax.ph,{fill:t.text}));const rows=[];for(const l of layers){const v=l.values[idx];if(v==null)continue;rows.push({color:l.color,name:l.name||l.type,value:M.formatNumber(+v,l.format??o.format)});}
277
+ this._tipNow={title:this._lab(cats[idx],useTime),rows};this._hover={_id:idx,index:idx,label:cats[idx]};this.tooltip.show(x,ax.py+8,this._tipNow,t);}else if(this._cursor){this.tooltip.hide();}}}
278
+ export{Chart,PALETTE,BarChart,HBarChart,LineChart,AreaChart,StackedBarChart,GroupedBarChart,MultiLineChart,ScatterChart,BubbleChart,PieChart,RadarChart,PolarChart,GaugeChart,HeatmapChart,BoxplotChart,FunnelChart,TreemapChart,SparklineChart,RealtimeChart,WaterfallChart,CompositeChart};