Show package KPI event name on metric card
This commit is contained in:
+3
-2
@@ -12,7 +12,7 @@
|
|||||||
.filters{display:grid;grid-template-columns:repeat(8,minmax(125px,1fr));gap:12px;padding:16px;margin-bottom:16px}.field label{display:block;color:var(--muted);font-size:12px;margin-bottom:5px}
|
.filters{display:grid;grid-template-columns:repeat(8,minmax(125px,1fr));gap:12px;padding:16px;margin-bottom:16px}.field label{display:block;color:var(--muted);font-size:12px;margin-bottom:5px}
|
||||||
select,input{width:100%;height:36px;border:1px solid #cfd7df;border-radius:7px;background:#fff;padding:0 9px;color:var(--text)}
|
select,input{width:100%;height:36px;border:1px solid #cfd7df;border-radius:7px;background:#fff;padding:0 9px;color:var(--text)}
|
||||||
select[multiple]{height:72px;padding:4px 8px}.range-note{font-size:11px;color:var(--muted);margin-top:4px}
|
select[multiple]{height:72px;padding:4px 8px}.range-note{font-size:11px;color:var(--muted);margin-top:4px}
|
||||||
.cards{display:grid;grid-template-columns:repeat(9,1fr);gap:12px;margin-bottom:16px}.card{padding:16px}.card .label{color:var(--muted);font-size:12px}.card .value{font-size:23px;font-weight:650;margin-top:5px;font-variant-numeric:tabular-nums}
|
.cards{display:grid;grid-template-columns:repeat(9,1fr);gap:12px;margin-bottom:16px}.card{padding:16px;min-width:0}.card .label{color:var(--muted);font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.card .value{font-size:23px;font-weight:650;margin-top:5px;font-variant-numeric:tabular-nums}
|
||||||
.grid{margin-bottom:16px}.chart{padding:18px;min-height:340px;position:relative}.title{font-size:16px;font-weight:650;margin-bottom:12px}.chart-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.chart-sub{font-size:12px;color:var(--muted);margin-top:-8px}
|
.grid{margin-bottom:16px}.chart{padding:18px;min-height:340px;position:relative}.title{font-size:16px;font-weight:650;margin-bottom:12px}.chart-head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px}.chart-sub{font-size:12px;color:var(--muted);margin-top:-8px}
|
||||||
svg{width:100%;height:270px;margin-top:8px}.axis{stroke:#d8dfe6}.gridline{stroke:#edf1f4}.spend-line{fill:none;stroke:var(--brand);stroke-width:2.5}.conv-line{fill:none;stroke:var(--good);stroke-width:2.5}.legend{display:flex;gap:8px}.legend button{border:1px solid var(--line);border-radius:16px;background:#fff;color:var(--muted);padding:5px 10px;cursor:pointer}.legend button.off{opacity:.4}.dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px}.axis-label{fill:#74808c;font-size:11px}.axis-title{font-size:11px;font-weight:650}.point{stroke:#fff;stroke-width:2;cursor:crosshair}.hover-line{stroke:#9aa7b3;stroke-dasharray:3 3}.tooltip{position:absolute;display:none;pointer-events:none;background:#17212b;color:#fff;border-radius:7px;padding:8px 10px;font-size:12px;line-height:1.55;box-shadow:0 4px 14px #0003;z-index:2}.num{text-align:right;font-variant-numeric:tabular-nums}
|
svg{width:100%;height:270px;margin-top:8px}.axis{stroke:#d8dfe6}.gridline{stroke:#edf1f4}.spend-line{fill:none;stroke:var(--brand);stroke-width:2.5}.conv-line{fill:none;stroke:var(--good);stroke-width:2.5}.legend{display:flex;gap:8px}.legend button{border:1px solid var(--line);border-radius:16px;background:#fff;color:var(--muted);padding:5px 10px;cursor:pointer}.legend button.off{opacity:.4}.dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:5px}.axis-label{fill:#74808c;font-size:11px}.axis-title{font-size:11px;font-weight:650}.point{stroke:#fff;stroke-width:2;cursor:crosshair}.hover-line{stroke:#9aa7b3;stroke-dasharray:3 3}.tooltip{position:absolute;display:none;pointer-events:none;background:#17212b;color:#fff;border-radius:7px;padding:8px 10px;font-size:12px;line-height:1.55;box-shadow:0 4px 14px #0003;z-index:2}.num{text-align:right;font-variant-numeric:tabular-nums}
|
||||||
.table-panel{padding:18px}.table-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}.table-note{color:var(--muted);font-size:12px}.table-wrap{overflow:auto;max-height:640px;border:1px solid var(--line);border-radius:8px}
|
.table-panel{padding:18px}.table-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}.table-note{color:var(--muted);font-size:12px}.table-wrap{overflow:auto;max-height:640px;border:1px solid var(--line);border-radius:8px}
|
||||||
@@ -52,8 +52,9 @@ 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 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)))}
|
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!=='-'};
|
const tracksEvent=r=>{const k=String(r.kpi||'').trim();return k&&k!=='-'};
|
||||||
|
function eventLabel(rows){const names=[...new Set(rows.filter(tracksEvent).map(r=>String(r.kpi).trim()))];return names.length?names.join(' / '):'KPI Event'}
|
||||||
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 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 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},eventName=eventLabel(rows);const vals=[['Spend',money(t.spend)], [eventName,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" title="${x[0]}">${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 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 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.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 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>'}
|
||||||
|
|||||||
Reference in New Issue
Block a user