520 lines
23 KiB
HTML
520 lines
23 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>ROI24 — Интерактивный дашборд</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,0.06);
|
||
--border-h: rgba(255,255,255,0.1);
|
||
--text: #fafafa;
|
||
--sec: #a1a1aa;
|
||
--muted: #52525b;
|
||
--g: #22c55e; --gd: rgba(34,197,94,0.12);
|
||
--b: #3b82f6; --bd: rgba(59,130,246,0.12);
|
||
--a: #f59e0b; --ad: rgba(245,158,11,0.12);
|
||
--r: #ef4444; --rd: rgba(239,68,68,0.12);
|
||
--p: #a855f7; --pd: rgba(168,85,247,0.12);
|
||
--c: #06b6d4; --cd: rgba(6,182,212,0.12);
|
||
--mono: 'JetBrains Mono', monospace;
|
||
--ease: cubic-bezier(0.4, 0, 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; overflow-x: hidden;
|
||
}
|
||
|
||
/* ============================================
|
||
HEADER
|
||
============================================ */
|
||
.header {
|
||
display: flex; align-items: center; justify-content: space-between;
|
||
padding: 12px 28px;
|
||
background: rgba(17,17,19,0.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;
|
||
letter-spacing: -0.3px; 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,0.4);
|
||
animation: pulse 2s ease-in-out infinite;
|
||
}
|
||
@keyframes pulse {
|
||
0%,100%{box-shadow:0 0 8px rgba(34,197,94,0.4);}
|
||
50%{box-shadow:0 0 16px rgba(34,197,94,0.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; }
|
||
|
||
/* ============================================
|
||
SWITCHER
|
||
============================================ */
|
||
.sw-wrap { display:flex; justify-content:center; padding:32px 24px 0; }
|
||
.sw {
|
||
display:flex; gap:4px; padding:5px;
|
||
background:var(--card); border:1px solid var(--border);
|
||
border-radius:14px;
|
||
}
|
||
.sw-btn {
|
||
padding:10px 20px; 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;
|
||
}
|
||
.sw-btn:hover{color:var(--text);}
|
||
.sw-btn.active{
|
||
background:var(--gd); color:var(--g);
|
||
box-shadow:0 0 0 1px rgba(34,197,94,0.2);
|
||
}
|
||
|
||
/* ============================================
|
||
KPI GRID — 6 cards, research-backed
|
||
============================================ */
|
||
.kpi-grid {
|
||
display:grid; grid-template-columns:repeat(6,1fr);
|
||
gap:12px; padding:24px; max-width:1280px; margin:0 auto;
|
||
}
|
||
.kpi {
|
||
background:var(--card); border:1px solid var(--border);
|
||
border-radius:12px; padding:16px;
|
||
position:relative; overflow:hidden;
|
||
transition:all .25s var(--ease);
|
||
opacity:0; transform:translateY(12px);
|
||
}
|
||
.kpi.vis { opacity:1; transform:translateY(0); }
|
||
.kpi:hover{border-color:var(--border-h); transform:translateY(-2px);}
|
||
.kpi-accent {
|
||
position:absolute; top:0;left:0; width:3px; height:100%; opacity:0; transition:opacity .25s;
|
||
}
|
||
.kpi:hover .kpi-accent{opacity:1;}
|
||
.kpi-name {
|
||
font-size:10px; text-transform:uppercase; letter-spacing:.08em;
|
||
color:var(--muted); font-weight:600; margin-bottom:8px;
|
||
}
|
||
.kpi-val {
|
||
font-family:var(--mono); font-size:22px; font-weight:700;
|
||
margin-bottom:6px; line-height:1.1;
|
||
}
|
||
.kpi-meta { display:flex; align-items:center; gap:6px; font-size:11px; }
|
||
.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;
|
||
}
|
||
.t.up{color:var(--g);background:var(--gd);}
|
||
.t.down{color:var(--r);background:var(--rd);}
|
||
.t.neu{color:var(--b);background:var(--bd);}
|
||
.per { color:var(--muted); font-size:10px; }
|
||
|
||
/* ============================================
|
||
CHARTS
|
||
============================================ */
|
||
.cr { display:grid; gap:12px; padding:12px 24px; max-width:1280px; margin:0 auto; }
|
||
.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:280px; }
|
||
.cd.s { height:200px; }
|
||
|
||
.metric-list { display:flex; flex-direction:column; gap:10px; margin-top:12px; }
|
||
.mi { display:flex; align-items:center; justify-content:space-between; padding:8px 0; border-bottom:1px solid var(--border); }
|
||
.mi:last-child{border-bottom:none;}
|
||
.ml { font-size:12px; color:var(--sec); flex:1; }
|
||
.mv { font-family:var(--mono); font-size:12px; font-weight:600; color:var(--text); margin:0 12px; min-width:60px; text-align:right; }
|
||
.mb { width:70px; height:4px; background:var(--border); border-radius:2px; overflow:hidden; }
|
||
.mf { height:100%; border-radius:2px; width:0; transition:width 1s var(--ease); }
|
||
|
||
.section-hdr { text-align:center; padding:40px 24px 20px; }
|
||
.section-hdr h2 { font-size:clamp(1.6rem,4vw,2.2rem); font-weight:800; letter-spacing:-.5px; margin-bottom:6px; }
|
||
.section-hdr p { color:var(--sec); font-size:14px; }
|
||
|
||
@media(max-width:1024px){
|
||
.kpi-grid{grid-template-columns:repeat(3,1fr);}
|
||
.cr2,.cr3{grid-template-columns:1fr;}
|
||
}
|
||
@media(max-width:640px){
|
||
.kpi-grid{grid-template-columns:repeat(2,1fr);}
|
||
.sw-btn{padding:8px 12px;font-size:11px;}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="header">
|
||
<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="section-hdr">
|
||
<h2>Все ключевые цифры — в одном месте</h2>
|
||
<p>Переключите бизнес — увидите свои метрики</p>
|
||
</div>
|
||
|
||
<div class="sw-wrap">
|
||
<div class="sw" id="sw">
|
||
<button class="sw-btn active" data-seg="ecom">🛒 Интернет-магазин</button>
|
||
<button class="sw-btn" data-seg="services">💼 Услуги / Агентство</button>
|
||
<button class="sw-btn" data-seg="production">🏭 Производство</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="dash">
|
||
<div class="kpi-grid" id="kpi-grid"></div>
|
||
<div class="cr cr2">
|
||
<div class="cc">
|
||
<div class="ch"><div class="ct" id="mc-title"></div><div class="cs" id="mc-sub"></div></div>
|
||
<div class="cd" id="mc"></div>
|
||
</div>
|
||
<div class="cc">
|
||
<div class="ch"><div class="ct" id="sc-title"></div><div class="cs" id="sc-sub"></div></div>
|
||
<div class="cd" id="sc"></div>
|
||
</div>
|
||
</div>
|
||
<div class="cr cr3">
|
||
<div class="cc">
|
||
<div class="ch"><div class="ct" id="c1-title"></div><div class="cs" id="c1-sub"></div></div>
|
||
<div class="cd s" id="c1"></div>
|
||
</div>
|
||
<div class="cc">
|
||
<div class="ch"><div class="ct" id="c2-title"></div><div class="cs" id="c2-sub"></div></div>
|
||
<div class="cd s" id="c2"></div>
|
||
</div>
|
||
<div class="cc">
|
||
<div class="ch"><div class="ct">Выполнение плана</div><div class="cs">Ключевые метрики</div></div>
|
||
<div class="metric-list" id="mlist"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
|
||
<script>
|
||
/* ============================================
|
||
DATA — KPIs from research:
|
||
E-com: Revenue, AOV, CVR, CAC, CLV, ROAS (Shopify)
|
||
Services: Revenue, Win Rate, Sales Cycle, Pipeline Coverage, CPL, ROMI (Optifai)
|
||
Production: Volume, OEE, Defect Rate, Cost/Unit, Inventory Turnover, ROMI
|
||
============================================ */
|
||
const D = {
|
||
ecom: {
|
||
kpis: [
|
||
{ n:'Выручка', v:'4.2М ₽', d:'+12%', t:'up', p:'vs прошл. мес' },
|
||
{ n:'AOV', v:'4 820 ₽', d:'+3%', t:'up', p:'ср. чек' },
|
||
{ n:'CVR', v:'3.8%', d:'+0.5%', t:'up', p:'клики→покупка' },
|
||
{ n:'CAC', v:'680 ₽', d:'−8%', t:'down', p:'стоим. клиента' },
|
||
{ n:'CLV', v:'12 400 ₽', d:'+18%', t:'up', p:'за 6 мес' },
|
||
{ n:'ROAS', v:'4.2×', d:'+15%', t:'up', p:'возврат рекламы' },
|
||
],
|
||
mc: { title:'Выручка и расходы на рекламу', sub:'12 месяцев · выручка (зелёный) vs маркетинг-бюджет (синий)', type:'dual',
|
||
labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'],
|
||
rev:[2.8,3.1,2.9,3.4,3.6,3.2,3.8,4.0,3.9,4.2,3.7,4.2],
|
||
ads:[0.6,0.7,0.65,0.72,0.68,0.75,0.7,0.65,0.6,0.58,0.62,0.55],
|
||
},
|
||
sc: { title:'ROAS по каналам', sub:'Возврат на каждый рубль рекламы', type:'bar-h',
|
||
data:[
|
||
{n:'SEO',v:520},{n:'Email',v:380},{n:'Прямой',v:280},
|
||
{n:'Контекст',v:150},{n:'Соцсети',v:120},{n:'Рефералы',v:95},
|
||
],
|
||
},
|
||
c1: { title:'Конверсия воронки', sub:'Просмотр → Корзина → Оплата → Покупка',
|
||
data:[
|
||
{n:'Посетители',v:45000},{n:'Корзина',v:8100},
|
||
{n:'Оформление',v:4950},{n:'Оплата',v:2723},
|
||
],
|
||
},
|
||
c2: { title:'Топ-5 товаров', sub:'По выручке за месяц',
|
||
labels:['Товар А','Товар Б','Товар В','Товар Г','Товар Д'],
|
||
data:[120,98,76,54,42],
|
||
},
|
||
metrics: [
|
||
{l:'Выручка (план)',v:'4.2М',p:93,c:'var(--g)'},
|
||
{l:'CVR (план)',v:'3.8%',p:95,c:'var(--g)'},
|
||
{l:'ROAS (план)',v:'4.2×',p:105,c:'var(--g)'},
|
||
{l:'CAC (план)',v:'680₽',p:85,c:'var(--a)'},
|
||
{l:'CLV (план)',v:'12.4К',p:104,c:'var(--g)'},
|
||
],
|
||
},
|
||
services: {
|
||
kpis: [
|
||
{ n:'Пайплайн', v:'12.8М ₽', d:'+18%', t:'up', p:'открытых сделок' },
|
||
{ n:'Win Rate', v:'24%', d:'+3%', t:'up', p:'закрыто/всего' },
|
||
{ n:'Sales Cycle', v:'32 дн', d:'−5%', t:'down', p:'длительность' },
|
||
{ n:'Pipeline Cov.', v:'4.1×', d:'+0.6', t:'up', p:'покрытие квоты' },
|
||
{ n:'CPL', v:'2 400 ₽', d:'−12%', t:'down', p:'стоимость лида' },
|
||
{ n:'ROMI', v:'245%', d:'+35%', t:'up', p:'маркетинг ROI' },
|
||
],
|
||
mc: { title:'Пайплайн и закрытые сделки', sub:'12 месяцев · пайплайн (синий) vs закрытие (зелёный)', type:'dual',
|
||
labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'],
|
||
rev:[6.2,7.0,7.5,8.1,8.8,9.2,9.8,10.5,11.0,11.5,12.2,12.8],
|
||
ads:[1.8,2.1,2.0,2.3,2.5,2.2,2.8,3.0,2.7,3.1,2.6,3.2],
|
||
},
|
||
sc: { title:'ROMI по услугам', sub:'Возврат инвестиций в каждую услугу', type:'bar-h',
|
||
data:[
|
||
{n:'SEO-контент',v:420},{n:'Рефералы',v:380},{n:'Консалтинг',v:290},
|
||
{n:'Дизайн',v:240},{n:'Таргет',v:160},{n:'Холодные',v:85},
|
||
],
|
||
},
|
||
c1: { title:'Воронка продаж', sub:'Лид → Квалификация → Предложение → Сделка',
|
||
data:[
|
||
{n:'Лиды',v:320},{n:'Квалиф.',v:224},
|
||
{n:'Предложение',v:128},{n:'Сделка',v:77},
|
||
],
|
||
},
|
||
c2: { title:'Quota Attainment', sub:'Выполнение квоты менеджерами',
|
||
labels:['Иванов','Петров','Сидоров','Козлов','Морозов'],
|
||
data:[112,95,88,105,78],
|
||
},
|
||
metrics: [
|
||
{l:'Пайплайн (план)',v:'12.8М',p:85,c:'var(--b)'},
|
||
{l:'Win Rate (план)',v:'24%',p:96,c:'var(--g)'},
|
||
{l:'Sales Cycle (план)',v:'32д',p:90,c:'var(--g)'},
|
||
{l:'Pipeline Cov. (план)',v:'4.1×',p:103,c:'var(--g)'},
|
||
{l:'ROMI (план)',v:'245%',p:123,c:'var(--g)'},
|
||
],
|
||
},
|
||
production: {
|
||
kpis: [
|
||
{ n:'Выпуск', v:'15 420 шт', d:'+8%', t:'up', p:'к плану' },
|
||
{ n:'OEE', v:'78%', d:'+4%', t:'up', p:'загрузка мощностей' },
|
||
{ n:'Брак', v:'2.1%', d:'−0.5%', t:'down', p:'дефекты' },
|
||
{ n:'Себест.', v:'3 200 ₽', d:'−3%', t:'down', p:'за единицу' },
|
||
{ n:'Оборачив.', v:'5.2×', d:'+0.3', t:'up', p:'запасов в год' },
|
||
{ n:'ROMI рекламы', v:'310%', d:'+45%', t:'up', p:'маркетинг ROI' },
|
||
],
|
||
mc: { title:'План / факт выпуска', sub:'12 месяцев · план (пунктир) vs фактический выпуск (сплошная)', type:'bar-cmp',
|
||
labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'],
|
||
plan:[12,13,12.5,14,14.5,13,15,15.5,15,15.5,14,16],
|
||
fact:[11.5,12.8,12.2,13.8,14.2,12.5,14.8,15.2,14.7,15.0,13.8,15.4],
|
||
},
|
||
sc: { title:'ROMI по каналам', sub:'Возврат инвестиций в привлечение заказов', type:'bar-h',
|
||
data:[
|
||
{n:'SEO',v:380},{n:'Прямые',v:310},{n:'Контекст',v:210},
|
||
{n:'Каталоги',v:170},{n:'Таргет',v:140},{n:'Выставки',v:110},
|
||
],
|
||
},
|
||
c1: { title:'Качество (воронка)', sub:'Выпуск → Контроль → Брак → Доработка',
|
||
data:[
|
||
{n:'Выпуск',v:15420},{n:'Контроль',v:15200},
|
||
{n:'Брак',v:324},{n:'Доработка',v:180},
|
||
],
|
||
},
|
||
c2: { title:'Структура затрат', sub:'Доля в себестоимости',
|
||
labels:['Сырьё','Зарплата','Энергия','Логистика','Прочее'],
|
||
data:[45,25,12,10,8],
|
||
},
|
||
metrics: [
|
||
{l:'Выпуск (план)',v:'15.4К',p:96,c:'var(--g)'},
|
||
{l:'OEE (план)',v:'78%',p:92,c:'var(--a)'},
|
||
{l:'Брак (план)',v:'2.1%',p:105,c:'var(--g)'},
|
||
{l:'ROMI (план)',v:'310%',p:111,c:'var(--g)'},
|
||
{l:'Себест. (план)',v:'3.2К',p:94,c:'var(--b)'},
|
||
],
|
||
},
|
||
};
|
||
|
||
let charts = {};
|
||
function seg(key) {
|
||
const s = D[key];
|
||
document.querySelectorAll('.sw-btn').forEach(b => b.classList.toggle('active', b.dataset.seg === key));
|
||
|
||
// KPIs
|
||
const g = document.getElementById('kpi-grid');
|
||
g.innerHTML = s.kpis.map((d,i) => `
|
||
<div class="kpi" style="transition-delay:${i*80}ms">
|
||
<div class="kpi-accent" style="background:${i<3?'var(--g)':i<5?'var(--b)':'var(--p)'}"></div>
|
||
<div class="kpi-name">${d.n}</div>
|
||
<div class="kpi-val">${d.v}</div>
|
||
<div class="kpi-meta">
|
||
<span class="trend ${d.t}">${d.t==='up'?'↑':d.t==='down'?'↓':'→'} ${d.d}</span>
|
||
<span class="per">${d.p}</span>
|
||
</div>
|
||
</div>`).join('');
|
||
requestAnimationFrame(() => g.querySelectorAll('.kpi').forEach(c=>c.classList.add('vis')));
|
||
|
||
// Main chart
|
||
const mc = s.mc;
|
||
const mcDom = document.getElementById('mc');
|
||
if (!charts.mc) charts.mc = echarts.init(mcDom);
|
||
document.getElementById('mc-title').textContent = mc.title;
|
||
document.getElementById('mc-sub').textContent = mc.sub;
|
||
|
||
let opt;
|
||
if (mc.type==='dual') {
|
||
opt = {
|
||
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
|
||
legend:{data:['Доход','Расход'],textStyle:{color:'#52525b',fontSize:10},top:0},
|
||
grid:{left:44,right:16,top:28,bottom:28},
|
||
xAxis:{type:'category',data:mc.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:'Доход',type:'line',data:mc.rev,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,0.2)'},{offset:1,color:'rgba(34,197,94,0.01)'}])}},
|
||
{name:'Расход',type:'line',data:mc.ads,smooth:.4,symbol:'none',
|
||
lineStyle:{width:2,color:'#3b82f6',type:'dashed'},
|
||
areaStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[
|
||
{offset:0,color:'rgba(59,130,246,0.08)'},{offset:1,color:'rgba(59,130,246,0.01)'}])}},
|
||
],
|
||
animationDuration:600,
|
||
};
|
||
} else if (mc.type==='bar-cmp') {
|
||
opt = {
|
||
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
|
||
legend:{data:['План','Факт'],textStyle:{color:'#52525b',fontSize:10},top:0},
|
||
grid:{left:44,right:16,top:28,bottom:28},
|
||
xAxis:{type:'category',data:mc.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:'План',type:'bar',data:mc.plan,barWidth:'35%',itemStyle:{color:'rgba(59,130,246,0.15)',borderRadius:[4,4,0,0]}},
|
||
{name:'Факт',type:'bar',data:mc.fact,barWidth:'35%',itemStyle:{color:'#3b82f6',borderRadius:[4,4,0,0]}},
|
||
],
|
||
animationDuration:600,
|
||
};
|
||
}
|
||
charts.mc.setOption(opt, true);
|
||
|
||
// Side chart (bar horizontal)
|
||
const sc = s.sc;
|
||
const scDom = document.getElementById('sc');
|
||
if (!charts.sc) charts.sc = echarts.init(scDom);
|
||
document.getElementById('sc-title').textContent = sc.title;
|
||
document.getElementById('sc-sub').textContent = sc.sub;
|
||
const cols = ['#22c55e','#06b6d4','#a855f7','#3b82f6','#f59e0b','#ef4444'];
|
||
const scData = sc.data.reverse();
|
||
const scOpt = {
|
||
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:scData.map(d=>d.n),axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#a1a1aa',fontSize:10}},
|
||
series:[{
|
||
type:'bar',data:scData.map(d=>d.v),barWidth:38,
|
||
itemStyle:{
|
||
borderRadius:[0,5,5,0],
|
||
color:function(p){const c=cols[p.dataIndex%cols.length];return new echarts.graphic.LinearGradient(0,0,1,0,[{offset:0,color:c+'88'},{offset:1,color:c}]);}
|
||
}
|
||
}],
|
||
animationDuration:600,
|
||
};
|
||
charts.sc.setOption(scOpt, true);
|
||
|
||
// Chart 1: Funnel
|
||
const c1 = s.c1;
|
||
const c1Dom = document.getElementById('c1');
|
||
if (!charts.c1) charts.c1 = echarts.init(c1Dom);
|
||
document.getElementById('c1-title').textContent = c1.title;
|
||
document.getElementById('c1-sub').textContent = c1.sub;
|
||
const fc = ['#22c55e','#06b6d4','#a855f7','#ef4444'];
|
||
const c1Opt = {
|
||
tooltip:{trigger:'item',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
|
||
series:[{
|
||
type:'funnel',left:'10%',bottom:5,width:'80%',min:0,max:c1.data[0].v,
|
||
minSize:'0%',maxSize:'100%',sort:'descending',gap:4,
|
||
label:{show:true,position:'inside',color:'#fff',fontSize:11,formatter:'{b}\n{c} ({d}%)'},
|
||
labelLine:{show:false},itemStyle:{borderColor:'#111',borderWidth:2},
|
||
data:c1.data.map((d,i)=>({name:d.n,value:d.v,itemStyle:{color:fc[i%fc.length]}})),
|
||
}],
|
||
animationDuration:600,
|
||
};
|
||
charts.c1.setOption(c1Opt, true);
|
||
|
||
// Chart 2: depends on segment (pie or bar)
|
||
const c2 = s.c2;
|
||
const c2Dom = document.getElementById('c2');
|
||
if (!charts.c2) charts.c2 = echarts.init(c2Dom);
|
||
document.getElementById('c2-title').textContent = c2.title;
|
||
document.getElementById('c2-sub').textContent = c2.sub;
|
||
if (c2.labels) {
|
||
// bar or pie depending on data
|
||
const isPie = c2.data.length && c2.data.reduce((a,b)=>a+b,0) <= 100;
|
||
const pc = ['#22c55e','#3b82f6','#f59e0b','#a855f7','#ef4444'];
|
||
if (isPie) {
|
||
charts.c2.setOption({
|
||
tooltip:{trigger:'item',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
|
||
series:[{
|
||
type:'pie',radius:['45%','70%'],center:['50%','55%'],
|
||
itemStyle:{borderRadius:6,borderColor:'#111',borderWidth:3},
|
||
label:{show:false},labelLine:{show:false},
|
||
data:c2.labels.map((l,i)=>({name:l,value:c2.data[i],itemStyle:{color:pc[i%pc.length]}})),
|
||
emphasis:{itemStyle:{shadowBlur:10,shadowOffsetX:0,shadowColor:'rgba(0,0,0,0.3)'}},
|
||
}],
|
||
animationDuration:600,
|
||
}, true);
|
||
} else {
|
||
charts.c2.setOption({
|
||
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
|
||
grid:{left:60,right:16,top:10,bottom:28},
|
||
xAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:9,formatter:'{value}%'}},
|
||
yAxis:{type:'category',data:c2.labels,
|
||
axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#a1a1aa',fontSize:10}},
|
||
series:[{
|
||
type:'bar',data:c2.data,barWidth:30,
|
||
itemStyle:{
|
||
borderRadius:[4,4,0,0],
|
||
color:function(p){return p.data===undefined ? '#22c55e' : (p.data>=100?'#22c55e':'var(--a)');}
|
||
},
|
||
}],
|
||
animationDuration:600,
|
||
}, true);
|
||
}
|
||
}
|
||
|
||
// Metrics list
|
||
const ml = document.getElementById('mlist');
|
||
ml.innerHTML = s.metrics.map(m => `
|
||
<div class="mi">
|
||
<span class="ml">${m.l}</span>
|
||
<span class="mv">${m.v}</span>
|
||
<div class="mb"><div class="mf" data-p="${m.p}" style="background:${m.c}"></div></div>
|
||
</div>`).join('');
|
||
requestAnimationFrame(() => setTimeout(() => {
|
||
ml.querySelectorAll('.mf').forEach(f=>f.style.width=f.dataset.p+'%');
|
||
}, 100));
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
document.getElementById('sw').addEventListener('click', e => {
|
||
const b = e.target.closest('.sw-btn');
|
||
if (!b) return;
|
||
seg(b.dataset.seg);
|
||
});
|
||
seg('ecom');
|
||
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);
|
||
new ResizeObserver(() => Object.values(charts).forEach(c=>c.resize())).observe(document.getElementById('dash'));
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|