← back to Ga Allsites
Add Global tab: animated liquid-fill gauge orbs (zero-dep SVG wave) for headline fleet stats
531a1ab4cb02b029332f9bbb7796f5a77ce419e1 · 2026-08-05 07:41:35 -0700 · Steve Abrams
Files touched
Diff
commit 531a1ab4cb02b029332f9bbb7796f5a77ce419e1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Aug 5 07:41:35 2026 -0700
Add Global tab: animated liquid-fill gauge orbs (zero-dep SVG wave) for headline fleet stats
---
server.py | 41 +++++++++++++++++++++++++++++++++++++++--
1 file changed, 39 insertions(+), 2 deletions(-)
diff --git a/server.py b/server.py
index 64465cd..047b6a5 100644
--- a/server.py
+++ b/server.py
@@ -307,6 +307,42 @@ function renderTable(){
document.getElementById('tf').innerHTML=vis().map((c,i)=>i===0?`<td class="l">Σ ${rows.length} sites</td>`:(c.t==='l'?'<td class="l"></td>':c.notot?'<td></td>':`<td>${num(tot(c.k))}</td>`)).join('');
}
+/* ---- GLOBAL: liquid-fill gauge orbs ---- */
+function liquidOrb(level,center,label,sub,color,i){
+ level=Math.max(0,Math.min(1,+level||0));
+ const R=80,cx=85,cy=85,top=(cy-R)+(2*R)*(1-level),amp=5,wl=80;
+ let d='M0 '+top.toFixed(1);
+ for(let x=0;x<=320;x+=8){d+=' L'+x+' '+(top+amp*Math.sin((x/wl)*Math.PI*2)).toFixed(1);}
+ d+=' L320 170 L0 170 Z';
+ const id='clip'+i,dur=(3+i*0.6).toFixed(1);
+ return `<div class="orb"><svg viewBox="0 0 170 170">
+ <defs><clipPath id="${id}"><circle cx="${cx}" cy="${cy}" r="${R}"/></clipPath></defs>
+ <circle cx="${cx}" cy="${cy}" r="${R+3}" fill="#f2f6fb" stroke="#dbe3ec" stroke-width="2"/>
+ <g clip-path="url(#${id})">
+ <path class="wave" d="${d}" fill="${color}" opacity=".85" style="animation-duration:${dur}s"/>
+ <path class="wave" d="${d}" fill="${color}" opacity=".4" style="animation-duration:${(+dur+1.6).toFixed(1)}s;animation-direction:reverse"/>
+ </g>
+ <circle cx="${cx}" cy="${cy}" r="${R}" fill="none" stroke="#c3d0de" stroke-width="1"/>
+ <text class="ocenter" x="${cx}" y="${cy+9}" text-anchor="middle">${center}</text>
+ </svg><div class="olabel">${esc(label)}</div><div class="osub">${esc(sub)}</div></div>`;
+}
+async function renderGlobal(){
+ await ensureCountries();
+ const rows=DATA.rows,total=rows.length,active=rows.filter(r=>dig(r,'d30.sessions')>0).length;
+ const t7=rows.reduce((s,r)=>s+dig(r,'d7.sessions'),0),t30=rows.reduce((s,r)=>s+dig(r,'d30.sessions'),0);
+ const pv=rows.reduce((s,r)=>s+dig(r,'d30.screenPageViews'),0);
+ const wsum=rows.reduce((s,r)=>s+(dig(r,'d30.averageSessionDuration')||0)*(dig(r,'d30.sessions')||0),0),wavg=t30?wsum/t30:0;
+ const topC=((CDATA&&CDATA.rows)||[])[0]||{country:'—',share:0};
+ document.getElementById('orbs').innerHTML=[
+ liquidOrb(active/Math.max(1,total),active,'Sites with traffic','of '+total+' total','#27ae60',0),
+ liquidOrb((topC.share||0)/100,(topC.share||0)+'%','Top country',topC.country,'#5b8def',1),
+ liquidOrb(t30?t7/t30:0,Math.round(t7).toLocaleString(),'7-day share','of 30d ('+Math.round(t30).toLocaleString()+')','#8a5bef',2),
+ liquidOrb(Math.min(1,wavg/90),fmtDurP(wavg),'Avg time on site','vs 1:30 benchmark','#e67e22',3),
+ ].join('');
+ const kpi=[['Domains',total],['Sessions · 7d',Math.round(t7).toLocaleString()],['Sessions · 30d',Math.round(t30).toLocaleString()],['Pageviews · 30d',Math.round(pv).toLocaleString()],['Top country',topC.country]];
+ document.getElementById('gkpis').innerHTML=kpi.map((k,i)=>`<div class="kpi"><div class="kv" style="color:${PAL[i%PAL.length]}">${k[1]}</div><div class="kl">${k[0]}</div></div>`).join('');
+}
+
/* ---- VISUALS ---- */
function renderVisuals(){
const top=DATA.rows.filter(r=>dig(r,'d30.sessions')>0).slice(0,10);
@@ -431,12 +467,13 @@ async function pollLive(){
}
/* ---- tabs ---- */
-const TITLES={sites:'All Sites',charts:'Charts — all domains',visuals:'Visuals',countries:'User Country',map:'User Map',live:'Live'};
+const TITLES={sites:'All Sites',global:'Global',charts:'Charts — all domains',visuals:'Visuals',countries:'User Country',map:'User Map',live:'Live'};
function showTab(tab){
document.querySelectorAll('.side button').forEach(b=>b.classList.toggle('on',b.dataset.tab===tab));
- ['sites','charts','visuals','countries','map','live'].forEach(t=>document.getElementById('v-'+t).hidden=(t!==tab));
+ ['sites','global','charts','visuals','countries','map','live'].forEach(t=>document.getElementById('v-'+t).hidden=(t!==tab));
document.getElementById('htitle').textContent=TITLES[tab];
clearInterval(liveTimer);liveTimer=null;
+ if(tab==='global')renderGlobal();
if(tab==='charts')renderCharts();
if(tab==='visuals')renderVisuals();
if(tab==='countries')renderCountries();
← 132e099 auto-save: 2026-08-05T07:40:04 (1 files) — server.py
·
back to Ga Allsites
·
ETL: flag-gated cache push to Kamatera (keeps analytics.agen c1f48fb →