← back to Ga Allsites
Add Charts overview page: 7 KPI cards + top-15 domains bar chart + country pie (shared renderPie helper)
74fe8721e99a5fe6a42f199b4845c642b5fb1f30 · 2026-08-04 15:22:27 -0700 · Steve Abrams
Files touched
Diff
commit 74fe8721e99a5fe6a42f199b4845c642b5fb1f30
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue Aug 4 15:22:27 2026 -0700
Add Charts overview page: 7 KPI cards + top-15 domains bar chart + country pie (shared renderPie helper)
---
server.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++----------
1 file changed, 46 insertions(+), 10 deletions(-)
diff --git a/server.py b/server.py
index d5e3c35..63f2052 100644
--- a/server.py
+++ b/server.py
@@ -141,6 +141,11 @@ PAGE = r"""<!DOCTYPE html><html><head><meta charset="utf-8">
.pyr .lbl{font-size:12px;color:#333;min-width:150px;text-align:right}
.pyr .val{font-size:12px;color:#666;min-width:60px;text-align:left;font-variant-numeric:tabular-nums}
svg text{font:12px -apple-system,sans-serif}
+ /* KPI cards */
+ .kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:14px;margin-bottom:18px}
+ .kpi{background:#fff;border:1px solid #e6e8ec;border-radius:10px;padding:16px}
+ .kpi .kv{font-size:25px;font-weight:800;font-variant-numeric:tabular-nums;line-height:1.1}
+ .kpi .kl{font-size:12px;color:#667;margin-top:5px}
/* live moving graphics */
@keyframes pulse{0%{transform:scale(1);opacity:.9}70%{transform:scale(2.6);opacity:0}100%{opacity:0}}
.livehero{position:relative;padding:22px 26px;background:linear-gradient(120deg,#180d12,#2a1019);color:#fff;display:flex;align-items:center;gap:22px;flex-wrap:wrap}
@@ -155,6 +160,7 @@ PAGE = r"""<!DOCTYPE html><html><head><meta charset="utf-8">
<nav class="side" id="side">
<div class="brand">GA<br>FLEET</div>
<button data-tab="sites" class="on"><span class="ic">📊</span>Sites</button>
+ <button data-tab="charts"><span class="ic">📈</span>Charts</button>
<button data-tab="visuals"><span class="ic">🔺</span>Visuals</button>
<button data-tab="countries"><span class="ic">🌍</span>Country</button>
<button data-tab="map"><span class="ic">🗺️</span>Map</button>
@@ -181,6 +187,13 @@ PAGE = r"""<!DOCTYPE html><html><head><meta charset="utf-8">
<table id="t"><thead><tr id="hr"></tr></thead><tbody id="tb"></tbody><tfoot><tr id="tf"></tr></tfoot></table>
</div>
+ <!-- CHARTS overview -->
+ <div class="view panel" id="v-charts" hidden>
+ <div class="kpis" id="kpis"></div>
+ <div class="card"><h2>Top domains by sessions — 30 days</h2><div id="domainbars"></div></div>
+ <div class="card"><h2>Sessions by country — 30 days</h2><div class="piewrap"><div class="pie" id="cpie"></div><ul class="legend" id="cpielegend"></ul></div></div>
+ </div>
+
<!-- VISUALS -->
<div class="view panel" id="v-visuals" hidden>
<div class="card"><h2>Fleet mind map — top sites by 30-day sessions</h2><div id="mindmap"></div></div>
@@ -302,18 +315,40 @@ function renderVisuals(){
document.getElementById('mindmap').innerHTML=svg;
}
-/* ---- COUNTRIES ---- */
+/* ---- COUNTRIES + shared pie ---- */
async function ensureCountries(){if(CDATA)return;try{const r=await fetch(location.origin+'/api/countries');CDATA=await r.json();}catch(e){CDATA={rows:[]};}}
-async function renderCountries(){
- await ensureCountries();
- const rows=(CDATA.rows||[]);const tot=CDATA.total_sessions||rows.reduce((s,r)=>s+r.sessions,0)||1;
- const top=rows.slice(0,8);const otherS=rows.slice(8).reduce((s,r)=>s+r.sessions,0);
+function renderPie(pieId,legendId){
+ const rows=(CDATA&&CDATA.rows)||[];const tot=(CDATA&&CDATA.total_sessions)||rows.reduce((s,r)=>s+r.sessions,0)||1;
+ const top=rows.slice(0,8),otherS=rows.slice(8).reduce((s,r)=>s+r.sessions,0);
const segs=top.map((r,i)=>({name:r.country,v:r.sessions,c:PAL[i%PAL.length]}));
if(otherS>0)segs.push({name:'Other',v:otherS,c:'#bdc3c7'});
let acc=0;const stops=segs.map(s=>{const a=100*acc/tot,b=100*(acc+s.v)/tot;acc+=s.v;return `${s.c} ${a.toFixed(2)}% ${b.toFixed(2)}%`;}).join(',');
- document.getElementById('pie').style.background=`conic-gradient(${stops})`;
- document.getElementById('pielegend').innerHTML=segs.map(s=>`<li><span class="sw" style="background:${s.c}"></span>${esc(s.name)}<span class="v">${Math.round(s.v)} · ${(100*s.v/tot).toFixed(1)}%</span></li>`).join('');
- document.getElementById('ctb').innerHTML=rows.map(r=>`<tr><td class="l">${esc(r.country)}</td><td>${Math.round(r.sessions).toLocaleString()}</td><td>${Math.round(r.users).toLocaleString()}</td><td>${r.share}%</td></tr>`).join('');
+ const pie=document.getElementById(pieId);if(pie)pie.style.background=`conic-gradient(${stops})`;
+ const lg=document.getElementById(legendId);if(lg)lg.innerHTML=segs.map(s=>`<li><span class="sw" style="background:${s.c}"></span>${esc(s.name)}<span class="v">${Math.round(s.v)} · ${(100*s.v/tot).toFixed(1)}%</span></li>`).join('');
+}
+async function renderCountries(){
+ await ensureCountries();
+ renderPie('pie','pielegend');
+ document.getElementById('ctb').innerHTML=(CDATA.rows||[]).map(r=>`<tr><td class="l">${esc(r.country)}</td><td>${Math.round(r.sessions).toLocaleString()}</td><td>${Math.round(r.users).toLocaleString()}</td><td>${r.share}%</td></tr>`).join('');
+}
+
+/* ---- CHARTS overview (all domains on one page) ---- */
+async function renderCharts(){
+ await ensureCountries();
+ const rows=DATA.rows;
+ const 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);
+ const wavg=t30?wsum/t30:0;
+ const topC=((CDATA&&CDATA.rows)||[])[0]||{country:'—',share:0};
+ const kpi=[['Domains',rows.length],['Sessions · 7d',Math.round(t7).toLocaleString()],['Sessions · 30d',Math.round(t30).toLocaleString()],
+ ['Pageviews · 30d',Math.round(pv).toLocaleString()],['Avg time on site',fmtDurP(wavg)],['Top country',topC.country+(topC.share?' · '+topC.share+'%':'')],['Sites w/ traffic',active+' / '+rows.length]];
+ document.getElementById('kpis').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('');
+ const top=rows.filter(r=>dig(r,'d30.sessions')>0).slice(0,15),mx=Math.max(1,...top.map(r=>dig(r,'d30.sessions')));
+ document.getElementById('domainbars').innerHTML=top.map(r=>{const v=Math.round(dig(r,'d30.sessions'));
+ return `<div class="lrow"><span class="nm"><a href="https://analytics.google.com/analytics/web/#/p${r.property_id}/reports/intelligenthome" target="_blank" rel="noopener noreferrer">${esc(r.property)}</a></span><div class="lbar" style="width:${Math.max(6,Math.round(100*v/mx))}%;background:linear-gradient(90deg,#5b8def,#8a5bef)">${v}</div></div>`;}).join('');
+ renderPie('cpie','cpielegend');
}
/* ---- MAP (choropleth, zero-dep SVG) ---- */
@@ -378,12 +413,13 @@ async function pollLive(){
}
/* ---- tabs ---- */
-const TITLES={sites:'All Sites',visuals:'Visuals',countries:'User Country',map:'User Map',live:'Live'};
+const TITLES={sites:'All Sites',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','visuals','countries','map','live'].forEach(t=>document.getElementById('v-'+t).hidden=(t!==tab));
+ ['sites','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==='charts')renderCharts();
if(tab==='visuals')renderVisuals();
if(tab==='countries')renderCountries();
if(tab==='map')renderMap();
← 56643dc Animated Live view: pulsing count-up counter + live-filling
·
back to Ga Allsites
·
auto-save: 2026-08-05T07:40:04 (1 files) — server.py 132e099 →