/* Research Labs charts: dependency-free SVG line charts. Rules applied: single axis, 2px lines, recessive grid, legend for >=2 series, crosshair + tooltip, text in ink tokens, fixed series colours. */ const SERIES=['#3987e5','#d95926','#199e70','#c98500','#d55181','#008300','#9085e9','#e66767']; const tip=(()=>{const d=document.createElement('div');d.className='tip';document.body.appendChild(d);return d;})(); function drawChart(el){ const cfg=JSON.parse(el.dataset.chart); // {series:[{label,slot,data:[[ts,v],...]}], unit, normalize} const W=el.clientWidth||800, H=el.clientHeight||220, P={l:46,r:12,t:10,b:22}; const all=cfg.series.flatMap(s=>s.data); if(!all.length){el.innerHTML='
No data in range.
';return;} const ts=all.map(d=>+new Date(d[0].replace(' ','T')+':00Z')); const t0=Math.min(...ts), t1=Math.max(...ts); const norm=cfg.normalize&&cfg.series.length>1; const yFor=s=>{const vs=s.data.map(d=>d[1]);const mn=Math.min(...vs),mx=Math.max(...vs);return v=> norm? (mx===mn?50:(v-mn)/(mx-mn)*100) : v;}; let vmin=Infinity,vmax=-Infinity; cfg.series.forEach(s=>{const f=yFor(s);s.data.forEach(d=>{const v=f(d[1]);if(vvmax)vmax=v;});}); if(vmin===vmax){vmin-=1;vmax+=1;} const X=t=>P.l+( (t-t0)/(t1-t0||1) )*(W-P.l-P.r); const Y=v=>H-P.b-((v-vmin)/(vmax-vmin))*(H-P.t-P.b); let g=''; const steps=4; for(let i=0;i<=steps;i++){const v=vmin+(vmax-vmin)*i/steps,y=Y(v); g+=``; g+=`${norm?Math.round(v)+'%':fmt(v)}`;} const days=(t1-t0)/86400000; const nlab=Math.min(6,Math.max(2,Math.round(days/10)+2)); for(let i=0;i${new Date(t).toISOString().slice(5,10)}`;} cfg.series.forEach((s,si)=>{ const f=yFor(s); const col=SERIES[(s.slot??si)%SERIES.length]; const pts=s.data.map(d=>`${X(+new Date(d[0].replace(' ','T')+':00Z')).toFixed(1)},${Y(f(d[1])).toFixed(1)}`).join(' '); g+=``; }); g+=``; el.innerHTML=`${g}`; if(cfg.series.length>1){ el.insertAdjacentHTML('afterend','
'+cfg.series.map((s,si)=>`${s.label}${norm?' (indexed)':''}`).join('')+'
'); } el.onmousemove=e=>{ const r=el.getBoundingClientRect(), fx=e.clientX-r.left; const t=t0+((fx-P.l)/(W-P.l-P.r))*(t1-t0); let rows=''; cfg.series.forEach((s,si)=>{ let best=null,bd=1e18; s.data.forEach(d=>{const dt=Math.abs(+new Date(d[0].replace(' ','T')+':00Z')-t);if(dt${s.label}: ${fmt(best[1])}${cfg.unit?' '+cfg.unit:''} ${best[0]}`; }); tip.innerHTML=rows; tip.style.display='block'; tip.style.left=Math.min(e.clientX+14,innerWidth-260)+'px'; tip.style.top=(e.clientY+12)+'px'; }; el.onmouseleave=()=>tip.style.display='none'; } function fmt(v){return Math.abs(v)>=100?v.toFixed(0):Math.abs(v)>=10?v.toFixed(1):v.toFixed(2);} function spark(el){ const d=JSON.parse(el.dataset.spark); if(!d.length){el.textContent='';return;} const W=el.clientWidth||140,H=26,vs=d.map(x=>x[1]),mn=Math.min(...vs),mx=Math.max(...vs); const pts=d.map((x,i)=>`${(i/(d.length-1))*(W-2)+1},${H-2-((x[1]-mn)/(mx-mn||1))*(H-4)}`).join(' '); el.innerHTML=``; } addEventListener('DOMContentLoaded',()=>{ document.querySelectorAll('[data-chart]').forEach(drawChart); document.querySelectorAll('[data-spark]').forEach(spark); });