Add KPI event and CAC data pipeline
This commit is contained in:
+3
-3
@@ -52,11 +52,11 @@ function updateDateBounds(reset=false){const months=selected('month'),rows=ancho
|
||||
function updateAnchoredFilters(resetDates=false){const rows=anchorRows();multiOptions('month',rows.map(r=>r.date.slice(0,7)));singleOptions('geo',rows.map(r=>r.geo));singleOptions('source',rows.map(r=>r.source));multiOptions('group',rows.map(r=>r.group));updateDateBounds(resetDates)}
|
||||
function filtered(){const q=$('search').value.trim().toLowerCase(),months=selected('month'),groups=selected('group');return all.filter(r=>r.status==='Active'&&(!$('pkg').value||r.pkg===$('pkg').value)&&(!months.length||months.includes(r.date.slice(0,7)))&&(!$('geo').value||r.geo===$('geo').value)&&(!$('source').value||r.source===$('source').value)&&(!groups.length||groups.includes(r.group))&&(!$('start').value||r.date>=$('start').value)&&(!$('end').value||r.date<=$('end').value)&&(!q||(`${r.campaign} ${r.adpingId}`).toLowerCase().includes(q)))}
|
||||
const tracksEvent=r=>{const k=String(r.kpi||'').trim();return k&&k!=='-'};
|
||||
function aggregate(rows,keys){const m=new Map;for(const r of rows){const k=keys.map(x=>r[x]).join('\u0001');if(!m.has(k))m.set(k,{...Object.fromEntries(keys.map(x=>[x,r[x]])),impression:0,click:0,conversions:0,spend:0,event:0,eventKnown:false,kpi:r.kpi||''});const o=m.get(k);for(const x of ['impression','click','conversions','spend'])o[x]+=+r[x]||0;if(r.event!==null&&r.event!==undefined&&r.event!==''){o.event+=+r.event||0;o.eventKnown=true}}return [...m.values()]}
|
||||
function cards(rows){const t=aggregate(rows,[])[0]||{impression:0,click:0,conversions:0,spend:0},e=aggregate(rows.filter(tracksEvent),[])[0]||{event:0,eventKnown:false,spend:0};const vals=[['Spend',money(t.spend)],['Event',e.eventKnown?n(e.event):'—'],['CAC',e.eventKnown&&e.event?money(e.spend/e.event):'—'],['Conversions',n(t.conversions)],['CPI',t.conversions?money(t.spend/t.conversions):'—'],['Impression',n(t.impression)],['Click',n(t.click)],['CTR',pct(rate(t.click,t.impression))],['CVR',pct(rate(t.conversions,t.click))]];$('cards').innerHTML=vals.map(x=>`<div class="panel card"><div class="label">${x[0]}</div><div class="value">${x[1]}</div></div>`).join('')}
|
||||
function aggregate(rows,keys){const m=new Map;for(const r of rows){const k=keys.map(x=>r[x]).join('\u0001');if(!m.has(k))m.set(k,{...Object.fromEntries(keys.map(x=>[x,r[x]])),impression:0,click:0,conversions:0,spend:0,event:0,eventSpend:0,eventKnown:false,kpi:r.kpi||''});const o=m.get(k);for(const x of ['impression','click','conversions','spend'])o[x]+=+r[x]||0;if(r.event!==null&&r.event!==undefined&&r.event!==''){o.event+=+r.event||0;o.eventSpend+=+r.spend||0;o.eventKnown=true}}return [...m.values()]}
|
||||
function cards(rows){const t=aggregate(rows,[])[0]||{impression:0,click:0,conversions:0,spend:0},e=aggregate(rows.filter(tracksEvent),[])[0]||{event:0,eventKnown:false,eventSpend:0};const vals=[['Spend',money(t.spend)],['Event',e.eventKnown?n(e.event):'—'],['CAC',e.eventKnown&&e.event?money(e.eventSpend/e.event):'—'],['Conversions',n(t.conversions)],['CPI',t.conversions?money(t.spend/t.conversions):'—'],['Impression',n(t.impression)],['Click',n(t.click)],['CTR',pct(rate(t.click,t.impression))],['CVR',pct(rate(t.conversions,t.click))]];$('cards').innerHTML=vals.map(x=>`<div class="panel card"><div class="label">${x[0]}</div><div class="value">${x[1]}</div></div>`).join('')}
|
||||
function compact(v,prefix=''){return prefix+Intl.NumberFormat('en-US',{notation:'compact',maximumFractionDigits:1}).format(v)}
|
||||
function trend(rows){const d=aggregate(rows,['date']).sort((a,b)=>a.date.localeCompare(b.date)),svg=$('trend'),tip=$('trendTip');if(!d.length){svg.innerHTML='<text x="450" y="135" text-anchor="middle" class="axis-label">当前筛选没有数据</text>';return}const W=900,H=270,L=62,R=66,T=20,B=38,maxS=Math.max(...d.map(x=>x.spend),1),maxC=Math.max(...d.map(x=>x.conversions),1),x=i=>L+(W-L-R)*(d.length===1?.5:i/(d.length-1)),y=(v,max)=>T+(H-T-B)*(1-v/max),path=(key,max)=>d.map((r,i)=>(i?'L':'M')+x(i).toFixed(1)+' '+y(r[key],max).toFixed(1)).join(' ');let parts=[];for(let i=0;i<=4;i++){const yy=T+(H-T-B)*i/4,ratio=1-i/4;parts.push(`<line class="gridline" x1="${L}" y1="${yy}" x2="${W-R}" y2="${yy}"/><text class="axis-label" x="${L-8}" y="${yy+4}" text-anchor="end">${compact(maxS*ratio,'$')}</text><text class="axis-label" x="${W-R+8}" y="${yy+4}">${compact(maxC*ratio)}</text>`)}const labelStep=Math.max(1,Math.ceil(d.length/8));parts.push(...d.map((r,i)=>(i%labelStep===0||i===d.length-1)?`<text class="axis-label" x="${x(i)}" y="${H-12}" text-anchor="middle">${r.date.slice(5)}</text>`:''));parts.push(`<text class="axis-title" x="${L}" y="11" fill="#1769e0">Spend (USD)</text><text class="axis-title" x="${W-R}" y="11" text-anchor="end" fill="#16825d">Conversions</text>`);if(trendVisible.spend)parts.push(`<path class="spend-line" d="${path('spend',maxS)}"/>`);if(trendVisible.conversions)parts.push(`<path class="conv-line" d="${path('conversions',maxC)}"/>`);parts.push(...d.map((r,i)=>`<g class="trend-hit" data-index="${i}"><circle cx="${x(i)}" cy="${trendVisible.spend?y(r.spend,maxS):y(r.conversions,maxC)}" r="12" fill="transparent"/><circle class="point" cx="${x(i)}" cy="${trendVisible.spend?y(r.spend,maxS):y(r.conversions,maxC)}" r="3.5" fill="${trendVisible.spend?'#1769e0':'#16825d'}"/></g>`));svg.innerHTML=parts.join('');svg.querySelectorAll('.trend-hit').forEach(el=>{el.addEventListener('mouseenter',()=>{const r=d[+el.dataset.index];tip.innerHTML=`<strong>${r.date}</strong><br>Spend:${money(r.spend)}<br>Conversions:${n(r.conversions)}`;tip.style.display='block'});el.addEventListener('mousemove',e=>{const box=svg.getBoundingClientRect(),panel=svg.parentElement.getBoundingClientRect();tip.style.left=Math.min(e.clientX-panel.left+12,panel.width-tip.offsetWidth-12)+'px';tip.style.top=(e.clientY-panel.top-tip.offsetHeight-10)+'px'});el.addEventListener('mouseleave',()=>tip.style.display='none')})}
|
||||
function table(rows){const d=aggregate(rows,['date','group','campaign','kpi']).sort((a,b)=>b.date.localeCompare(a.date)||(a.group==='对照组'?-1:1)-(b.group==='对照组'?-1:1)||b.spend-a.spend);$('count').textContent=`${n(d.length)} 行`;const limited=d.slice(0,2000),dateCounts={};for(const r of limited)dateCounts[r.date]=(dateCounts[r.date]||0)+1;const seen=new Set;$('body').innerHTML=limited.map(r=>{const first=!seen.has(r.date),required=tracksEvent(r);seen.add(r.date);return`<tr class="${first?'date-start':''}">${first?`<td class="date-cell" rowspan="${dateCounts[r.date]}">${r.date}</td>`:''}<td class="${r.group==='对照组'?'control':'experiment'}">${r.group}</td><td>${r.campaign}</td><td>${r.kpi||'-'}</td><td class="num">${required&&r.eventKnown?n(r.event):'—'}</td><td class="num">${money(r.spend)}</td><td class="num">${required&&r.eventKnown&&r.event?money(r.spend/r.event):'—'}</td><td class="num">${n(r.impression)}</td><td class="num">${n(r.click)}</td><td class="num">${n(r.conversions)}</td><td class="num">${pct(rate(r.click,r.impression))}</td><td class="num">${pct(rate(r.conversions,r.click))}</td><td class="num">${r.conversions?money(r.spend/r.conversions):'—'}</td></tr>`}).join('')||'<tr><td colspan="13" class="empty">当前筛选没有数据</td></tr>'}
|
||||
function table(rows){const d=aggregate(rows,['date','group','campaign','kpi']).sort((a,b)=>b.date.localeCompare(a.date)||(a.group==='对照组'?-1:1)-(b.group==='对照组'?-1:1)||b.spend-a.spend);$('count').textContent=`${n(d.length)} 行`;const limited=d.slice(0,2000),dateCounts={};for(const r of limited)dateCounts[r.date]=(dateCounts[r.date]||0)+1;const seen=new Set;$('body').innerHTML=limited.map(r=>{const first=!seen.has(r.date),required=tracksEvent(r);seen.add(r.date);return`<tr class="${first?'date-start':''}">${first?`<td class="date-cell" rowspan="${dateCounts[r.date]}">${r.date}</td>`:''}<td class="${r.group==='对照组'?'control':'experiment'}">${r.group}</td><td>${r.campaign}</td><td>${r.kpi||'-'}</td><td class="num">${required&&r.eventKnown?n(r.event):'—'}</td><td class="num">${money(r.spend)}</td><td class="num">${required&&r.eventKnown&&r.event?money(r.eventSpend/r.event):'—'}</td><td class="num">${n(r.impression)}</td><td class="num">${n(r.click)}</td><td class="num">${n(r.conversions)}</td><td class="num">${pct(rate(r.click,r.impression))}</td><td class="num">${pct(rate(r.conversions,r.click))}</td><td class="num">${r.conversions?money(r.spend/r.conversions):'—'}</td></tr>`}).join('')||'<tr><td colspan="13" class="empty">当前筛选没有数据</td></tr>'}
|
||||
function render(){lastRows=filtered();cards(lastRows);trend(lastRows);table(lastRows)}
|
||||
function toggleTrend(key,id){trendVisible[key]=!trendVisible[key];if(!trendVisible.spend&&!trendVisible.conversions)trendVisible[key]=true;$(id).classList.toggle('off',!trendVisible[key]);$(id).setAttribute('aria-pressed',trendVisible[key]);trend(lastRows)}
|
||||
$('toggleSpend').addEventListener('click',()=>toggleTrend('spend','toggleSpend'));$('toggleConversions').addEventListener('click',()=>toggleTrend('conversions','toggleConversions'));
|
||||
|
||||
Reference in New Issue
Block a user