@aeriz/chart 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +60 -34
  2. package/dist/aeriz.cjs +609 -354
  3. package/dist/aeriz.esm.js +578 -331
  4. package/dist/aeriz.esm.min.js +119 -105
  5. package/dist/aeriz.js +609 -354
  6. package/dist/aeriz.min.cjs +123 -107
  7. package/dist/aeriz.min.js +123 -107
  8. package/dist/esm/base.js +33 -22
  9. package/dist/esm/cartesian.js +7 -5
  10. package/dist/esm/charts/area.js +6 -6
  11. package/dist/esm/charts/bar.js +3 -3
  12. package/dist/esm/charts/boxplot.js +3 -3
  13. package/dist/esm/charts/bubble.js +3 -3
  14. package/dist/esm/charts/composite.js +8 -8
  15. package/dist/esm/charts/funnel.js +3 -3
  16. package/dist/esm/charts/gauge.js +5 -5
  17. package/dist/esm/charts/grouped-bar.js +4 -4
  18. package/dist/esm/charts/hbar.js +5 -5
  19. package/dist/esm/charts/heatmap.js +6 -6
  20. package/dist/esm/charts/line.js +6 -6
  21. package/dist/esm/charts/multi-line.js +5 -5
  22. package/dist/esm/charts/pie.js +6 -6
  23. package/dist/esm/charts/polar.js +3 -3
  24. package/dist/esm/charts/radar.js +6 -6
  25. package/dist/esm/charts/realtime.js +11 -9
  26. package/dist/esm/charts/scatter.js +4 -4
  27. package/dist/esm/charts/sparkline.js +4 -4
  28. package/dist/esm/charts/stacked-bar.js +5 -5
  29. package/dist/esm/charts/treemap.js +4 -4
  30. package/dist/esm/charts/waterfall.js +4 -4
  31. package/dist/esm/engine.js +1 -1
  32. package/dist/esm/index.js +2 -2
  33. package/dist/esm/jquery.js +5 -4
  34. package/dist/esm/math.js +1 -1
  35. package/dist/esm/react.js +4 -4
  36. package/dist/esm/shallow.js +4 -2
  37. package/dist/esm/svelte.js +4 -4
  38. package/dist/esm/vue.js +4 -2
  39. package/dist/index.d.ts +228 -552
  40. package/dist/jquery.d.ts +19 -8
  41. package/dist/react.d.ts +55 -18
  42. package/dist/svelte.d.ts +53 -14
  43. package/dist/types/sparkline.d.ts +1 -1
  44. package/dist/vue.d.ts +60 -10
  45. package/package.json +2 -2
package/dist/esm/base.js CHANGED
@@ -1,11 +1,8 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
- import{Engine}from'./engine.js';import*as M from'./math.js';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;}
3
- frames++;let active=false;for(const c of charts){try{if(c._tick(now))active=true;}catch(e){console.error(e);}}
1
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
+ import{Engine}from'./engine.js';import*as M from'./math.js';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);}}
4
3
  rafId=charts.size>0&&active?requestAnimationFrame(tick):null;}
5
- function schedule(chart){if(chart._destroyed)return;charts.add(chart);if(rafId===null){fpsT=performance.now();rafId=requestAnimationFrame(tick);}}
6
- export const scheduler={schedule,fps:()=>fps,count:()=>charts.size};export function getFPS(){return fps;}
7
- export function getActiveCount(){return charts.size;}
8
- 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;}
4
+ export function schedule(chart){if(chart._destroyed)return;charts.add(chart);if(rafId===null)rafId=requestAnimationFrame(tick);}
5
+ 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;}
9
6
  return'#ffffff';}
10
7
  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',};}
11
8
  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);}
@@ -27,25 +24,38 @@ clear(){this.regions.length=0;}
27
24
  add(r){this.regions.push(r);}
28
25
  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;}
29
26
  return null;}}
