Add attribution dashboard — CPA by keywords/conversion by managers/LTV by channel
This commit is contained in:
1 parent
43d7cf49cf
commit
d5fb5b4949
1 file changed
+850
@@ -0,0 +1,850 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ROI24 — Attribution Dashboard</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg:#09090b;--card:#111113;--border:rgba(255,255,255,.06);--border-h:rgba(255,255,255,.1);
|
||||
--text:#fafafa;--sec:#a1a1aa;--muted:#52525b;
|
||||
--g:#22c55e;--gd:rgba(34,197,94,.12);
|
||||
--b:#3b82f6;--bd:rgba(59,130,246,.12);
|
||||
--a:#f59e0b;--ad:rgba(245,158,11,.12);
|
||||
--r:#ef4444;--rd:rgba(239,68,68,.12);
|
||||
--p:#a855f7;--pd:rgba(168,85,247,.12);
|
||||
--c:#06b6d4;--cd:rgba(6,182,212,.12);
|
||||
--mono:'JetBrains Mono',monospace;--ease:cubic-bezier(.4,0,.2,1);
|
||||
}
|
||||
body{font-family:'Inter',-apple-system,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;-webkit-font-smoothing:antialiased;min-height:100vh}
|
||||
.hdr{display:flex;align-items:center;justify-content:space-between;padding:12px 28px;background:rgba(17,17,19,.85);backdrop-filter:blur(16px);border-bottom:1px solid var(--border);position:sticky;top:0;z-index:100}
|
||||
.logo{font-family:var(--mono);font-weight:700;font-size:15px;display:flex;align-items:center;gap:8px}
|
||||
.dot{width:8px;height:8px;background:var(--g);border-radius:50%;box-shadow:0 0 8px rgba(34,197,94,.4);animation:pulse 2s ease-in-out infinite}
|
||||
@keyframes pulse{0%,100%{box-shadow:0 0 8px rgba(34,197,94,.4)}50%{box-shadow:0 0 16px rgba(34,197,94,.15)}}
|
||||
.hdr-right{display:flex;align-items:center;gap:16px;font-size:12px}
|
||||
.live{display:flex;align-items:center;gap:6px;color:var(--g);font-weight:600}
|
||||
.live .dot{width:6px;height:6px}
|
||||
.clock{font-family:var(--mono);color:var(--muted);font-size:11px}
|
||||
|
||||
/* TABS */
|
||||
.tabs-wrap{display:flex;justify-content:center;padding:32px 24px 0}
|
||||
.tabs{display:flex;gap:4px;padding:5px;background:var(--card);border:1px solid var(--border);border-radius:14px}
|
||||
.tab{padding:10px 24px;border:none;background:transparent;color:var(--sec);font-family:var(--mono);font-size:12px;font-weight:500;border-radius:10px;cursor:pointer;transition:all .25s var(--ease);white-space:nowrap;display:flex;align-items:center;gap:6px}
|
||||
.tab:hover{color:var(--text)}
|
||||
.tab.active{background:var(--gd);color:var(--g);box-shadow:0 0 0 1px rgba(34,197,94,.2)}
|
||||
|
||||
/* BUSINESS SWITCHER */
|
||||
.bswitch-wrap{display:flex;justify-content:center;padding:16px 24px 0}
|
||||
.bswitch{display:flex;gap:4px;padding:4px;background:var(--card);border:1px solid var(--border);border-radius:12px}
|
||||
.bbtn{padding:8px 16px;border:none;background:transparent;color:var(--sec);font-family:var(--mono);font-size:11px;font-weight:500;border-radius:8px;cursor:pointer;transition:all .25s var(--ease);white-space:nowrap}
|
||||
.bbtn:hover{color:var(--text)}
|
||||
.bbtn.active{background:var(--bd);color:var(--b);box-shadow:0 0 0 1px rgba(59,130,246,.2)}
|
||||
|
||||
/* SECTION */
|
||||
.section{display:none;padding:20px 24px;max-width:1280px;margin:0 auto;animation:fadeIn .4s ease}
|
||||
.section.active{display:block}
|
||||
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}
|
||||
.s-head{text-align:center;margin-bottom:20px}
|
||||
.s-head h2{font-size:clamp(1.4rem,3vw,1.8rem);font-weight:800;letter-spacing:-.5px;margin-bottom:4px}
|
||||
.s-head p{color:var(--sec);font-size:13px}
|
||||
|
||||
/* SUMMARY CARDS */
|
||||
.sum-row{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:16px}
|
||||
.sc{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:16px;position:relative;overflow:hidden}
|
||||
.sc:hover{border-color:var(--border-h)}
|
||||
.sc-accent{position:absolute;top:0;left:0;width:3px;height:100%;opacity:0;transition:opacity .25s}
|
||||
.sc:hover .sc-accent{opacity:1}
|
||||
.sc-label{font-size:10px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);font-weight:600;margin-bottom:6px}
|
||||
.sc-val{font-family:var(--mono);font-size:24px;font-weight:700;margin-bottom:4px;line-height:1.1}
|
||||
.sc-trend{display:inline-flex;align-items:center;gap:3px;padding:2px 6px;border-radius:5px;font-family:var(--mono);font-weight:600;font-size:10px}
|
||||
.sc-trend.up{color:var(--g);background:var(--gd)}
|
||||
.sc-trend.down{color:var(--r);background:var(--rd)}
|
||||
.sc-note{font-size:10px;color:var(--muted);margin-top:4px}
|
||||
|
||||
/* CHART ROWS */
|
||||
.cr{display:grid;gap:12px;margin-bottom:12px}
|
||||
.cr2{grid-template-columns:2fr 1fr}
|
||||
.cr3{grid-template-columns:1fr 1fr 1fr}
|
||||
.cc{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:20px;transition:border-color .25s}
|
||||
.cc:hover{border-color:var(--border-h)}
|
||||
.ch{margin-bottom:4px}
|
||||
.ct{font-size:13px;font-weight:600;color:var(--text)}
|
||||
.cs{font-size:11px;color:var(--muted)}
|
||||
.cd{width:100%;height:260px}
|
||||
.cd.s{height:200px}
|
||||
|
||||
/* DATA TABLE */
|
||||
.dt{width:100%;border-collapse:collapse;font-size:12px}
|
||||
.dt th{text-align:left;padding:8px 10px;color:var(--muted);font-size:10px;text-transform:uppercase;letter-spacing:.06em;font-weight:600;border-bottom:1px solid var(--border)}
|
||||
.dt td{padding:8px 10px;border-bottom:1px solid var(--border);color:var(--sec);font-family:var(--mono);font-size:11px}
|
||||
.dt td:first-child{color:var(--text);font-family:var(--mono)}
|
||||
.dt tr:hover td{background:rgba(255,255,255,.02)}
|
||||
.badge{display:inline-block;padding:2px 6px;border-radius:4px;font-size:10px;font-weight:600;font-family:var(--mono)}
|
||||
.badge.g{color:var(--g);background:var(--gd)}
|
||||
.badge.b{color:var(--b);background:var(--bd)}
|
||||
.badge.a{color:var(--a);background:var(--ad)}
|
||||
.badge.r{color:var(--r);background:var(--rd)}
|
||||
|
||||
@media(max-width:1024px){.sum-row{grid-template-columns:repeat(2,1fr)}.cr2,.cr3{grid-template-columns:1fr}}
|
||||
@media(max-width:640px){.sum-row{grid-template-columns:1fr}.tab{padding:8px 12px;font-size:11px}.tab .lbl{display:none}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="hdr">
|
||||
<div class="logo"><span class="dot"></span>ROI24</div>
|
||||
<div class="hdr-right">
|
||||
<div class="live"><span class="dot"></span>Live</div>
|
||||
<div class="clock" id="clock"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tabs-wrap">
|
||||
<div class="tabs" id="tabs">
|
||||
<button class="tab active" data-t="cpa">💰 CPA</button>
|
||||
<button class="tab" data-t="conv">📊 Конверсия</button>
|
||||
<button class="tab" data-t="ltv">📈 LTV</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bswitch-wrap">
|
||||
<div class="bswitch" id="bswitch">
|
||||
<button class="bbtn active" data-b="e-com">Интернет-магазин</button>
|
||||
<button class="bbtn" data-b="services">Услуги</button>
|
||||
<button class="bbtn" data-b="production">Производство</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="dashboard">
|
||||
|
||||
<!-- ===================== CPA SECTION ===================== -->
|
||||
<div class="section active" id="sec-cpa">
|
||||
<div class="s-head"><h2>Стоимость за продажу (CPA)</h2><p>Сколько стоит привлечение платящего клиента</p></div>
|
||||
|
||||
<div class="sum-row" id="cpa-sum"></div>
|
||||
|
||||
<div class="cr cr2">
|
||||
<div class="cc">
|
||||
<div class="ch"><div class="ct" id="cpa-main-t"></div><div class="cs" id="cpa-main-s"></div></div>
|
||||
<div class="cd" id="cpa-main"></div>
|
||||
</div>
|
||||
<div class="cc">
|
||||
<div class="ch"><div class="ct">Стоимость канала</div><div class="cs">Расход / количество продаж</div></div>
|
||||
<div class="cd s" id="cpa-side"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cc" style="margin-bottom:12px">
|
||||
<div class="ch"><div class="ct">Раскрытие: ключевые запросы</div><div class="cs">Нажмите на канал для детализации</div></div>
|
||||
<div id="cpa-table-wrap" style="max-height:350px;overflow:auto;margin-top:12px"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================== CONV SECTION ===================== -->
|
||||
<div class="section" id="sec-conv">
|
||||
<div class="s-head"><h2>Конверсия в продажу</h2><p>Какой трафик и какие менеджеры конвертят лучше</p></div>
|
||||
|
||||
<div class="sum-row" id="conv-sum"></div>
|
||||
|
||||
<div class="cr cr2">
|
||||
<div class="cc">
|
||||
<div class="ch"><div class="ct" id="cv-main-t"></div><div class="cs" id="cv-main-s"></div></div>
|
||||
<div class="cd" id="cv-main"></div>
|
||||
</div>
|
||||
<div class="cc">
|
||||
<div class="ch"><div class="ct">Конверсия по каналам</div><div class="cs">Заявка → Продажа</div></div>
|
||||
<div class="cd s" id="cv-side"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cr cr3">
|
||||
<div class="cc">
|
||||
<div class="ch"><div class="ct">Конверсия по ключевым запросам</div><div class="cs">Кликабельный список</div></div>
|
||||
<div id="cv-keywords" style="max-height:250px;overflow:auto;margin-top:12px"></div>
|
||||
</div>
|
||||
<div class="cc">
|
||||
<div class="ch"><div class="ct">Конверсия по менеджерам</div><div class="cs">Обработка заявок</div></div>
|
||||
<div id="cv-managers" style="max-height:250px;overflow:auto;margin-top:12px"></div>
|
||||
</div>
|
||||
<div class="cc">
|
||||
<div class="ch"><div class="ct">Воронка продаж</div><div class="cs">Этапы: трафик → заявка → продажа</div></div>
|
||||
<div class="cd s" id="cv-funnel"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===================== LTV SECTION ===================== -->
|
||||
<div class="section" id="sec-ltv">
|
||||
<div class="s-head"><h2>LTV клиента</h2><p>Сколько клиент платит за всё время и как растёт</p></div>
|
||||
|
||||
<div class="sum-row" id="ltv-sum"></div>
|
||||
|
||||
<div class="cr cr2">
|
||||
<div class="cc">
|
||||
<div class="ch"><div class="ct" id="ltv-main-t"></div><div class="cs" id="ltv-main-s"></div></div>
|
||||
<div class="cd" id="ltv-main"></div>
|
||||
</div>
|
||||
<div class="cc">
|
||||
<div class="ch"><div class="ct">LTV по каналам</div><div class="cs">Кумулятивный доход от клиента</div></div>
|
||||
<div class="cd s" id="ltv-side"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cr cr2">
|
||||
<div class="cc">
|
||||
<div class="ch"><div class="ct">Кривая удержания</div><div class="cs">Какой % клиентов остаётся</div></div>
|
||||
<div class="cd s" id="ltv-retention"></div>
|
||||
</div>
|
||||
<div class="cc">
|
||||
<div class="ch"><div class="ct">Средний чек по каналам</div><div class="cs">Сколько платит клиент за 1 заказ</div></div>
|
||||
<div class="cd s" id="ltv-check"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
|
||||
<script>
|
||||
/* ============================================
|
||||
DATA — Attribution Dashboard
|
||||
CPA / Conversion / LTV по каналам и ключам
|
||||
============================================ */
|
||||
|
||||
const COLORS = ['#22c55e','#3b82f6','#f59e0b','#a855f7','#ef4444','#06b6d4'];
|
||||
|
||||
const DATA = {
|
||||
/* ==================== E-COM ==================== */
|
||||
'e-com': {
|
||||
/* CPA */
|
||||
cpa: {
|
||||
avg: {v:'890 ₽',d:'+5%',t:'up',n:'средняя стоимость за продажу'},
|
||||
total: {v:'1 247',d:'+12%',t:'up',n:'продаж за 30 дней'},
|
||||
spend: {v:'1.1М ₽',d:'+8%',t:'up',n:'расход на рекламу'},
|
||||
ratio: {v:'62%',d:'−3%',t:'down',n:'CPA / средний чек'},
|
||||
channels: [
|
||||
{n:'Яндекс Директ',spend:520000,conv:120,cpa:4333,avg:863},
|
||||
{n:'VK Реклама',spend:280000,conv:65,cpa:4308,avg:862},
|
||||
{n:'SEO',spend:150000,conv:35,cpa:4286,avg:857},
|
||||
{n:'Ретаргетинг',spend:120000,conv:15,cpa:8000,avg:800},
|
||||
],
|
||||
keywords: {
|
||||
'Яндекс Директ': [
|
||||
{q:'купить [товар] москва', clicks:3200, conv:85, cpa:3200, cost_per_click:45},
|
||||
{q:'[товар] цена', clicks:5100, conv:110, cpa:3800, cost_per_click:38},
|
||||
{q:'[товар] недорого', clicks:7800, conv:95, cpa:5100, cost_per_click:42},
|
||||
{q:'[товар] отзывы', clicks:2100, conv:62, cpa:2900, cost_per_click:55},
|
||||
{q:'[бренд] купить', clicks:1800, conv:78, cpa:2200, cost_per_click:48},
|
||||
{q:'[товар] дешево', clicks:9200, conv:42, cpa:6800, cost_per_click:35},
|
||||
],
|
||||
'VK Реклама': [
|
||||
{q:'пост о [товаре]', clicks:4100, conv:38, cpa:6200, cost_per_click:42},
|
||||
{q:'таргет [товар]', clicks:2800, conv:22, cpa:7500, cost_per_click:52},
|
||||
{q:'реклама [товар]', clicks:1900, conv:18, cpa:8100, cost_per_click:48},
|
||||
{q:'подбор [товара]', clicks:3200, conv:28, cpa:5800, cost_per_click:38},
|
||||
],
|
||||
'SEO': [
|
||||
{q:'[товар] обзор', clicks:2800, conv:42, cpa:2800, cost_per_click:32},
|
||||
{q:'[товар] сравнение', clicks:1500, conv:35, cpa:3100, cost_per_click:38},
|
||||
{q:'лучший [товар]', clicks:1900, conv:38, cpa:2900, cost_per_click:35},
|
||||
],
|
||||
},
|
||||
trend: {labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'], cpa:[1200,1100,980,920,890,870,850,830,810,790,780,760], spend:[350,380,420,450,480,520,490,510,470,460,500,550]},
|
||||
},
|
||||
|
||||
/* Conv */
|
||||
conv: {
|
||||
avg: {v:'2.8%',d:'+0.3%',t:'up',n:'заявка → продажа'},
|
||||
leads: {v:'44 500',d:'+15%',t:'up',n:'лидов за 30 дней'},
|
||||
sales: {v:'1 247',d:'+12%',t:'up',n:'продаж за 30 дней'},
|
||||
cycle: {v:'4 дн',d:'−1дн',t:'down',n:'средний цикл'},
|
||||
channels: [
|
||||
{n:'Яндекс Директ',leads:28500,conv:260,cpv:0.91},
|
||||
{n:'VK Реклама',leads:8200,conv:95,cpv:1.16},
|
||||
{n:'SEO',leads:5800,conv:180,cpv:3.10},
|
||||
{n:'Ретаргетинг',leads:2000,conv:220,cpv:11.0},
|
||||
],
|
||||
keywords: [
|
||||
{q:'[бренд] купить', leads:4200, sales:380, cv:9.0,c:'var(--g)'},
|
||||
{q:'[товар] отзывы', leads:2100, sales:190, cv:9.0,c:'var(--g)'},
|
||||
{q:'купить [товар] москва', leads:3200, sales:260, cv:8.1,c:'var(--g)'},
|
||||
{q:'[товар] цена', leads:5100, sales:380, cv:7.5,c:'var(--g)'},
|
||||
{q:'[товар] обзор', leads:2800, sales:180, cv:6.4,c:'var(--b)'},
|
||||
{q:'[товар] сравнение', leads:1500, sales:85, cv:5.7,c:'var(--b)'},
|
||||
{q:'[товар] недорого', leads:7800, sales:380, cv:4.9,c:'var(--a)'},
|
||||
{q:'[товар] дешево', leads:9200, sales:310, cv:3.4,c:'var(--r)'},
|
||||
],
|
||||
managers: [
|
||||
{n:'Козлов А.', leads:820, sales:260, cv:31.7,c:'var(--g)'},
|
||||
{n:'Иванова М.', leads:940, sales:280, cv:29.8,c:'var(--g)'},
|
||||
{n:'Петров Д.', leads:780, sales:210, cv:26.9,c:'var(--b)'},
|
||||
{n:'Сидорова Е.', leads:690, sales:170, cv:24.6,c:'var(--a)'},
|
||||
{n:'Новиков К.', leads:1100, sales:250, cv:22.7,c:'var(--a)'},
|
||||
{n:'Волкова Т.', leads:620, sales:120, cv:19.4,c:'var(--r)'},
|
||||
],
|
||||
funnel: [
|
||||
{n:'Посетители',v:45000},
|
||||
{n:'Оформление',v:8100},
|
||||
{n:'Корзина',v:4950},
|
||||
{n:'Оплата',v:2723},
|
||||
],
|
||||
trend: {labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'], data:[2.1,2.2,2.3,2.4,2.5,2.6,2.7,2.7,2.8,2.8,2.9,2.8]},
|
||||
},
|
||||
|
||||
/* LTV */
|
||||
ltv: {
|
||||
avg: {v:'12 400 ₽',d:'+18%',t:'up',n:'за 6 мес'},
|
||||
repeat: {v:'3.2×',d:'+0.3',t:'up',n:'ср. покупок'},
|
||||
retention: {v:'68%',d:'+5%',t:'up',n:'через 3 мес'},
|
||||
avgCheck: {v:'1 850 ₽',d:'−2%',t:'down',n:'за 1 заказ'},
|
||||
channels: [
|
||||
{n:'SEO',ltv:18200,checks:7,retention:78},
|
||||
{n:'Ретаргетинг',ltv:15800,checks:5,retention:72},
|
||||
{n:'Яндекс Директ',ltv:12400,checks:4,retention:68},
|
||||
{n:'VK Реклама',ltv:9800,checks:3,retention:58},
|
||||
],
|
||||
curve: {
|
||||
labels:['М1','М2','М3','М4','М5','М6','М7','М8','М9','М10','М11','М12'],
|
||||
seo:[180,380,620,900,1200,1500,1800,2100,2400,2700,3000,3300],
|
||||
direct:[160,320,520,780,1050,1350,1650,1900,2200,2500,2800,3100],
|
||||
vk:[140,260,420,620,850,1100,1350,1600,1850,2100,2400,2700],
|
||||
},
|
||||
retention: {
|
||||
labels:['М1','М2','М3','М4','М5','М6','М7','М8','М9','М10','М11','М12'],
|
||||
seo:[100,78,65,56,50,45,40,36,33,30,28,26],
|
||||
direct:[100,72,58,48,42,37,33,30,28,25,23,22],
|
||||
vk:[100,65,50,38,30,24,20,17,15,13,12,11],
|
||||
},
|
||||
checks: [
|
||||
{n:'Яндекс Директ',avg:1600,c:'var(--b)'},
|
||||
{n:'SEO',avg:1850,c:'var(--g)'},
|
||||
{n:'VK Реклама',avg:1350,c:'var(--a)'},
|
||||
{n:'Ретаргетинг',avg:2100,c:'var(--p)'},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
/* ==================== SERVICES ==================== */
|
||||
'services': {
|
||||
cpa: {
|
||||
avg: {v:'3 200 ₽',d:'+2%',t:'up',n:'средняя стоимость за продажу'},
|
||||
total: {v:'86',d:'+8%',t:'up',n:'продаж за 30 дней'},
|
||||
spend: {v:'275К ₽',d:'+5%',t:'up',n:'расход на рекламу'},
|
||||
ratio: {v:'18%',d:'−2%',t:'down',n:'CPA / средний чек'},
|
||||
channels: [
|
||||
{n:'Яндекс Директ',spend:140000,conv:42,cpa:3333,avg:18476},
|
||||
{n:'VK Реклама',spend:75000,conv:22,cpa:3409,avg:18409},
|
||||
{n:'SEO',spend:60000,conv:22,cpa:2727,avg:18182},
|
||||
],
|
||||
keywords: {
|
||||
'Яндекс Директ': [
|
||||
{q:'создать сайт цена', clicks:1800, conv:42, cpa:2800, cost_per_click:52},
|
||||
{q:'заказать лендинг', clicks:2200, conv:38, cpa:3100, cost_per_click:48},
|
||||
{q:'разработка сайта', clicks:3100, conv:28, cpa:4200, cost_per_click:55},
|
||||
{q:'сайт под ключ', clicks:1400, conv:18, cpa:5200, cost_per_click:62},
|
||||
],
|
||||
'VK Реклама': [
|
||||
{q:'лендинг дизайн', clicks:1200, conv:12, cpa:5500, cost_per_click:58},
|
||||
{q:'сайт для бизнеса', clicks:900, conv:8, cpa:7200, cost_per_click:65},
|
||||
],
|
||||
'SEO': [
|
||||
{q:'создание сайтов москва', clicks:2400, conv:22, cpa:2400, cost_per_click:28},
|
||||
],
|
||||
},
|
||||
trend: {labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'], cpa:[3800,3600,3500,3400,3350,3200,3100,3000,2900,2850,2800,2750], spend:[80,85,90,100,105,110,108,115,112,118,120,125]},
|
||||
},
|
||||
conv: {
|
||||
avg: {v:'5.2%',d:'+0.4%',t:'up',n:'заявка → продажа'},
|
||||
leads: {v:'1 650',d:'+10%',t:'up',n:'лидов за 30 дней'},
|
||||
sales: {v:'86',d:'+8%',t:'up',n:'продаж за 30 дней'},
|
||||
cycle: {v:'12 дн',d:'−2дн',t:'down',n:'средний цикл'},
|
||||
channels: [
|
||||
{n:'Яндекс Директ',leads:820,conv:48,cpv:5.85},
|
||||
{n:'VK Реклама',leads:380,conv:22,cpv:5.79},
|
||||
{n:'SEO',leads:450,conv:16,cpv:3.56},
|
||||
],
|
||||
keywords: [
|
||||
{q:'создать сайт цена', leads:1800, sales:92, cv:5.1,c:'var(--g)'},
|
||||
{q:'заказать лендинг', leads:2200, sales:105, cv:4.8,c:'var(--g)'},
|
||||
{q:'разработка сайта', leads:3100, sales:120, cv:3.9,c:'var(--b)'},
|
||||
{q:'сайт под ключ', leads:1400, sales:48, cv:3.4,c:'var(--a)'},
|
||||
],
|
||||
managers: [
|
||||
{n:'Смирнов О.', leads:420, sales:32, cv:7.6,c:'var(--g)'},
|
||||
{n:'Кузнецова Н.', leads:380, sales:28, cv:7.4,c:'var(--g)'},
|
||||
{n:'Фёдоров В.', leads:480, sales:30, cv:6.3,c:'var(--b)'},
|
||||
{n:'Морозова И.', leads:370, sales:18, cv:4.9,c:'var(--a)'},
|
||||
],
|
||||
funnel: [
|
||||
{n:'Посетители',v:3200},
|
||||
{n:'Заявки',v:166},
|
||||
{n:'Контракт',v:86},
|
||||
],
|
||||
trend: {labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'], data:[3.8,4.0,4.2,4.5,4.7,4.8,4.9,5.0,5.1,5.1,5.2,5.2]},
|
||||
},
|
||||
ltv: {
|
||||
avg: {v:'89 000 ₽',d:'+22%',t:'up',n:'за 12 мес'},
|
||||
repeat: {v:'1.4×',d:'+0.1',t:'up',n:'повторных заказов'},
|
||||
retention: {v:'72%',d:'+4%',t:'up',n:'через 6 мес'},
|
||||
avgCheck: {v:'63 500 ₽',d:'+5%',t:'up',n:'за 1 заказ'},
|
||||
channels: [
|
||||
{n:'SEO',ltv:125000,checks:2.1,retention:82},
|
||||
{n:'Яндекс Директ',ltv:89000,checks:1.4,retention:72},
|
||||
{n:'VK Реклама',ltv:58000,checks:1.1,retention:58},
|
||||
],
|
||||
curve: {
|
||||
labels:['М1','М2','М3','М4','М5','М6','М7','М8','М9','М10','М11','М12'],
|
||||
seo:[12000,24000,38000,52000,68000,82000,96000,110000,120000,128000,135000,142000],
|
||||
direct:[10000,18000,28000,40000,55000,68000,80000,92000,100000,108000,115000,120000],
|
||||
vk:[7000,12000,18000,26000,35000,45000,54000,62000,70000,78000,84000,90000],
|
||||
},
|
||||
retention: {
|
||||
labels:['М1','М2','М3','М4','М5','М6','М7','М8','М9','М10','М11','М12'],
|
||||
seo:[100,85,78,72,68,65,62,60,58,56,55,54],
|
||||
direct:[100,78,65,58,52,48,44,42,40,38,37,36],
|
||||
vk:[100,68,52,42,35,30,26,23,21,19,18,17],
|
||||
},
|
||||
checks: [
|
||||
{n:'Яндекс Директ',avg:63500,c:'var(--b)'},
|
||||
{n:'SEO',avg:67500,c:'var(--g)'},
|
||||
{n:'VK Реклама',avg:48000,c:'var(--a)'},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
/* ==================== PRODUCTION ==================== */
|
||||
'production': {
|
||||
cpa: {
|
||||
avg: {v:'12 500 ₽',d:'−8%',t:'down',n:'средняя стоимость за заказ'},
|
||||
total: {v:'340',d:'+6%',t:'up',n:'заказов за 30 дней'},
|
||||
spend: {v:'4.2М ₽',d:'+4%',t:'up',n:'расход на рекламу'},
|
||||
ratio: {v:'5.2%',d:'−1%',t:'down',n:'CPA / средний чек'},
|
||||
channels: [
|
||||
{n:'Яндекс Директ',spend:2100000,conv:150,cpa:14000,avg:270000},
|
||||
{n:'VK Реклама',spend:980000,conv:100,cpa:9800,avg:245000},
|
||||
{n:'SEO',spend:520000,conv:50,cpa:10400,avg:260000},
|
||||
{n:'Выставки',spend:600000,conv:40,cpa:15000,avg:300000},
|
||||
],
|
||||
keywords: {
|
||||
'Яндекс Директ': [
|
||||
{q:'купить [оборудование] оптом', clicks:2800, conv:55, cpa:11200, cost_per_click:48},
|
||||
{q:'производство [оборудования]', clicks:3500, conv:38, cpa:15200, cost_per_click:52},
|
||||
{q:'[оборудование] цена опт', clicks:4200, conv:32, cpa:17800, cost_per_click:58},
|
||||
{q:'поставщик [оборудования]', clicks:1900, conv:28, cpa:13500, cost_per_click:55},
|
||||
{q:'[оборудование] каталог', clicks:2200, conv:22, cpa:18500, cost_per_click:45},
|
||||
],
|
||||
'VK Реклама': [
|
||||
{q:'партнёр [оборудование]', clicks:1800, conv:22, cpa:12500, cost_per_click:42},
|
||||
{q:'опт [оборудование]', clicks:1400, conv:18, cpa:14200, cost_per_click:48},
|
||||
],
|
||||
'SEO': [
|
||||
{q:'производитель [оборудования]', clicks:1600, conv:32, cpa:9800, cost_per_click:32},
|
||||
],
|
||||
},
|
||||
trend: {labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'], cpa:[18000,16500,15200,14000,13500,12800,12200,11800,11500,11200,11000,10800], spend:[280,320,350,380,400,420,410,430,420,440,460,480]},
|
||||
},
|
||||
conv: {
|
||||
avg: {v:'2.1%',d:'+0.2%',t:'up',n:'заявка → заказ'},
|
||||
leads: {v:'16 200',d:'+8%',t:'up',n:'заявок за 30 дней'},
|
||||
sales: {v:'340',d:'+6%',t:'up',n:'заказов за 30 дней'},
|
||||
cycle: {v:'18 дн',d:'−3дн',t:'down',n:'средний цикл'},
|
||||
channels: [
|
||||
{n:'Яндекс Директ',leads:8200,conv:150,cpv:1.83},
|
||||
{n:'VK Реклама',leads:4800,conv:100,cpv:2.08},
|
||||
{n:'SEO',leads:2400,conv:50,cpv:2.08},
|
||||
{n:'Выставки',leads:800,conv:40,cpv:5.0},
|
||||
],
|
||||
keywords: [
|
||||
{q:'[оборудование] каталог', leads:2200, sales:55, cv:2.5,c:'var(--g)'},
|
||||
{q:'купить [оборудование] опт', leads:2800, sales:62, cv:2.2,c:'var(--g)'},
|
||||
{q:'производство [оборудования]', leads:3500, sales:72, cv:2.1,c:'var(--b)'},
|
||||
{q:'поставщик [оборудования]', leads:1900, sales:38, cv:2.0,c:'var(--b)'},
|
||||
{q:'[оборудование] цена опт', leads:4200, sales:75, cv:1.8,c:'var(--a)'},
|
||||
{q:'[оборудование] каталог', leads:2200, sales:32, cv:1.5,c:'var(--r)'},
|
||||
],
|
||||
managers: [
|
||||
{n:'Борисов Р.', leads:4200, sales:110, cv:2.6,c:'var(--g)'},
|
||||
{n:'Тимофеева А.', leads:3800, sales:95, cv:2.5,c:'var(--g)'},
|
||||
{n:'Орлов С.', leads:4500, sales:98, cv:2.2,c:'var(--b)'},
|
||||
{n:'Лебедева О.', leads:2800, sales:55, cv:2.0,c:'var(--a)'},
|
||||
{n:'Егоров П.', leads:2200, sales:42, cv:1.9,c:'var(--a)'},
|
||||
{n:'Соколова Н.', leads:1800, sales:28, cv:1.6,c:'var(--r)'},
|
||||
],
|
||||
funnel: [
|
||||
{n:'Посетители',v:85000},
|
||||
{n:'Заявки',v:1785},
|
||||
{n:'Контракт',v:340},
|
||||
],
|
||||
trend: {labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'], data:[1.4,1.5,1.6,1.7,1.8,1.8,1.9,1.9,2.0,2.0,2.1,2.1]},
|
||||
},
|
||||
ltv: {
|
||||
avg: {v:'1 250 000 ₽',d:'+15%',t:'up',n:'за 12 мес'},
|
||||
repeat: {v:'2.3×',d:'+0.2',t:'up',n:'повторных заказов'},
|
||||
retention: {v:'55%',d:'+3%',t:'up',n:'через 6 мес'},
|
||||
avgCheck: {v:'543 000 ₽',d:'+4%',t:'up',n:'за 1 заказ'},
|
||||
channels: [
|
||||
{n:'SEO',ltv:1850000,checks:3.5,retention:68},
|
||||
{n:'Выставки',ltv:1420000,checks:2.8,retention:60},
|
||||
{n:'Яндекс Директ',ltv:1100000,checks:2.0,retention:52},
|
||||
{n:'VK Реклама',ltv:780000,checks:1.4,retention:38},
|
||||
],
|
||||
curve: {
|
||||
labels:['М1','М2','М3','М4','М5','М6','М7','М8','М9','М10','М11','М12'],
|
||||
seo:[180000,380000,580000,780000,980000,1180000,1380000,1580000,1750000,1900000,2050000,2200000],
|
||||
direct:[160000,320000,480000,640000,820000,1000000,1180000,1350000,1500000,1650000,1800000,1950000],
|
||||
vk:[100000,200000,320000,450000,600000,750000,900000,1050000,1200000,1350000,1500000,1650000],
|
||||
},
|
||||
retention: {
|
||||
labels:['М1','М2','М3','М4','М5','М6','М7','М8','М9','М10','М11','М12'],
|
||||
seo:[100,82,72,65,60,56,53,51,50,49,48,48],
|
||||
direct:[100,70,55,45,38,34,30,28,26,25,24,24],
|
||||
vk:[100,60,42,30,22,16,12,10,9,8,7,7],
|
||||
},
|
||||
checks: [
|
||||
{n:'Яндекс Директ',avg:543000,c:'var(--b)'},
|
||||
{n:'SEO',avg:528000,c:'var(--g)'},
|
||||
{n:'VK Реклама',avg:420000,c:'var(--a)'},
|
||||
{n:'Выставки',avg:620000,c:'var(--p)'},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let charts = {};
|
||||
let curSeg = 'e-com', curTab = 'cpa';
|
||||
|
||||
/* ============================================
|
||||
HELPERS
|
||||
============================================ */
|
||||
function sumRow(id, items) {
|
||||
const el = document.getElementById(id);
|
||||
el.innerHTML = items.map((s,i) => `
|
||||
<div class="sc">
|
||||
<div class="sc-accent" style="background:${['var(--g)','var(--b)','var(--p)','var(--a)'][i%4]}"></div>
|
||||
<div class="sc-label">${s.l}</div>
|
||||
<div class="sc-val">${s.v}</div>
|
||||
<span class="sc-trend ${s.t}">${s.t==='up'?'↑':s.t==='down'?'↓':'→'} ${s.d}</span>
|
||||
<div class="sc-note">${s.n}</div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
function makeLineChart(domId, title, opt, sub) {
|
||||
const dom = document.getElementById(domId);
|
||||
if (!charts[domId]) charts[domId] = echarts.init(dom);
|
||||
if (title) document.getElementById(title)?.querySelector('.ct') && (document.getElementById(title).querySelector('.ct').textContent = title);
|
||||
charts[domId].setOption(opt, true);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
CPA RENDER
|
||||
============================================ */
|
||||
function renderCPA(d) {
|
||||
const c = d.cpa;
|
||||
|
||||
// Summary
|
||||
sumRow('cpa-sum', [
|
||||
{l:'CPA средний',v:c.avg.v,d:c.avg.d,t:c.avg.t,n:c.avg.n},
|
||||
{l:'Продажи',v:c.total.v,d:c.total.d,t:c.total.t,n:c.total.n},
|
||||
{l:'Расход',v:c.spend.v,d:c.spend.d,t:c.spend.d,t:c.spend.t,n:c.spend.n},
|
||||
{l:'CPA / чек',v:c.ratio.v,d:c.ratio.d,t:c.ratio.t,n:c.ratio.n},
|
||||
]);
|
||||
|
||||
// Main: CPA trend
|
||||
const mc = document.getElementById('cpa-main');
|
||||
if (!charts.cpaMain) charts.cpaMain = echarts.init(mc);
|
||||
document.getElementById('cpa-main-t').textContent = c.trend.labels.map((_,i)=>{
|
||||
const months=['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'];
|
||||
return '';
|
||||
}).join('');
|
||||
document.getElementById('cpa-main-t').textContent = 'CPA и расход на рекламу';
|
||||
document.getElementById('cpa-main-s').textContent = 'Тренд за 12 месяцев';
|
||||
|
||||
charts.cpaMain.setOption({
|
||||
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
|
||||
legend:{data:['CPA','Расход (×100)'],textStyle:{color:'#52525b',fontSize:10},top:0},
|
||||
grid:{left:44,right:44,top:28,bottom:28},
|
||||
xAxis:{type:'category',data:c.trend.labels,axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#52525b',fontSize:10}},
|
||||
yAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:10,formatter:'{value}К'}},
|
||||
series:[
|
||||
{name:'CPA',type:'line',data:c.trend.cpa.map(v=>v*100/100),smooth:.4,symbol:'none',lineStyle:{width:2.5,color:'#22c55e'},
|
||||
yAxisIndex:0,
|
||||
areaStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:'rgba(34,197,94,.2)'},{offset:1,color:'rgba(34,197,94,.01)'}])},
|
||||
},
|
||||
{name:'Расход',type:'bar',data:c.trend.spend,barWidth:'35%',itemStyle:{color:'rgba(59,130,246,.2)',borderRadius:[4,4,0,0]}},
|
||||
],
|
||||
animationDuration:600,
|
||||
},true);
|
||||
|
||||
// Side: Channel CPA
|
||||
const sc = document.getElementById('cpa-side');
|
||||
if (!charts.cpaSide) charts.cpaSide = echarts.init(sc);
|
||||
const chs = c.channels.sort((a,b)=>a.cpa-b.cpa);
|
||||
charts.cpaSide.setOption({
|
||||
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
|
||||
grid:{left:80,right:30,top:10,bottom:10},
|
||||
xAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:9,formatter:'{value}₽'}},
|
||||
yAxis:{type:'category',data:chs.map(x=>x.n).reverse(),axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#a1a1aa',fontSize:10}},
|
||||
series:[{
|
||||
type:'bar',data:chs.map(x=>x.cpa).reverse(),barWidth:38,
|
||||
itemStyle:{borderRadius:[0,5,5,0],color:p=>{const c=COLORS[p.dataIndex%COLORS.length];return new echarts.graphic.LinearGradient(0,0,1,0,[{offset:0,color:c+'88'},{offset:1,color:c}])}}
|
||||
}],
|
||||
animationDuration:600,
|
||||
},true);
|
||||
|
||||
// Table: Keywords
|
||||
renderKeywordTable(c.keywords);
|
||||
}
|
||||
|
||||
function renderKeywordTable(keywords) {
|
||||
const wrap = document.getElementById('cpa-table-wrap');
|
||||
let html = '<table class="dt"><thead><tr><th>Канал</th><th>Ключевой запрос</th><th>Клики</th><th>Продажи</th><th>CPA</th><th>Цена клика</th></tr></thead><tbody>';
|
||||
|
||||
for (const [channel, kws] of Object.entries(keywords)) {
|
||||
kws.forEach(kw => {
|
||||
const badge = kw.cpa < 3000 ? 'g' : kw.cpa < 5000 ? 'b' : kw.cpa < 6000 ? 'a' : 'r';
|
||||
html += `<tr>
|
||||
<td><span class="badge ${badge}">${channel}</span></td>
|
||||
<td style="color:var(--text);font-family:var(--mono)">${kw.q}</td>
|
||||
<td>${kw.clicks.toLocaleString('ru')}</td>
|
||||
<td>${kw.conv}</td>
|
||||
<td>${kw.cpa.toLocaleString('ru')} ₽</td>
|
||||
<td>${kw.cost_per_click} ₽</td>
|
||||
</tr>`;
|
||||
});
|
||||
}
|
||||
html += '</tbody></table>';
|
||||
wrap.innerHTML = html;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
CONV RENDER
|
||||
============================================ */
|
||||
function renderConv(d) {
|
||||
const c = d.conv;
|
||||
|
||||
sumRow('conv-sum', [
|
||||
{l:'Конверсия',v:c.avg.v,d:c.avg.d,t:c.avg.t,n:c.avg.n},
|
||||
{l:'Лиды',v:c.leads.v,d:c.leads.d,t:c.leads.t,n:c.leads.n},
|
||||
{l:'Продажи',v:c.sales.v,d:c.sales.d,t:c.sales.t,n:c.sales.n},
|
||||
{l:'Цикл',v:c.cycle.v,d:c.cycle.d,t:c.cycle.t,n:c.cycle.n},
|
||||
]);
|
||||
|
||||
// Main: Conv trend
|
||||
const mc = document.getElementById('cv-main');
|
||||
if (!charts.convMain) charts.convMain = echarts.init(mc);
|
||||
document.getElementById('cv-main-t').textContent = 'Конверсия в продажу';
|
||||
document.getElementById('cv-main-s').textContent = 'Тренд за 12 месяцев';
|
||||
charts.convMain.setOption({
|
||||
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
|
||||
grid:{left:44,right:16,top:28,bottom:28},
|
||||
xAxis:{type:'category',data:c.trend.labels,axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#52525b',fontSize:10}},
|
||||
yAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:10,formatter:'{value}%'}},
|
||||
series:[{
|
||||
type:'line',data:c.trend.data,smooth:.4,symbol:'none',lineStyle:{width:2.5,color:'#22c55e'},
|
||||
areaStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:'rgba(34,197,94,.2)'},{offset:1,color:'rgba(34,197,94,.01)'}])},
|
||||
}],
|
||||
animationDuration:600,
|
||||
},true);
|
||||
|
||||
// Side: Conv by channel
|
||||
const sc = document.getElementById('cv-side');
|
||||
if (!charts.convSide) charts.convSide = echarts.init(sc);
|
||||
const chs = c.channels.sort((a,b)=>a.conv-b.conv);
|
||||
charts.convSide.setOption({
|
||||
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
|
||||
grid:{left:80,right:30,top:10,bottom:10},
|
||||
xAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:9,formatter:'{value}%'}},
|
||||
yAxis:{type:'category',data:chs.map(x=>x.n).reverse(),axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#a1a1aa',fontSize:10}},
|
||||
series:[{
|
||||
type:'bar',data:chs.map(x=>x.conv).reverse(),barWidth:38,
|
||||
itemStyle:{borderRadius:[0,5,5,0],color:p=>{const c=COLORS[p.dataIndex%COLORS.length];return new echarts.graphic.LinearGradient(0,0,1,0,[{offset:0,color:c+'88'},{offset:1,color:c}])}}
|
||||
}],
|
||||
animationDuration:600,
|
||||
},true);
|
||||
|
||||
// Keywords
|
||||
const kwEl = document.getElementById('cv-keywords');
|
||||
kwEl.innerHTML = `<table class="dt"><thead><tr><th>Запрос</th><th>Лиды</th><th>Продажи</th><th>CVR</th></tr></thead><tbody>` +
|
||||
c.keywords.map(k => `<tr>
|
||||
<td style="color:var(--text)">${k.q}</td>
|
||||
<td>${k.leads.toLocaleString('ru')}</td>
|
||||
<td>${k.sales}</td>
|
||||
<td><span class="badge ${k.c==='var(--g)'?'g':k.c==='var(--b)'?'b':k.c==='var(--a)'?'a':'r'}">${k.cv}%</span></td>
|
||||
</tr>`).join('') + '</tbody></table>';
|
||||
|
||||
// Managers
|
||||
const mgEl = document.getElementById('cv-managers');
|
||||
mgEl.innerHTML = `<table class="dt"><thead><tr><th>Менеджер</th><th>Лиды</th><th>Продажи</th><th>CVR</th></tr></thead><tbody>` +
|
||||
c.managers.map(m => `<tr>
|
||||
<td style="color:var(--text)">${m.n}</td>
|
||||
<td>${m.leads.toLocaleString('ru')}</td>
|
||||
<td>${m.sales}</td>
|
||||
<td><span class="badge ${m.c==='var(--g)'?'g':m.c==='var(--b)'?'b':m.c==='var(--a)'?'a':'r'}">${m.cv}%</span></td>
|
||||
</tr>`).join('') + '</tbody></table>';
|
||||
|
||||
// Funnel
|
||||
const fn = document.getElementById('cv-funnel');
|
||||
if (!charts.convFunnel) charts.convFunnel = echarts.init(fn);
|
||||
const fc = ['#22c55e','#3b82f6','#a855f7'];
|
||||
const funnelData = c.funnel.map((f,i)=>({name:f.n,value:f.v,itemStyle:{color:fc[i%fc.length]}}));
|
||||
charts.convFunnel.setOption({
|
||||
tooltip:{trigger:'item',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
|
||||
series:[{
|
||||
type:'funnel',left:'10%',bottom:5,width:'80%',min:0,max:c.funnel[0].v,
|
||||
minSize:'0%',maxSize:'100%',sort:'descending',gap:4,
|
||||
label:{show:true,position:'inside',color:'#fff',fontSize:11,formatter:'{b}\n{d}%'},
|
||||
labelLine:{show:false},itemStyle:{borderColor:'#111',borderWidth:2},
|
||||
data:funnelData,
|
||||
}],
|
||||
animationDuration:600,
|
||||
},true);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
LTV RENDER
|
||||
============================================ */
|
||||
function renderLTV(d) {
|
||||
const c = d.ltv;
|
||||
|
||||
sumRow('ltv-sum', [
|
||||
{l:'LTV средний',v:c.avg.v,d:c.avg.d,t:c.avg.t,n:c.avg.n},
|
||||
{l:'Повторные',v:c.repeat.v,d:c.repeat.d,t:c.repeat.t,n:c.repeat.n},
|
||||
{l:'Удержание',v:c.retention.v,d:c.retention.d,t:c.retention.t,n:c.retention.n},
|
||||
{l:'Ср. чек',v:c.avgCheck.v,d:c.avgCheck.d,t:c.avgCheck.t,n:c.avgCheck.n},
|
||||
]);
|
||||
|
||||
// Main: LTV curve
|
||||
const mc = document.getElementById('ltv-main');
|
||||
if (!charts.ltvMain) charts.ltvMain = echarts.init(mc);
|
||||
document.getElementById('ltv-main-t').textContent = 'Кумулятивный LTV';
|
||||
document.getElementById('ltv-main-s').textContent = 'Накопленный доход от клиента по месяцам';
|
||||
charts.ltvMain.setOption({
|
||||
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
|
||||
legend:{data:['SEO','Директ','VK'],textStyle:{color:'#52525b',fontSize:10},top:0},
|
||||
grid:{left:44,right:16,top:28,bottom:28},
|
||||
xAxis:{type:'category',data:c.curve.labels,axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#52525b',fontSize:10}},
|
||||
yAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:10,formatter:'{value}К'}},
|
||||
series:[
|
||||
{name:'SEO',type:'line',data:c.curve.seo,smooth:.4,symbol:'none',lineStyle:{width:2,color:'#22c55e'}},
|
||||
{name:'Директ',type:'line',data:c.curve.direct,smooth:.4,symbol:'none',lineStyle:{width:2,color:'#3b82f6'}},
|
||||
{name:'VK',type:'line',data:c.curve.vk,smooth:.4,symbol:'none',lineStyle:{width:2,color:'#f59e0b'}},
|
||||
],
|
||||
animationDuration:600,
|
||||
},true);
|
||||
|
||||
// Side: LTV by channel
|
||||
const sc = document.getElementById('ltv-side');
|
||||
if (!charts.ltvSide) charts.ltvSide = echarts.init(sc);
|
||||
const ltvChannels = c.channels.sort((a,b)=>a.ltv-b.ltv);
|
||||
charts.ltvSide.setOption({
|
||||
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
|
||||
grid:{left:80,right:30,top:10,bottom:10},
|
||||
xAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:9,formatter:'{value}К'}},
|
||||
yAxis:{type:'category',data:ltvChannels.map(x=>x.n).reverse(),axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#a1a1aa',fontSize:10}},
|
||||
series:[{
|
||||
type:'bar',data:ltvChannels.map(x=>x.ltv/1000).reverse(),barWidth:38,
|
||||
itemStyle:{borderRadius:[0,5,5,0],color:p=>{const c=COLORS[p.dataIndex%COLORS.length];return new echarts.graphic.LinearGradient(0,0,1,0,[{offset:0,color:c+'88'},{offset:1,color:c}])}}
|
||||
}],
|
||||
animationDuration:600,
|
||||
},true);
|
||||
|
||||
// Retention curve
|
||||
const rt = document.getElementById('ltv-retention');
|
||||
if (!charts.ltvRet) charts.ltvRet = echarts.init(rt);
|
||||
charts.ltvRet.setOption({
|
||||
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
|
||||
legend:{data:['SEO','Директ','VK'],textStyle:{color:'#52525b',fontSize:10},top:0},
|
||||
grid:{left:44,right:16,top:28,bottom:28},
|
||||
xAxis:{type:'category',data:c.retention.labels,axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#52525b',fontSize:10}},
|
||||
yAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:10,formatter:'{value}%'}},
|
||||
series:[
|
||||
{name:'SEO',type:'line',data:c.retention.seo,smooth:.4,symbol:'none',lineStyle:{width:2,color:'#22c55e'}},
|
||||
{name:'Директ',type:'line',data:c.retention.direct,smooth:.4,symbol:'none',lineStyle:{width:2,color:'#3b82f6'}},
|
||||
{name:'VK',type:'line',data:c.retention.vk,smooth:.4,symbol:'none',lineStyle:{width:2,color:'#f59e0b'}},
|
||||
],
|
||||
animationDuration:600,
|
||||
},true);
|
||||
|
||||
// Average check by channel
|
||||
const ck = document.getElementById('ltv-check');
|
||||
if (!charts.ltvCheck) charts.ltvCheck = echarts.init(ck);
|
||||
const checks = [...c.checks].sort((a,b)=>parseInt(a.avg)-parseInt(b.avg));
|
||||
charts.ltvCheck.setOption({
|
||||
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
|
||||
grid:{left:80,right:30,top:10,bottom:10},
|
||||
xAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:9,formatter:'{value}К'}},
|
||||
yAxis:{type:'category',data:checks.map(x=>x.n).reverse(),axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#a1a1aa',fontSize:10}},
|
||||
series:[{
|
||||
type:'bar',data:checks.map(x=>parseInt(x.avg)/1000).reverse(),barWidth:38,
|
||||
itemStyle:{borderRadius:[0,5,5,0],color:p=>{
|
||||
const ch = checks[p.dataIndex];
|
||||
return new echarts.graphic.LinearGradient(0,0,1,0,[{offset:0,color:ch.c+'88'},{offset:1,color:ch.c}]);
|
||||
}}
|
||||
}],
|
||||
animationDuration:600,
|
||||
},true);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
MAIN RENDER
|
||||
============================================ */
|
||||
function render() {
|
||||
const d = DATA[curSeg];
|
||||
|
||||
if (curTab === 'cpa') {
|
||||
document.querySelectorAll('.section').forEach(s=>s.classList.remove('active'));
|
||||
document.getElementById('sec-cpa').classList.add('active');
|
||||
renderCPA(d);
|
||||
} else if (curTab === 'conv') {
|
||||
document.querySelectorAll('.section').forEach(s=>s.classList.remove('active'));
|
||||
document.getElementById('sec-conv').classList.add('active');
|
||||
renderConv(d);
|
||||
} else {
|
||||
document.querySelectorAll('.section').forEach(s=>s.classList.remove('active'));
|
||||
document.getElementById('sec-ltv').classList.add('active');
|
||||
renderLTV(d);
|
||||
}
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
EVENTS
|
||||
============================================ */
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Tabs
|
||||
document.getElementById('tabs').addEventListener('click', e => {
|
||||
const tab = e.target.closest('.tab');
|
||||
if (!tab) return;
|
||||
curTab = tab.dataset.t;
|
||||
document.querySelectorAll('.tab').forEach(t=>t.classList.toggle('active', t.dataset.t===curTab));
|
||||
render();
|
||||
});
|
||||
|
||||
// Business switcher
|
||||
document.getElementById('bswitch').addEventListener('click', e => {
|
||||
const btn = e.target.closest('.bbtn');
|
||||
if (!btn) return;
|
||||
curSeg = btn.dataset.b;
|
||||
document.querySelectorAll('.bbtn').forEach(b=>b.classList.toggle('active', b.dataset.b===curSeg));
|
||||
render();
|
||||
});
|
||||
|
||||
render();
|
||||
|
||||
// Clock
|
||||
function tick() {
|
||||
const n = new Date();
|
||||
document.getElementById('clock').textContent =
|
||||
String(n.getHours()).padStart(2,'0')+':'+String(n.getMinutes()).padStart(2,'0')+':'+String(n.getSeconds()).padStart(2,'0');
|
||||
}
|
||||
tick(); setInterval(tick, 1000);
|
||||
|
||||
// Resize
|
||||
new ResizeObserver(() => Object.values(charts).forEach(c=>c.resize())).observe(document.getElementById('dashboard'));
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user