Files
roi24/dashboard-mockup.html
T

567 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=IBM+Plex+Sans:400,500,600,700&family=IBM+Plex+Mono:400,500,600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0a0a0a;
--surface: #161616;
--surface-hover: #1f1f1f;
--border: #262626;
--border-strong: #393939;
--text-primary: #f4f4f4;
--text-secondary: #a6a6a6;
--text-tertiary: #6f6f6f;
--accent: #0f6fff;
--accent-hover: #3d8eff;
--green: #16a34a;
--green-dim: rgba(22,163,74,0.12);
--red: #e21d1d;
--red-dim: rgba(226,29,29,0.12);
--yellow: #eac518;
--mono: 'IBM Plex Mono', monospace;
--sans: 'IBM Plex Sans', -apple-system, sans-serif;
--ease: cubic-bezier(0.2, 0, 0, 1);
}
html { font-size: 14px; }
body {
font-family: var(--sans);
background: var(--bg);
color: var(--text-primary);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
}
/* TOP BAR */
.topbar {
display: flex; align-items: center; justify-content: space-between;
padding: 0 32px; height: 48px;
background: var(--surface);
border-bottom: 1px solid var(--border);
position: sticky; top: 0; z-index: 100;
}
.topbar-brand {
font-family: var(--mono); font-size: 13px; font-weight: 600;
letter-spacing: 0.5px; display: flex; align-items: center; gap: 10px;
}
.topbar-brand .icon {
width: 20px; height: 20px; background: var(--accent);
display: flex; align-items: center; justify-content: center;
font-size: 11px; font-weight: 700; color: #fff;
}
.topbar-right { display: flex; align-items: center; gap: 20px; font-size: 12px; }
.status { display: flex; align-items: center; gap: 6px; color: var(--green); font-weight: 500; }
.status-dot { width: 6px; height: 6px; background: var(--green); animation: blink 2s ease-in-out infinite; }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:0.4} }
.clock { font-family: var(--mono); color: var(--text-tertiary); font-size: 11px; letter-spacing: 0.3px; }
/* PAGE HEADER */
.page-header {
max-width: 1280px; margin: 0 auto;
padding: 32px 32px 0;
}
.page-title {
font-size: 28px; font-weight: 700;
letter-spacing: -0.5px; line-height: 1.2;
color: var(--text-primary); margin-bottom: 8px;
}
.page-desc { font-size: 14px; color: var(--text-secondary); font-weight: 400; }
/* TABS */
.tabs-section {
max-width: 1280px; margin: 0 auto;
padding: 24px 32px 0;
}
.tabs {
display: flex; align-items: center; gap: 0;
border-bottom: 1px solid var(--border-strong);
overflow-x: auto; scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }
.tab-btn {
padding: 12px 20px; border: none; background: transparent;
color: var(--text-secondary); font-family: var(--sans);
font-size: 14px; font-weight: 600; cursor: pointer;
position: relative; white-space: nowrap;
transition: color 0.15s var(--ease);
}
.tab-btn:hover { color: var(--text-primary); }
.tab-btn::after {
content: ''; position: absolute;
bottom: -1px; left: 0; right: 0; height: 2px;
background: var(--accent); transform: scaleX(0);
transform-origin: left; transition: transform 0.2s var(--ease);
}
.tab-btn.active { color: var(--accent); }
.tab-btn.active::after { transform: scaleX(1); }
.tab-label { display: flex; align-items: center; gap: 8px; }
/* KPI GRID — IBM Carbon style: single border around all */
.kpi-grid {
display: grid; grid-template-columns: repeat(6, 1fr);
gap: 1px; padding: 0; max-width: 1280px; margin: 0 auto;
background: var(--border); border: 1px solid var(--border);
border-radius: 4px; overflow: hidden;
}
.kpi {
background: var(--surface);
padding: 20px 24px;
position: relative;
transition: background 0.15s;
opacity: 0; transform: translateY(8px);
}
.kpi.visible { opacity: 1; transform: translateY(0); }
.kpi:hover { background: var(--surface-hover); }
.kpi-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.kpi-name {
font-size: 11px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.06em;
color: var(--text-secondary);
}
.kpi-badge {
font-family: var(--mono); font-size: 10px; font-weight: 500;
padding: 2px 6px; border-radius: 0;
text-transform: uppercase; letter-spacing: 0.05em;
}
.kpi-badge.up { color: var(--green); background: var(--green-dim); }
.kpi-badge.down { color: var(--red); background: var(--red-dim); }
.kpi-badge.neutral { color: var(--accent); background: rgba(15,111,255,0.1); }
.kpi-value {
font-family: var(--mono);
font-size: 24px; font-weight: 600;
letter-spacing: -0.5px; line-height: 1.1;
color: var(--text-primary); margin-bottom: 8px;
}
.kpi-context { font-size: 11px; color: var(--text-tertiary); }
/* CHARTS WRAPPER */
.charts-wrap {
max-width: 1280px; margin: 0 auto;
padding: 0 32px 32px;
}
.charts-grid {
display: grid; gap: 1px;
background: var(--border);
border: 1px solid var(--border);
border-radius: 4px; overflow: hidden;
}
.chart-panel {
background: var(--surface);
padding: 24px;
}
.chart-header { margin-bottom: 20px; }
.chart-title { font-size: 14px; font-weight: 600; color: var(--text-primary); margin-bottom: 4px; }
.chart-subtitle { font-size: 12px; color: var(--text-tertiary); }
.chart-canvas { width: 100%; height: 260px; }
.chart-canvas.small { height: 200px; }
/* Metric list */
.metric-list { display: flex; flex-direction: column; gap: 0; margin-top: 8px; }
.metric-item {
display: flex; align-items: center;
padding: 12px 0;
border-bottom: 1px solid var(--border);
}
.metric-item:last-child { border-bottom: none; }
.m-label { font-size: 12px; color: var(--text-secondary); flex: 1; }
.m-value { font-family: var(--mono); font-size: 13px; font-weight: 600; color: var(--text-primary); margin: 0 16px; min-width: 64px; text-align: right; }
.m-bar { width: 80px; height: 4px; background: var(--border-strong); overflow: hidden; }
.m-fill { height: 100%; width: 0; transition: width 0.8s var(--ease); }
.m-fill.green { background: var(--green); }
.m-fill.red { background: var(--red); }
.m-fill.yellow { background: var(--yellow); }
.m-fill.blue { background: var(--accent); }
/* RESPONSIVE */
@media(max-width: 1024px) {
.kpi-grid { grid-template-columns: repeat(3, 1fr); }
}
@media(max-width: 640px) {
.kpi-grid { grid-template-columns: repeat(2, 1fr); }
.topbar { padding: 0 16px; }
.page-header, .tabs-section, .charts-wrap { padding-left: 16px; padding-right: 16px; }
.tab-btn { padding: 12px 14px; font-size: 13px; }
}
</style>
</head>
<body>
<div class="topbar">
<div class="topbar-brand"><div class="icon">R</div>ROI24</div>
<div class="topbar-right">
<div class="status"><div class="status-dot"></div> Live data</div>
<div class="clock" id="clock"></div>
</div>
</div>
<div class="page-header">
<h1 class="page-title">Аналитика бизнеса</h1>
<p class="page-desc">Ключевые показатели эффективности в реальном времени</p>
</div>
<div class="tabs-section">
<div class="tabs" id="tabs">
<button class="tab-btn active" data-seg="ecom"><span class="tab-label"><span class="tab-icon">🛒</span>E-commerce</span></button>
<button class="tab-btn" data-seg="services"><span class="tab-label"><span class="tab-icon">💼</span>Услуги</span></button>
<button class="tab-btn" data-seg="production"><span class="tab-label"><span class="tab-icon">🏭</span>Производство</span></button>
</div>
</div>
<div class="kpi-grid" id="kpi-grid"></div>
<div class="charts-wrap">
<div class="charts-grid">
<!-- Row 1: Main + Side -->
<div class="chart-panel"><div class="chart-header"><div class="chart-title" id="mc-title"></div><div class="chart-subtitle" id="mc-sub"></div></div><div class="chart-canvas" id="mc-canvas"></div></div>
<div class="chart-panel"><div class="chart-header"><div class="chart-title" id="sc-title"></div><div class="chart-subtitle" id="sc-sub"></div></div><div class="chart-canvas" id="sc-canvas"></div></div>
<!-- Row 2: Funnel + Second + Metrics -->
<div class="chart-panel"><div class="chart-header"><div class="chart-title" id="c1-title"></div><div class="chart-subtitle" id="c1-sub"></div></div><div class="chart-canvas small" id="c1-canvas"></div></div>
<div class="chart-panel"><div class="chart-header"><div class="chart-title" id="c2-title"></div><div class="chart-subtitle" id="c2-sub"></div></div><div class="chart-canvas small" id="c2-canvas"></div></div>
<div class="chart-panel"><div class="chart-header"><div class="chart-title">Выполнение плана</div><div class="chart-subtitle">Ключевые метрики</div></div><div class="metric-list" id="metric-list"></div></div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
<script>
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:'green'},
{l:'CVR (план)',v:'3.8%',p:95,c:'green'},
{l:'ROAS (план)',v:'4.2×',p:105,c:'green'},
{l:'CAC (план)',v:'680₽',p:85,c:'yellow'},
{l:'CLV (план)',v:'12.4К',p:104,c:'green'},
],
},
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:'blue'},
{l:'Win Rate (план)',v:'24%',p:96,c:'green'},
{l:'Sales Cycle (план)',v:'32д',p:90,c:'green'},
{l:'Pipeline Cov. (план)',v:'4.1×',p:103,c:'green'},
{l:'ROMI (план)',v:'245%',p:123,c:'green'},
],
},
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:'green'},
{l:'OEE (план)',v:'78%',p:92,c:'yellow'},
{l:'Брак (план)',v:'2.1%',p:105,c:'green'},
{l:'ROMI (план)',v:'310%',p:111,c:'green'},
{l:'Себест. (план)',v:'3.2К',p:94,c:'blue'},
],
},
};
let charts = {};
function seg(key) {
const s = D[key];
// Tabs
document.querySelectorAll('.tab-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) => {
const arrow = d.t === 'up' ? '▲' : d.t === 'down' ? '▼' : '●';
return `<div class="kpi" style="transition-delay:${i*60}ms">
<div class="kpi-top">
<span class="kpi-name">${d.n}</span>
<span class="kpi-badge ${d.t}">${arrow} ${d.d}</span>
</div>
<div class="kpi-value">${d.v}</div>
<div class="kpi-context">${d.p}</div>
</div>`;
}).join('');
requestAnimationFrame(() => g.querySelectorAll('.kpi').forEach(c => c.classList.add('visible')));
// Main chart
document.getElementById('mc-title').textContent = s.mc.title;
document.getElementById('mc-sub').textContent = s.mc.sub;
const mcEl = document.getElementById('mc-canvas');
if (!charts.mc) charts.mc = echarts.init(mcEl);
charts.mc.clear();
let mcOpt;
if (s.mc.type === 'dual') {
mcOpt = {
tooltip: { trigger: 'axis', backgroundColor: '#161616', borderColor: '#262626', textStyle: { color: '#f4f4f4', fontSize: 12 } },
legend: { data: ['Доход', 'Расход'], textStyle: { color: '#6f6f6f', fontSize: 11 }, top: 0, itemWidth: 12, itemHeight: 2 },
grid: { left: 48, right: 20, top: 32, bottom: 28 },
xAxis: {
type: 'category', data: s.mc.labels,
axisLine: { lineStyle: { color: '#262626' } },
axisLabel: { color: '#6f6f6f', fontSize: 11 },
axisTick: { show: false },
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: '#1f1f1f' } },
axisLabel: { color: '#6f6f6f', fontSize: 11, formatter: '{value}М' },
},
series: [
{
name: 'Доход', type: 'line', data: s.mc.rev, smooth: 0.4, symbol: 'none',
lineStyle: { width: 2.5, color: '#16a34a' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(22,163,74,0.15)' },
{ offset: 1, color: 'rgba(22,163,74,0.01)' }
])
}
},
{
name: 'Расход', type: 'line', data: s.mc.ads, smooth: 0.4, symbol: 'none',
lineStyle: { width: 1.5, color: '#0f6fff', type: 'dashed' },
},
],
animationDuration: 500,
};
} else {
mcOpt = {
tooltip: { trigger: 'axis', backgroundColor: '#161616', borderColor: '#262626', textStyle: { color: '#f4f4f4', fontSize: 12 } },
legend: { data: ['План', 'Факт'], textStyle: { color: '#6f6f6f', fontSize: 11 }, top: 0, itemWidth: 12, itemHeight: 14 },
grid: { left: 48, right: 20, top: 32, bottom: 28 },
xAxis: {
type: 'category', data: s.mc.labels,
axisLine: { lineStyle: { color: '#262626' } },
axisLabel: { color: '#6f6f6f', fontSize: 11 },
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: '#1f1f1f' } },
axisLabel: { color: '#6f6f6f', fontSize: 11, formatter: '{value}К' },
},
series: [
{ name: 'План', type: 'bar', data: s.mc.plan, barWidth: '30%', itemStyle: { color: 'rgba(15,111,255,0.1)' } },
{ name: 'Факт', type: 'bar', data: s.mc.fact, barWidth: '30%', itemStyle: { color: '#0f6fff' } },
],
animationDuration: 500,
};
}
charts.mc.setOption(mcOpt);
// Side chart
document.getElementById('sc-title').textContent = s.sc.title;
document.getElementById('sc-sub').textContent = s.sc.sub;
const scData = s.sc.data.slice().reverse();
const scColors = ['#0f6fff', '#3d8eff', '#16a34a', '#06b6d4', '#a855f7', '#eac518'];
if (!charts.sc) charts.sc = echarts.init(document.getElementById('sc-canvas'));
charts.sc.clear();
charts.sc.setOption({
grid: { left: 90, right: 28, top: 8, bottom: 8 },
xAxis: {
type: 'value',
splitLine: { lineStyle: { color: '#1f1f1f' } },
axisLabel: { color: '#6f6f6f', fontSize: 10, formatter: '{value}%' },
},
yAxis: {
type: 'category', data: scData.map(d => d.n),
axisLine: { lineStyle: { color: '#262626' } },
axisLabel: { color: '#a6a6a6', fontSize: 11 },
},
series: [{
type: 'bar', data: scData.map((d, i) => ({
value: d.v,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: scColors[i % scColors.length] + '66' },
{ offset: 1, color: scColors[i % scColors.length] }
])
}
})),
barWidth: 28,
}],
animationDuration: 500,
});
// Funnel
document.getElementById('c1-title').textContent = s.c1.title;
document.getElementById('c1-sub').textContent = s.c1.sub;
const fc = ['#0f6fff', '#3d8eff', '#16a34a', '#e21d1d'];
if (!charts.c1) charts.c1 = echarts.init(document.getElementById('c1-canvas'));
charts.c1.clear();
charts.c1.setOption({
series: [{
type: 'funnel', left: '8%', bottom: 5, width: '84%', min: 0, max: s.c1.data[0].v,
minSize: '0%', maxSize: '100%', sort: 'descending', gap: 2,
label: { show: true, position: 'inside', color: '#f4f4f4', fontSize: 11, formatter: '{b}\n{d}%' },
labelLine: { show: false },
itemStyle: { borderColor: '#161616', borderWidth: 2 },
data: s.c1.data.map((d, i) => ({ name: d.n, value: d.v, itemStyle: { color: fc[i % fc.length] } })),
}],
animationDuration: 500,
});
// Second chart
document.getElementById('c2-title').textContent = s.c2.title;
document.getElementById('c2-sub').textContent = s.c2.sub;
const c2 = s.c2;
const isPie = c2.labels && c2.data.reduce((a, b) => a + b, 0) <= 100;
const pc = ['#0f6fff', '#16a34a', '#eac518', '#a855f7', '#e21d1d'];
if (!charts.c2) charts.c2 = echarts.init(document.getElementById('c2-canvas'));
charts.c2.clear();
if (isPie) {
charts.c2.setOption({
series: [{
type: 'pie', radius: ['40%', '65%'], center: ['50%', '55%'],
itemStyle: { borderRadius: 0, borderColor: '#161616', borderWidth: 2 },
label: { show: false }, labelLine: { show: false },
data: c2.labels.map((l, i) => ({ name: l, value: c2.data[i], itemStyle: { color: pc[i % pc.length] } })),
}],
animationDuration: 500,
});
} else {
charts.c2.setOption({
grid: { left: 60, right: 16, top: 8, bottom: 28 },
xAxis: {
type: 'value',
splitLine: { lineStyle: { color: '#1f1f1f' } },
axisLabel: { color: '#6f6f6f', fontSize: 10, formatter: '{value}%' },
},
yAxis: {
type: 'category', data: c2.labels,
axisLine: { lineStyle: { color: '#262626' } },
axisLabel: { color: '#a6a6a6', fontSize: 11 },
},
series: [{
type: 'bar', data: c2.data.map(v => ({
value: v,
itemStyle: { color: v >= 100 ? '#16a34a' : '#eac518' }
})),
barWidth: 24,
}],
animationDuration: 500,
});
}
// Metrics list
const ml = document.getElementById('metric-list');
ml.innerHTML = s.metrics.map(m => `
<div class="metric-item">
<span class="m-label">${m.l}</span>
<span class="m-value">${m.v}</span>
<div class="m-bar"><div class="m-fill ${m.c}" data-p="${m.p}"></div></div>
</div>`).join('');
requestAnimationFrame(() => setTimeout(() => {
ml.querySelectorAll('.m-fill').forEach(f => f.style.width = f.dataset.p + '%');
}, 100));
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('tabs').addEventListener('click', e => {
const b = e.target.closest('.tab-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.querySelector('.charts-grid'));
});
</script>
</body>
</html>