30
- export function toRows(data){if(!data)return[];if(!Array.isArray(data)){return typeof data==='object'?Object.keys(data).map((k)=>({label:k,value:+data[k]})):[];}
31
- 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;}
32
- export 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);}
33
- 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);}
34
- 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;}
27
+ const isList=(d)=>Array.isArray(d)||ArrayBuffer.isView(d);const isObj=(d)=>d!=null&&typeof d==='object'&&!isList(d);export 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;}
28
+ return isObj(data)?Object.keys(data).map((k)=>({label:k,value:+data[k]})):[];}
29
+ const said=new Set();export function warn(msg){if(said.has(msg))return;said.add(msg);console.warn('[aeriz] '+msg);}
30
+ export 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;}}
31
+ return d[b.length];}
32
+ 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;}
33
+ 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;}}
34
+ warn(`${name}: unknown option "${k}"${best ? ` — did you mean "${best}"?` : `. Options: ${allowed.join(', ')}`}`);continue;}
35
+ 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\']'}.`);}
36
+ if('data'in o)checkData(meta,o.data,o);}
37
+ 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;}
38
+ 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.`);}}}
39
+ const STATES=['loading','empty','error'];export class Chart{static get meta(){return null;}
40
+ 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);}
41
+ 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;}
35
42
  this._label();this._restart();return this;}
36
- setData(data){return this.update({data});}
37
- setLoading(msg){return this._setState('loading',msg||'');}
38
- setEmpty(msg){return this._setState('empty',msg||'No data');}
39
- setError(msg){return this._setState('error',msg||'Error');}
40
- clearState(){return this._setState(null);}
43
+ setState(state,text){if(state!=null&&!STATES.includes(state)){throw new Error(`aeriz: setState() takes 'loading', 'empty', 'error' or null — got ${JSON.stringify(state)}`);}
44
+ 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;}
45
+ render(){if(this._destroyed)return this;this._theme();this._anim.progress=1;this._resize();this._dirty=false;this._draw();return this;}
41
46
  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;}
42
- _setState(kind,msg){this._state=kind;this._stateMsg=msg;this._auto=false;this._dirty=true;schedule(this);return this;}
47
+ _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;}
43
48
  _empty(msg=this.options.emptyText||'No data'){this._state='empty';this._stateMsg=msg;this._auto=true;this.overlay.show('empty',msg,this.theme);}
44
49
  _restart(){const instant=this.options.animate===false||reducedMotion?.matches;this._anim.start=0;this._anim.progress=instant?1:0;this._dirty=true;schedule(this);}
45
- _label(){const o=this.options;this.canvas.setAttribute('aria-label',o.ariaLabel||o.title||this._kind||'Chart');}
50
+ _label(){this.canvas.setAttribute('aria-label',this.options.title||this.constructor.meta?.aria||'Chart');}
46
51
  _fmt(v){return M.formatNumber(v,this.options.format);}
47
52
  _tickFmt(v){return M.formatNumber(v,this.options.format,true);}
53
+ _lab(l,time){const f=this._fn('labelFormat');return f?String(f(l)):time?new Date(+l).toLocaleString():String(l);}
54
+ _xFmt(v){const f=this._fn('labelFormat');return f?String(f(v)):this._fmt(v);}
48
55
  _isOff(i){return this._off.has(i);}
56
+ _fn(k){const f=this.options[k];return typeof f==='function'?f:null;}
57
+ _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;}
58
+ _click(h){const hit=this._hit(h);if(hit)this.options.onClick?.(hit,this);}
49
59
  _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;}
50
60
  _observe(){if(typeof ResizeObserver!=='undefined'){this._ro=new ResizeObserver(()=>{if(this._resize())schedule(this);});this._ro.observe(this.plot);}
51
61
  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);}}
@@ -58,8 +68,8 @@ if(changed||this._follow){this._dirty=true;schedule(this);}
58
68
  if(typeof this._onMoveX==='function')this._onMoveX(p);return changed;}
59
69
  _onMove(e){const p=this._ptr(e);this._cursor=p;this._setHover(this.hits.find(p.x,p.y),p);}
60
70
  _onLeave(){this._cursor=null;this._hover=null;this._key=-1;this.tooltip.hide();this._dirty=true;schedule(this);}
