Research Labs

2026-09-17 20:38 UTCopen field data · live

assets/app.js

Snapshot 2026.09.17-a2894de (current) · 65 lines · 4,037 bytes · tree · plain text

SHA-256 b56d9cacf29ef638bc9344dcabaf2165b172429a8d3f960774fa420e26311656
1/* Research Labs charts: dependency-free SVG line charts.
2 Rules applied: single axis, 2px lines, recessive grid, legend for >=2 series,
3 crosshair + tooltip, text in ink tokens, fixed series colours. */
4const SERIES=['#3987e5','#d95926','#199e70','#c98500','#d55181','#008300','#9085e9','#e66767'];
5const tip=(()=>{const d=document.createElement('div');d.className='tip';document.body.appendChild(d);return d;})();
6 
7function drawChart(el){
8 const cfg=JSON.parse(el.dataset.chart); // {series:[{label,slot,data:[[ts,v],...]}], unit, normalize}
9 const W=el.clientWidth||800, H=el.clientHeight||220, P={l:46,r:12,t:10,b:22};
10 const all=cfg.series.flatMap(s=>s.data);
11 if(!all.length){el.innerHTML='<div class="kv">No data in range.</div>';return;}
12 const ts=all.map(d=>+new Date(d[0].replace(' ','T')+':00Z'));
13 const t0=Math.min(...ts), t1=Math.max(...ts);
14 const norm=cfg.normalize&&cfg.series.length>1;
15 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;};
16 let vmin=Infinity,vmax=-Infinity;
17 cfg.series.forEach(s=>{const f=yFor(s);s.data.forEach(d=>{const v=f(d[1]);if(v<vmin)vmin=v;if(v>vmax)vmax=v;});});
18 if(vmin===vmax){vmin-=1;vmax+=1;}
19 const X=t=>P.l+( (t-t0)/(t1-t0||1) )*(W-P.l-P.r);
20 const Y=v=>H-P.b-((v-vmin)/(vmax-vmin))*(H-P.t-P.b);
21 let g='';
22 const steps=4;
23 for(let i=0;i<=steps;i++){const v=vmin+(vmax-vmin)*i/steps,y=Y(v);
24 g+=`<line class="gridline" x1="${P.l}" x2="${W-P.r}" y1="${y}" y2="${y}"/>`;
25 g+=`<text x="4" y="${y+3}">${norm?Math.round(v)+'%':fmt(v)}</text>`;}
26 const days=(t1-t0)/86400000;
27 const nlab=Math.min(6,Math.max(2,Math.round(days/10)+2));
28 for(let i=0;i<nlab;i++){const t=t0+(t1-t0)*i/(nlab-1),x=X(t);
29 g+=`<text x="${x-24}" y="${H-6}">${new Date(t).toISOString().slice(5,10)}</text>`;}
30 cfg.series.forEach((s,si)=>{
31 const f=yFor(s); const col=SERIES[(s.slot??si)%SERIES.length];
32 const pts=s.data.map(d=>`${X(+new Date(d[0].replace(' ','T')+':00Z')).toFixed(1)},${Y(f(d[1])).toFixed(1)}`).join(' ');
33 g+=`<polyline fill="none" stroke="${col}" stroke-width="2" points="${pts}"/>`;
34 });
35 g+=`<line class="axis" x1="${P.l}" x2="${W-P.r}" y1="${H-P.b}" y2="${H-P.b}"/>`;
36 el.innerHTML=`<svg width="${W}" height="${H}" role="img">${g}</svg>`;
37 if(cfg.series.length>1){
38 el.insertAdjacentHTML('afterend','<div class="legend">'+cfg.series.map((s,si)=>`<span><i class="sw" style="background:${SERIES[(s.slot??si)%SERIES.length]}"></i>${s.label}${norm?' (indexed)':''}</span>`).join('')+'</div>');
39 }
40 el.onmousemove=e=>{
41 const r=el.getBoundingClientRect(), fx=e.clientX-r.left;
42 const t=t0+((fx-P.l)/(W-P.l-P.r))*(t1-t0);
43 let rows='';
44 cfg.series.forEach((s,si)=>{
45 let best=null,bd=1e18;
46 s.data.forEach(d=>{const dt=Math.abs(+new Date(d[0].replace(' ','T')+':00Z')-t);if(dt<bd){bd=dt;best=d;}});
47 if(best)rows+=`<div><i class="sw" style="background:${SERIES[(s.slot??si)%SERIES.length]}"></i>${s.label}: <b>${fmt(best[1])}</b>${cfg.unit?' '+cfg.unit:''} <span style="color:#8b8b83">${best[0]}</span></div>`;
48 });
49 tip.innerHTML=rows; tip.style.display='block';
50 tip.style.left=Math.min(e.clientX+14,innerWidth-260)+'px'; tip.style.top=(e.clientY+12)+'px';
51 };
52 el.onmouseleave=()=>tip.style.display='none';
53}
54function fmt(v){return Math.abs(v)>=100?v.toFixed(0):Math.abs(v)>=10?v.toFixed(1):v.toFixed(2);}
55function spark(el){
56 const d=JSON.parse(el.dataset.spark); if(!d.length){el.textContent='';return;}
57 const W=el.clientWidth||140,H=26,vs=d.map(x=>x[1]),mn=Math.min(...vs),mx=Math.max(...vs);
58 const pts=d.map((x,i)=>`${(i/(d.length-1))*(W-2)+1},${H-2-((x[1]-mn)/(mx-mn||1))*(H-4)}`).join(' ');
59 el.innerHTML=`<svg width="${W}" height="${H}"><polyline fill="none" stroke="#8b8b83" stroke-width="1.5" points="${pts}"/></svg>`;
60}
61addEventListener('DOMContentLoaded',()=>{
62 document.querySelectorAll('[data-chart]').forEach(drawChart);
63 document.querySelectorAll('[data-spark]').forEach(spark);
64});
65 

Lines can be cited as assets/app.js L120-L140, platform code 2026.09.17-a2894de; add #L120-L140 to this page's address to highlight them. Source shown for scrutiny of the research platform. All rights reserved. Not offered as installable software.