← back to Rentv Ad Engine
analytics: per-client airing-history CSV (one download per advertiser) + guest-location map (geo scatter) + sponsor-overlap matrix (shared-episode heatmap)
59cc97721ae9070ecb3f3d5166e35618d90d41cd · 2026-08-07 11:29:28 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
Diff
commit 59cc97721ae9070ecb3f3d5166e35618d90d41cd
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Aug 7 11:29:28 2026 -0700
analytics: per-client airing-history CSV (one download per advertiser) + guest-location map (geo scatter) + sponsor-overlap matrix (shared-episode heatmap)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
public/charts.html | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 79 insertions(+)
diff --git a/public/charts.html b/public/charts.html
index 6b67880..9d1095e 100644
--- a/public/charts.html
+++ b/public/charts.html
@@ -92,6 +92,20 @@
</div>
<div style="max-height:520px;overflow:auto"><table id="airtbl"></table></div>
</div>
+
+ <h2>Per-client airing history <span class="muted" style="text-transform:none;font-weight:400">— download one client's spots as CSV</span></h2>
+ <div class="panel" id="perclient"></div>
+
+ <div class="two">
+ <div>
+ <h2>Guest location map</h2>
+ <div class="panel" id="gmap"></div>
+ </div>
+ <div>
+ <h2>Sponsor-overlap matrix <span class="muted" style="text-transform:none;font-weight:400">— shared episodes</span></h2>
+ <div class="panel" id="overlap"></div>
+ </div>
+ </div>
</main>
<script>
@@ -130,6 +144,9 @@ async function boot(){
renderGuests(guests);
renderTopics(guests);
renderAiringTable(airings,rv);
+ renderPerClient(clients,airings);
+ renderGuestMap(guests);
+ renderOverlap(airings);
}
// ── LIQUID FILL GAUGES (animated wave) ──
@@ -360,6 +377,68 @@ function renderAiringTable(airings,rv){
draw();
}
+// ── PER-CLIENT airing history CSV ──
+function renderPerClient(clients,airings){
+ const byC={};airings.forEach(a=>{(byC[a.advertiser]=byC[a.advertiser]||[]).push(a);});
+ const list=clients.filter(c=>byC[c.name]).map(c=>({name:c.name,rows:byC[c.name].slice().sort((x,y)=>y.episode_id-x.episode_id||x.start-y.start)}));
+ const dl=(name,rows)=>{
+ const head="Episode,Timecode,Deep link,Spoken copy";
+ const body=rows.map(r=>[r.episode_title,r.timecode,r.deeplink,r.spoken_copy].map(v=>`"${String(v||"").replace(/"/g,'""')}"`).join(",")).join("\n");
+ const b=new Blob([head+"\n"+body],{type:"text/csv"});const u=URL.createObjectURL(b);
+ const a=document.createElement("a");a.href=u;a.download=`airings-${name.replace(/[^a-z0-9]+/gi,"-").toLowerCase()}.csv`;a.click();URL.revokeObjectURL(u);
+ };
+ $("#perclient").innerHTML=`<div style="display:flex;flex-wrap:wrap;gap:8px">`+
+ list.map((c,i)=>`<button class="mailbtn" data-i="${i}" style="background:#0e1116;color:var(--ink);border:1px solid var(--line);font-weight:500">⬇ ${esc(c.name)} <span class="muted">(${c.rows.length})</span></button>`).join("")+`</div>`;
+ $("#perclient").querySelectorAll("button").forEach(b=>b.onclick=()=>{const c=list[+b.dataset.i];dl(c.name,c.rows);});
+}
+
+// ── GUEST LOCATION MAP (geographic scatter over a US frame) ──
+const GEO=[["san diego",32.72,-117.16],["marina del rey",33.98,-118.45],["newport beach",33.62,-117.93],
+ ["irvine",33.68,-117.83],["long beach",33.77,-118.19],["santa ana",33.75,-117.87],["carlsbad",33.16,-117.35],
+ ["orange county",33.72,-117.83],["inland empire",34.06,-117.65],["ontario",34.06,-117.65],
+ ["los angeles",34.05,-118.24],["hollywood",34.09,-118.33],["san francisco",37.77,-122.42],
+ ["sacramento",38.58,-121.49],["new york",40.71,-74.01],["phoenix",33.45,-112.07],
+ ["bethesda",39.0,-77.1],["maryland",39.0,-77.1],["southern california",34.05,-118.24],["california",36.7,-119.7]];
+function geo(loc){const s=(loc||"").toLowerCase();for(const[k,la,lo]of GEO)if(s.includes(k))return[la,lo];return null;}
+function renderGuestMap(guests){
+ const cnt={},unmapped=[];
+ guests.forEach(g=>{const p=geo(g.location);if(p){const key=p.join(",");(cnt[key]=cnt[key]||{p,n:0,names:[]});cnt[key].n++;cnt[key].names.push(g.name);}else if((g.location||"").trim())unmapped.push(g.location);});
+ const W=520,H=300,padX=20,padY=20;
+ const X=lo=>padX+((lo-(-125))/((-66)-(-125)))*(W-2*padX);
+ const Y=la=>padY+((50-la)/(50-24))*(H-2*padY);
+ const pts=Object.values(cnt);const max=Math.max(...pts.map(p=>p.n),1);
+ const dots=pts.map((p,i)=>{const x=X(p.p[1]),y=Y(p.p[0]),r=6+14*Math.sqrt(p.n/max);
+ return `<circle cx="${x.toFixed(0)}" cy="${y.toFixed(0)}" r="${r.toFixed(0)}" fill="var(--teal)" opacity=".55" stroke="var(--teal)"><title>${esc(p.names.join(", "))} (${p.n})</title></circle>`;}).join("");
+ $("#gmap").innerHTML=`<svg width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" style="background:#0e1116;border-radius:8px">
+ <rect x="${padX}" y="${padY}" width="${W-2*padX}" height="${H-2*padY}" fill="none" stroke="#2a3038"></rect>
+ <text x="${X(-119).toFixed(0)}" y="${Y(37).toFixed(0)}" fill="#3a4450" font-size="11">CA</text>
+ <text x="${X(-74).toFixed(0)}" y="${Y(40.7).toFixed(0)}" fill="#3a4450" font-size="11">NY</text>
+ ${dots}</svg>
+ <div class="muted" style="margin-top:6px">Guests plotted by where their company is based · bubble = # guests${unmapped.length?` · ${unmapped.length} unplaced`:""}</div>`;
+}
+
+// ── SPONSOR-OVERLAP MATRIX (shared episodes heatmap) ──
+function renderOverlap(airings){
+ const epSet={};airings.forEach(a=>{(epSet[a.advertiser]=epSet[a.advertiser]||new Set()).add(a.episode_id);});
+ const top=Object.entries(epSet).map(([k,v])=>[k,v]).sort((a,b)=>b[1].size-a[1].size).slice(0,8);
+ if(top.length<2){$("#overlap").innerHTML='<div class="muted">Not enough co-sponsored episodes.</div>';return;}
+ const shared=(a,b)=>{let n=0;a.forEach(e=>{if(b.has(e))n++;});return n;};
+ const maxOff=Math.max(1,...top.flatMap(([,a],i)=>top.map(([,b],j)=>i===j?0:shared(a,b))));
+ const short=n=>n.replace(/ (Group|Companies|Savings Bank|Property Tax Appeal|Real Estate Inspectors|Mortgage Lenders|Partners|Mortgage)$/,"").slice(0,14);
+ let h='<table style="border-collapse:collapse;font-size:11px"><thead><tr><th></th>'+
+ top.map(([n])=>`<th style="padding:4px 6px;color:var(--dim);writing-mode:vertical-rl;transform:rotate(180deg);white-space:nowrap;height:80px">${esc(short(n))}</th>`).join("")+'</tr></thead><tbody>';
+ top.forEach(([rn,ra],i)=>{
+ h+=`<tr><td style="padding:4px 8px;color:var(--dim);white-space:nowrap;text-align:right">${esc(short(rn))}</td>`;
+ top.forEach(([,ca],j)=>{
+ if(i===j){h+=`<td style="width:30px;height:30px;text-align:center;background:#1c2530;color:var(--dim)">${ra.size}</td>`;}
+ else{const s=shared(ra,ca);const op=s/maxOff;h+=`<td title="${s} shared" style="width:30px;height:30px;text-align:center;background:rgba(45,212,191,${(op*0.85).toFixed(2)});color:${op>0.4?'#08131a':'var(--dim)'}">${s||""}</td>`;}
+ });
+ h+='</tr>';
+ });
+ h+='</tbody></table><div class="muted" style="margin-top:6px">Diagonal = total episodes sponsored; cell = episodes two sponsors shared.</div>';
+ $("#overlap").innerHTML=h;
+}
+
boot();
</script>
</body>
← 39e94f3 analytics: add guest-topics word-frequency cloud + ad airing
·
back to Rentv Ad Engine
·
auto-data-snapshot: 2026-08-07T11:47:33 (14 data files) — da 91e1534 →