Files
roi24/dashboard-mockup.html
T

520 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>