@aeriz/chart 1.1.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -34
- package/dist/aeriz.cjs +609 -354
- package/dist/aeriz.esm.js +578 -331
- package/dist/aeriz.esm.min.js +119 -105
- package/dist/aeriz.js +609 -354
- package/dist/aeriz.min.cjs +123 -107
- package/dist/aeriz.min.js +123 -107
- package/dist/esm/base.js +33 -22
- package/dist/esm/cartesian.js +7 -5
- package/dist/esm/charts/area.js +6 -6
- package/dist/esm/charts/bar.js +3 -3
- package/dist/esm/charts/boxplot.js +3 -3
- package/dist/esm/charts/bubble.js +3 -3
- package/dist/esm/charts/composite.js +8 -8
- package/dist/esm/charts/funnel.js +3 -3
- package/dist/esm/charts/gauge.js +5 -5
- package/dist/esm/charts/grouped-bar.js +4 -4
- package/dist/esm/charts/hbar.js +5 -5
- package/dist/esm/charts/heatmap.js +6 -6
- package/dist/esm/charts/line.js +6 -6
- package/dist/esm/charts/multi-line.js +5 -5
- package/dist/esm/charts/pie.js +6 -6
- package/dist/esm/charts/polar.js +3 -3
- package/dist/esm/charts/radar.js +6 -6
- package/dist/esm/charts/realtime.js +11 -9
- package/dist/esm/charts/scatter.js +4 -4
- package/dist/esm/charts/sparkline.js +4 -4
- package/dist/esm/charts/stacked-bar.js +5 -5
- package/dist/esm/charts/treemap.js +4 -4
- package/dist/esm/charts/waterfall.js +4 -4
- package/dist/esm/engine.js +1 -1
- package/dist/esm/index.js +2 -2
- package/dist/esm/jquery.js +5 -4
- package/dist/esm/math.js +1 -1
- package/dist/esm/react.js +4 -4
- package/dist/esm/shallow.js +4 -2
- package/dist/esm/svelte.js +4 -4
- package/dist/esm/vue.js +4 -2
- package/dist/index.d.ts +228 -552
- package/dist/jquery.d.ts +19 -8
- package/dist/react.d.ts +55 -18
- package/dist/svelte.d.ts +53 -14
- package/dist/types/sparkline.d.ts +1 -1
- package/dist/vue.d.ts +60 -10
- package/package.json +2 -2
package/dist/aeriz.esm.min.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/*! @aeriz/chart
|
|
2
|
-
/*! @aeriz/chart
|
|
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;
|
|
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)
|
|
69
|
-
const
|
|
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(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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(){
|
|
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);
|
|
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.
|
|
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.
|
|
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=
|
|
143
|
-
if(xToPx){let lastEnd=-Infinity;for(const v of xTicks){const txt=xFmt(v
|
|
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
|
-
|
|
148
|
-
|
|
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
|
|
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=
|
|
152
|
-
e.ctx.stroke();for(const v of xTicks){e.text(
|
|
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(
|
|
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
|
|
156
|
-
_render(){const
|
|
157
|
-
const values=c.values;const[lo,hi]=(c.range||=range([values],
|
|
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
|
|
161
|
-
e.unclip();const
|
|
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
|
|
164
|
-
_legend(){return this._seriesLegend(this.options.series||[]);}
|
|
165
|
-
_render(){const o=this.options;const
|
|
166
|
-
const[yLo,yHi]=
|
|
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:
|
|
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
|
|
172
|
-
_legend(){return this._seriesLegend(this.options.series||[]);}
|
|
173
|
-
_render(){const o=this.options;const
|
|
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]=
|
|
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
|
|
178
|
-
_legend(){return this._seriesLegend(this.options.series||[]);}
|
|
179
|
-
_render(){const o=this.options;const
|
|
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
|
|
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
|
|
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=
|
|
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
|
|
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]=
|
|
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
|
|
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.
|
|
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
|
|
197
|
-
_legend(){const o=this.options;const labelOf=
|
|
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=
|
|
199
|
-
if(innerR>0&&o.
|
|
200
|
-
class RadarChart extends Chart{get
|
|
201
|
-
_legend(){return this._seriesLegend(this.options.series||[]);}
|
|
202
|
-
_render(){const o=this.options;const
|
|
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(
|
|
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:
|
|
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
|
|
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=
|
|
211
|
-
class GaugeChart extends Chart{get
|
|
212
|
-
|
|
213
|
-
_render(){const e=this.engine;const t=this.theme;const o=this.options;const min=o.min
|
|
214
|
-
this.canvas.setAttribute('aria-label',`${o.
|
|
215
|
-
class HeatmapChart extends Chart{get
|
|
216
|
-
_render(){const o=this.options;const
|
|
217
|
-
const cellW=pw/xLabels.length;const cellH=ph/yLabels.length;const
|
|
218
|
-
let lastEnd=-Infinity;for(let i=0;i<xLabels.length;i++){const txt=
|
|
219
|
-
const step=Math.ceil((t.fontSize+2)/cellH);for(let i=0;i<yLabels.length;i+=step){e.text(
|
|
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
|
|
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]=
|
|
224
|
-
class FunnelChart extends Chart{get
|
|
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=
|
|
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
|
|
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=
|
|
234
|
-
this.hits.add({_id:it._id,x,y,w,h,tipX:x+w/2,tipY:y,
|
|
235
|
-
class
|
|
236
|
-
_render(){const o=this.options;const data=o.data
|
|
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
|
|
240
|
-
constructor(container,options={}){super(container,options);this.
|
|
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.
|
|
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;
|
|
245
|
-
clear(){for(const s of this._series)s.values.length=0;this._dirty=true;
|
|
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:
|
|
247
|
-
const[lo,hi]=range(on.map((s)=>this._series[s].values.filter((v)=>v===v)),
|
|
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
|
|
251
|
-
_render(){const o=this.options;const data=
|
|
252
|
-
this.hits.add({_id:i,x,y:top,w:barW,h:Math.max(h,4),tipX:x+barW/2,tipY:top,
|
|
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
|
|
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.
|
|
257
|
-
_render(){const o=this.options;const
|
|
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');
|
|
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.
|
|
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){
|
|
263
|
-
this._tipNow={title:
|
|
264
|
-
|
|
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};
|