61
- _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);}
62
- _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);}}
71
+ _onClick(e){const p=this._ptr(e);this._click(this.hits.find(p.x,p.y)||(this._follow?this._hover:null));}
72
+ _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);}}
63
73
  _tick(now){if(this._destroyed)return false;if(!this._visible){if(this._anim.progress<1)this._anim.start=0;return false;}
64
74
  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;}
65
75
  if(this._dirty){this._dirty=false;this._draw();}
@@ -70,4 +80,5 @@ this._state=null;this._auto=false;this.overlay.hide();if(typeof this._render==='
70
80
  _legend(){return null;}
71
81
  _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));});}
72
82
  _seriesLegend(series){return series.map((s,i)=>({label:s.name||'Series '+(i+1),color:this._colorOf(i,s)}));}
73
- _colorOf(idx,item){if(item&&item.color)return item.color;return this.palette[idx%this.palette.length];}}
83
+ _colorOf(idx,item){if(item&&item.color)return item.color;return this.palette[idx%this.palette.length];}}
84
+ 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]}.`);};}
@@ -1,11 +1,13 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */
1
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
2
  import*as M from'./math.js';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;}
3
3
  export 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);}
4
4
  export 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);}
5
- 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));}
5
+ 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));}
6
6
  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);}
7
- 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);}
8
- 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'});}}
7
+ 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);}
8
+ 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'});}}
9
9
  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};}
10
10
  export 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;}}
11
- if(lo===Infinity)return[0,1];return[lo,hi];}
11
+ if(lo===Infinity)return[0,1];return[lo,hi];}
12
+ export function valueDomain(o,lo,hi){const[a,b]=M.niceDomain(o.min??lo,o.max??hi,5);return[o.min??a,o.max??b];}
13
+ export const AXIS=['labelFormat','min','max'];
@@ -1,9 +1,9 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
- import{Chart}from'../base.js';import{drawCartesian,range}from'../cartesian.js';import*as M from'../math.js';export class AreaChart extends Chart{get _kind(){return'Area chart';}
3
- _legend(){return this._seriesLegend(this.options.series||[]);}
4
- _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);}
5
- 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;}
1
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
+ import{Chart}from'../base.js';import{drawCartesian,range,valueDomain}from'../cartesian.js';import*as M from'../math.js';export class AreaChart extends Chart{static get meta(){return{id:'area',name:'AreaChart',aria:'Area chart',data:'series',options:['labelFormat','min','max','stacked']};}
3
+ _legend(){return this._seriesLegend(this.options.data?.series||[]);}
4
+ _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);}
5
+ 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;}
6
6
  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);}
7
7
  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});}
8
- 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])})),},});}
8
+ 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])})),},});}
9
9
  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]});}}}
@@ -1,4 +1,4 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
- import{Chart,toRows}from'../base.js';import{drawCartesian,range}from'../cartesian.js';import*as M from'../math.js';export class BarChart extends Chart{get _kind(){return'Bar chart';}
3
- _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',});}}
1
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
+ import{Chart,toRows}from'../base.js';import{drawCartesian,range,valueDomain}from'../cartesian.js';export class BarChart extends Chart{static get meta(){return{id:'bar',name:'BarChart',aria:'Bar chart',data:'rows',options:['label','value','labelFormat','min','max','dataLabels']};}
3
+ _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',});}}
4
4
  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});}}}
@@ -1,4 +1,4 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
- import{Chart}from'../base.js';import{drawCartesian}from'../cartesian.js';import*as M from'../math.js';export class BoxplotChart extends Chart{get _kind(){return'Boxplot chart';}
1
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
+ import{Chart}from'../base.js';import{drawCartesian,valueDomain}from'../cartesian.js';import*as M from'../math.js';export class BoxplotChart extends Chart{static get meta(){return{id:'boxplot',name:'BoxplotChart',aria:'Box plot',data:'boxes',options:['labelFormat','min','max']};}
3
3
  _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;}}
4
- 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)})),},});}}}
4
+ 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)})),},});}}}
@@ -1,3 +1,3 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
- import{Chart}from'../base.js';import{drawCartesian}from'../cartesian.js';import{xyExtent}from'./scatter.js';import*as M from'../math.js';export class BubbleChart extends Chart{get _kind(){return'Bubble chart';}
3
- _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)}`,});}}}
1
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
+ import{Chart}from'../base.js';import{drawCartesian,valueDomain}from'../cartesian.js';import{xyExtent}from'./scatter.js';import*as M from'../math.js';export class BubbleChart extends Chart{static get meta(){return{id:'bubble',name:'BubbleChart',aria:'Bubble chart',data:'points',options:['labelFormat','min','max','radius']};}
3
+ _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)}`,});}}}
@@ -1,11 +1,11 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
- import{Chart}from'../base.js';import{drawCartesian}from'../cartesian.js';import*as M from'../math.js';export class CompositeChart extends Chart{get _kind(){return'Composite chart';}
1
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
+ import{Chart,warn}from'../base.js';import{drawCartesian,valueDomain}from'../cartesian.js';import*as M from'../math.js';const TYPES=['bar','line','area','scatter'];export 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']}};}
3
3
  constructor(container,options){super(container,options);this._follow=true;}
4
- _legend(){return(this.options.layers||[]).map((l,i)=>({label:(l.name||l.type)+(l.axis==='right'?' (R)':''),color:this._colorOf(i,l),}));}
5
- _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;}
6
- 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});}
4
+ _legend(){return(this.options.data?.series||[]).map((l,i)=>({label:(l.name||l.type)+(l.axis==='right'?' (R)':''),color:this._colorOf(i,l),}));}
5
+ _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;}
6
+ 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});}
7
7
  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});}
8
8
  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);}
9
- 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));}
10
- 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)});}
11
- 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();}}}
9
+ 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));}
10
+ 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)});}
11
+ 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();}}}
@@ -1,3 +1,3 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
- import{Chart,toRows}from'../base.js';export class FunnelChart extends Chart{get _kind(){return'Funnel chart';}
3
- _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)}%)`,});}}}
1
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
+ import{Chart,toRows}from'../base.js';export class FunnelChart extends Chart{static get meta(){return{id:'funnel',name:'FunnelChart',aria:'Funnel chart',data:'rows',options:['label','value','labelColor']};}
3
+ _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)}%)`,});}}}
@@ -1,5 +1,5 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
- import{Chart}from'../base.js';export class GaugeChart extends Chart{get _kind(){return'Gauge chart';}
3
- setValue(v){this._from=this._shown??0;return this.update({value:v});}
4
- _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'});}
5
- this.canvas.setAttribute('aria-label',`${o.ariaLabel || o.label || o.title || 'Gauge'}: ${txt}`);}}
1
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
+ import{Chart}from'../base.js';export 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'},};}
3
+ update(patch={}){if('data'in patch)this._from=this._shown;return super.update(patch);}
4
+ _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'});}
5
+ this.canvas.setAttribute('aria-label',`${o.title || 'Gauge'}: ${txt}`);}}
@@ -1,5 +1,5 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
- import{Chart}from'../base.js';import{drawCartesian,range}from'../cartesian.js';import*as M from'../math.js';export class GroupedBarChart extends Chart{get _kind(){return'Grouped bar chart';}
3
- _legend(){return this._seriesLegend(this.options.series||[]);}
4
- _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)}]},});});}
1
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
+ import{Chart}from'../base.js';import{drawCartesian,range,valueDomain}from'../cartesian.js';export class GroupedBarChart extends Chart{static get meta(){return{id:'grouped-bar',name:'GroupedBarChart',aria:'Grouped bar chart',data:'series',options:['labelFormat','min','max']};}
3
+ _legend(){return this._seriesLegend(this.options.data?.series||[]);}
4
+ _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)}]},});});}
5
5
  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});}}}
@@ -1,6 +1,6 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
- import{Chart,toRows}from'../base.js';import{range}from'../cartesian.js';import*as M from'../math.js';export class HBarChart extends Chart{get _kind(){return'Horizontal bar chart';}
3
- _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);}
4
- 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'});}
5
- 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'});}
1
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
+ import{Chart,toRows}from'../base.js';import{range,valueDomain}from'../cartesian.js';import*as M from'../math.js';export class HBarChart extends Chart{static get meta(){return{id:'hbar',name:'HBarChart',aria:'Horizontal bar chart',data:'rows',options:['label','value','labelFormat','min','max']};}
3
+ _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);}
4
+ 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'});}
5
+ 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'});}
6
6
  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});}}}
@@ -1,7 +1,7 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
- import{Chart}from'../base.js';import*as M from'../math.js';export class HeatmapChart extends Chart{get _kind(){return'Heatmap chart';}
3
- _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;}
4
- 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)}`,});}
5
- 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'});}
6
- 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'});}
1
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
+ import{Chart}from'../base.js';import*as M from'../math.js';export 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\' }'};}
3
+ _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;}}
4
+ 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)}`,});}}
5
+ 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'});}
6
+ 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'});}
7
7
  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});}}}
@@ -1,9 +1,9 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
- import{Chart,toRows}from'../base.js';import{drawCartesian,range}from'../cartesian.js';import*as M from'../math.js';const LABEL=(d)=>d.label;const VALUE=(d)=>d.value;export class LineChart extends Chart{get _kind(){return'Line chart';}
3
- _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};}
4
- 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]);}
1
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
+ import{Chart,toRows}from'../base.js';import{drawCartesian,range,valueDomain}from'../cartesian.js';import*as M from'../math.js';const LABEL=(d)=>d.label;const VALUE=(d)=>d.value;export 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']},};}
3
+ _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};}
4
+ 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]);}
5
5
  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};}
6
6
  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});}
7
- 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});}
8
- 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],});}}
7
+ 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});}
8
+ 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],});}}
9
9
  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});}}}
@@ -1,8 +1,8 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
- import{Chart}from'../base.js';import{drawCartesian,range}from'../cartesian.js';import*as M from'../math.js';export class MultiLineChart extends Chart{get _kind(){return'Multi line chart';}
1
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
+ import{Chart}from'../base.js';import{drawCartesian,range,valueDomain}from'../cartesian.js';import*as M from'../math.js';export 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']}};}
3
3
  constructor(container,options){super(container,options);this._follow=true;}
4
- _legend(){return this._seriesLegend(this.options.series||[]);}
5
- _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);}
4
+ _legend(){return this._seriesLegend(this.options.data?.series||[]);}
5
+ _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);}
6
6
  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;}}
7
7
  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])});}
8
- 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();}}}
8
+ 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();}}}
@@ -1,6 +1,6 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
- import{Chart,toRows}from'../base.js';import{TAU}from'../math.js';export 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;};}
3
- export class PieChart extends Chart{get _kind(){return'Pie chart';}
4
- _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)}));}
5
- _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;}
6
- 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',});}}}
1
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
+ import{Chart,toRows,warn}from'../base.js';import{TAU}from'../math.js';export 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;};}
3
+ export class PieChart extends Chart{static get meta(){return{id:'pie',name:'PieChart',aria:'Pie chart',data:'rows',options:['label','value','innerRadius','center']};}
4
+ _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)}));}
5
+ _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;}
6
+ 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',});}}}
@@ -1,3 +1,3 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
- import{Chart,toRows}from'../base.js';import{inSector}from'./pie.js';import{TAU}from'../math.js';export class PolarChart extends Chart{get _kind(){return'Polar chart';}
3
- _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)}`,});}}}
1
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
+ import{Chart,toRows}from'../base.js';import{inSector}from'./pie.js';import{TAU}from'../math.js';export 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']}};}
3
+ _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)}`,});}}}
@@ -1,10 +1,10 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
- import{Chart}from'../base.js';import{range}from'../cartesian.js';import*as M from'../math.js';import{TAU}from'../math.js';export class RadarChart extends Chart{get _kind(){return'Radar chart';}
3
- _legend(){return this._seriesLegend(this.options.series||[]);}
4
- _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);}
1
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
+ import{Chart}from'../base.js';import{range}from'../cartesian.js';import*as M from'../math.js';import{TAU}from'../math.js';export class RadarChart extends Chart{static get meta(){return{id:'radar',name:'RadarChart',aria:'Radar chart',data:'series',options:['labelFormat','max']};}
3
+ _legend(){return this._seriesLegend(this.options.data?.series||[]);}
4
+ _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);}
5
5
  ctx.stroke();}
6
6
  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);}
7
- 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',});}
8
- 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)}]},});}
7
+ 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',});}
8
+ 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)}]},});}
9
9
  e.poly(pts,{fill:M.colorAlpha(color,0.2),stroke:color,lineWidth:2,close:true,join:'round'});e.circles(pts,color,t.pointRadius);}
10
10
  if(this._hover){const h=this._hover;e.circle(h.x+7,h.y+7,t.pointRadius+3,{stroke:t.text,lineWidth:1.5});}}}
@@ -1,12 +1,14 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
- import{Chart,scheduler}from'../base.js';import{drawCartesian,range}from'../cartesian.js';import*as M from'../math.js';export class RealtimeChart extends Chart{get _kind(){return'Realtime chart';}
3
- 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;}
1
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
+ import{Chart,schedule}from'../base.js';import{drawCartesian,range,valueDomain}from'../cartesian.js';export class RealtimeChart extends Chart{static get meta(){return{id:'realtime',name:'RealtimeChart',aria:'Realtime chart',data:'stream',options:['min','max','window']};}
3
+ constructor(container,options={}){super(container,options);this._series=[];this._sync();this._follow=true;}
4
+ _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||[]}));}
5
+ update(patch={}){super.update(patch);if('data'in patch)this._sync();return this;}
4
6
  _legend(){return this._series.length>1?this._seriesLegend(this._series):null;}
5
- 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);}
7
+ 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);}
6
8
  if(this._auto){this._state=null;this._auto=false;}
7
- this._dirty=true;this._anim.progress=1;scheduler.schedule(this);return this;}
8
- clear(){for(const s of this._series)s.values.length=0;this._dirty=true;scheduler.schedule(this);return this;}
9
- _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;}
10
- 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;}});}
9
+ this._dirty=true;this._anim.progress=1;schedule(this);return this;}
10
+ clear(){for(const s of this._series)s.values.length=0;this._dirty=true;schedule(this);return this;}
11
+ _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;}
12
+ 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;}});}
11
13
  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])});}}
12
- 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);}}}
14
+ 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);}}}
@@ -1,6 +1,6 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
- import{Chart}from'../base.js';import{drawCartesian}from'../cartesian.js';import*as M from'../math.js';export 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;}
1
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
+ import{Chart}from'../base.js';import{drawCartesian,valueDomain}from'../cartesian.js';import*as M from'../math.js';export 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;}
3
3
  return[xMin,xMax,yMin,yMax];}
4
- export class ScatterChart extends Chart{get _kind(){return'Scatter chart';}
5
- _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}` : ''}`,});}}
4
+ export class ScatterChart extends Chart{static get meta(){return{id:'scatter',name:'ScatterChart',aria:'Scatter chart',data:'points',options:['labelFormat','min','max']};}
5
+ _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)})`,});}}
6
6
  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});}}}
@@ -1,5 +1,5 @@
1
- /*! @aeriz/chart v1.1.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
- import{Chart}from'../base.js';import{range}from'../cartesian.js';import*as M from'../math.js';export class Sparkline extends Chart{get _kind(){return'Sparkline';}
3
- _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);}
1
+ /*! @aeriz/chart v2.0.0 | Apache-2.0 | https://aeriz.dev/chart/ */
2
+ import{Chart,toRows}from'../base.js';import{range}from'../cartesian.js';import*as M from'../math.js';export class SparklineChart extends Chart{static get meta(){return{id:'sparkline',name:'SparklineChart',aria:'Sparkline',data:'rows',options:['label','value','min','max','fill','lastDot']};}
3
+ _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);}
4
4
  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});}
5
- 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});}}}
5
+ 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});}}}