Files
roi24/dashboard-mockup.html
T

879 lines
27 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;
--bg-card: #111113;
--bg-card-hover: #16161a;
--border: rgba(255,255,255,0.06);
--border-hover: rgba(255,255,255,0.1);
--text: #fafafa;
--text-sec: #a1a1aa;
--text-muted: #52525b;
--green: #22c55e;
--green-dim: rgba(34,197,94,0.12);
--blue: #3b82f6;
--blue-dim: rgba(59,130,246,0.12);
--amber: #f59e0b;
--amber-dim: rgba(245,158,11,0.12);
--red: #ef4444;
--red-dim: rgba(239,68,68,0.12);
--purple: #a855f7;
--purple-dim: rgba(168,85,247,0.12);
--cyan: #06b6d4;
--cyan-dim: 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;
}
/* Ambient glow */
body::before {
content: '';
position: fixed;
top: -200px;
left: 50%;
transform: translateX(-50%);
width: 600px;
height: 400px;
background: radial-gradient(ellipse, rgba(34,197,94,0.04) 0%, transparent 70%);
pointer-events: none;
}
/* ============================================
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;
}
.logo-dot {
width: 8px; height: 8px;
background: var(--green);
border-radius: 50%;
animation: pulse 2s ease-in-out infinite;
box-shadow: 0 0 8px rgba(34,197,94,0.4);
}
@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); }
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
font-size: 12px;
}
.live-badge {
display: flex;
align-items: center;
gap: 6px;
color: var(--green);
font-weight: 600;
}
.live-dot {
width: 6px; height: 6px;
background: var(--green);
border-radius: 50%;
animation: pulse 2s ease-in-out infinite;
}
.clock {
font-family: var(--mono);
color: var(--text-muted);
font-size: 11px;
}
/* ============================================
SEGMENT SWITCHER
============================================ */
.switcher-wrap {
display: flex;
justify-content: center;
padding: 32px 24px 0;
}
.switcher {
display: flex;
gap: 4px;
padding: 5px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 14px;
}
.sw-btn {
padding: 10px 20px;
border: none;
background: transparent;
color: var(--text-sec);
font-family: var(--mono);
font-size: 12px;
font-weight: 500;
border-radius: 10px;
cursor: pointer;
transition: all 0.25s var(--ease);
white-space: nowrap;
display: flex;
align-items: center;
gap: 6px;
}
.sw-btn:hover { color: var(--text); }
.sw-btn.active {
background: var(--green-dim);
color: var(--green);
box-shadow: 0 0 0 1px rgba(34,197,94,0.2);
}
.sw-btn .icon { font-size: 15px; }
/* ============================================
KPI GRID — 6 cards
============================================ */
.kpi-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 12px;
padding: 24px 24px 0;
max-width: 1280px;
margin: 0 auto;
}
.kpi {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 16px;
position: relative;
overflow: hidden;
transition: all 0.25s var(--ease);
opacity: 0;
transform: translateY(12px);
}
.kpi.visible {
opacity: 1;
transform: translateY(0);
}
.kpi:hover {
border-color: var(--border-hover);
transform: translateY(-2px);
}
.kpi-accent {
position: absolute;
top: 0; left: 0;
width: 3px; height: 100%;
opacity: 0;
transition: opacity 0.25s;
}
.kpi:hover .kpi-accent { opacity: 1; }
.kpi-name {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-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;
}
.kpi-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;
}
.kpi-trend.up { color: var(--green); background: var(--green-dim); }
.kpi-trend.down { color: var(--red); background: var(--red-dim); }
.kpi-trend.neutral { color: var(--blue); background: var(--blue-dim); }
.kpi-period {
color: var(--text-muted);
font-size: 10px;
}
/* ============================================
CHART ROWS
============================================ */
.chart-row {
display: grid;
gap: 12px;
padding: 16px 24px;
max-width: 1280px;
margin: 0 auto;
}
.chart-row.two { grid-template-columns: 2fr 1fr; }
.chart-row.three { grid-template-columns: 1fr 1fr 1fr; }
.chart-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 20px;
transition: border-color 0.25s;
}
.chart-card:hover { border-color: var(--border-hover); }
.chart-head {
margin-bottom: 4px;
}
.chart-title {
font-size: 13px;
font-weight: 600;
color: var(--text);
}
.chart-sub {
font-size: 11px;
color: var(--text-muted);
}
.chart-dom {
width: 100%;
height: 280px;
}
.chart-dom.small { height: 200px; }
/* ============================================
METRIC LIST
============================================ */
.metric-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 12px;
}
.metric-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid var(--border);
}
.metric-item:last-child { border-bottom: none; }
.metric-label {
font-size: 12px;
color: var(--text-sec);
flex: 1;
}
.metric-val {
font-family: var(--mono);
font-size: 12px;
font-weight: 600;
color: var(--text);
margin: 0 12px;
min-width: 60px;
text-align: right;
}
.metric-bar {
width: 70px; height: 4px;
background: var(--border);
border-radius: 2px;
overflow: hidden;
}
.metric-fill {
height: 100%;
border-radius: 2px;
width: 0;
transition: width 1s var(--ease);
}
/* ============================================
SECTION HEADER
============================================ */
.section-header {
text-align: center;
padding: 40px 24px 20px;
}
.section-header h2 {
font-size: clamp(1.6rem, 4vw, 2.2rem);
font-weight: 800;
letter-spacing: -0.5px;
margin-bottom: 6px;
}
.section-header p {
color: var(--text-sec);
font-size: 14px;
}
/* ============================================
RESPONSIVE
============================================ */
@media (max-width: 1024px) {
.kpi-grid { grid-template-columns: repeat(3, 1fr); }
.chart-row.two,
.chart-row.three { grid-template-columns: 1fr; }
}
@media (max-width: 640px) {
.kpi-grid { grid-template-columns: repeat(2, 1fr); }
.sw-btn { padding: 8px 12px; font-size: 11px; }
.sw-btn .label { display: none; }
}
</style>
</head>
<body>
<!-- HEADER -->
<div class="header">
<div class="logo">
<span class="logo-dot"></span>
ROI24
</div>
<div class="header-right">
<div class="live-badge">
<span class="live-dot"></span>
Live
</div>
<div class="clock" id="clock">--:--:--</div>
</div>
</div>
<!-- SECTION HEADER -->
<div class="section-header">
<h2>Все ключевые цифры — в одном месте</h2>
<p>Переключите бизнес, чтобы увидеть свой дашборд</p>
</div>
<!-- SWITCHER -->
<div class="switcher-wrap">
<div class="switcher" id="switcher">
<button class="sw-btn active" data-seg="ecom">
<span class="icon">🛒</span>
<span class="label">Интернет-магазин</span>
</button>
<button class="sw-btn" data-seg="services">
<span class="icon">💼</span>
<span class="label">Услуги / Агентство</span>
</button>
<button class="sw-btn" data-seg="production">
<span class="icon">🏭</span>
<span class="label">Производство</span>
</button>
</div>
</div>
<!-- DASHBOARD CONTAINER -->
<div id="dash">
<!-- KPI Row -->
<div class="kpi-grid" id="kpi-grid"></div>
<!-- Row 1: Main + Side -->
<div class="chart-row two">
<div class="chart-card">
<div class="chart-head">
<div class="chart-title" id="mc-title">Выручка</div>
<div class="chart-sub" id="mc-sub">12 месяцев · с трендом</div>
</div>
<div class="chart-dom" id="mc"></div>
</div>
<div class="chart-card">
<div class="chart-head">
<div class="chart-title" id="sc-title">ROI по каналам</div>
<div class="chart-sub">Текущий период</div>
</div>
<div class="chart-dom" id="sc"></div>
</div>
</div>
<!-- Row 2: Three small -->
<div class="chart-row three">
<div class="chart-card">
<div class="chart-head">
<div class="chart-title">ROMI рекламных кампаний</div>
<div class="chart-sub">Возврат инвестиций в маркетинг</div>
</div>
<div class="chart-dom small" id="ch1"></div>
</div>
<div class="chart-card">
<div class="chart-head">
<div class="chart-title">Воронка продаж</div>
<div class="chart-sub">Конверсия по этапам</div>
</div>
<div class="chart-dom small" id="ch2"></div>
</div>
<div class="chart-card">
<div class="chart-head">
<div class="chart-title">Выполнение плана</div>
<div class="chart-sub">Ключевые метрики</div>
</div>
<div class="metric-list" id="mlist"></div>
</div>
</div>
</div>
<!-- ECharts -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
<script>
// ============================================
// DATA
// ============================================
const D = {
ecom: {
kpis: [
{ name: 'Выручка', val: '4.2М ₽', trend: 12, trendTxt: '+12%', per: 'vs прошл. мес' },
{ name: 'CPC', val: '42 ₽', trend: -15, trendTxt: '-15%', per: 'оптимизация' },
{ name: 'Конверсия', val: '3.8%', trend: 0.5, trendTxt: '+0.5%', per: 'от клика до оплаты' },
{ name: 'CAC', val: '680 ₽', trend: -8, trendTxt: '-8%', per: 'стоимость клиента' },
{ name: 'LTV', val: '12 400 ₽', trend: 18, trendTxt: '+18%', per: 'за 6 мес' },
{ name: 'LTV/CAC', val: '18.2x', trend: 28, trendTxt: '+28%', per: 'коэф. окупаемости' },
],
mainChart: {
title: 'Выручка и реклама',
sub: '12 месяцев · выручка (зелёный) vs расход (синий)',
type: 'dual',
labels: ['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'],
revenue: [2.8,3.1,2.9,3.4,3.6,3.2,3.8,4.0,3.9,4.2,3.7,4.2],
adSpend: [0.6,0.7,0.65,0.72,0.68,0.75,0.7,0.65,0.6,0.58,0.62,0.55],
},
sideChart: {
title: 'ROI по каналам',
type: 'bar-h',
data: [
{ name: 'SEO', val: 340 },
{ name: 'Email', val: 280 },
{ name: 'Прямой', val: 210 },
{ name: 'Контекст', val: 150 },
{ name: 'Соцсети', val: 120 },
{ name: 'Рефералы', val: 95 },
],
},
romiChart: {
type: 'bar-group',
labels: ['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'],
romi: [180,210,195,240,260,220,280,310,290,320,270,340],
},
funnel: {
data: [
{ name: 'Посетители', value: 45000 },
{ name: 'Корзина', value: 8100 },
{ name: 'Оформление', value: 4950 },
{ name: 'Оплата', value: 2723 },
],
},
metrics: [
{ label: 'Выручка (план)', value: '4.2М', max: '4.5М', pct: 93, color: 'var(--green)' },
{ label: 'Конверсия (план)', value: '3.8%', max: '4.0%', pct: 95, color: 'var(--green)' },
{ label: 'CAC (план)', value: '680₽', max: '600₽', pct: 88, color: 'var(--amber)' },
{ label: 'LTV (план)', value: '12.4К', max: '12К', pct: 104, color: 'var(--green)' },
{ label: 'ROMI (план)', value: '290%', max: '250%', pct: 116, color: 'var(--green)' },
],
},
services: {
kpis: [
{ name: 'Пайплайн', val: '12.8М ₽', trend: 18, trendTxt: '+18%', per: 'к прошлому мес' },
{ name: 'CPC', val: '85 ₽', trend: -10, trendTxt: '-10%', per: 'контекст' },
{ name: 'Конверсия', val: '24%', trend: 3, trendTxt: '+3%', per: 'лид→сделка' },
{ name: 'CAC', val: '4 200 ₽', trend: -12, trendTxt: '-12%', per: 'стоимость лида' },
{ name: 'LTV', val: '89 000 ₽', trend: 22, trendTxt: '+22%', per: 'за 12 мес' },
{ name: 'ROMI', val: '245%', trend: 35, trendTxt: '+35%', per: 'маркетинг ROI' },
],
mainChart: {
title: 'Пайплайн и доход',
sub: '12 месяцев · пайплайн (синий) vs закрытие (зелёный)',
type: 'dual',
labels: ['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'],
pipeline: [6.2,7.0,7.5,8.1,8.8,9.2,9.8,10.5,11.0,11.5,12.2,12.8],
closed: [1.8,2.1,2.0,2.3,2.5,2.2,2.8,3.0,2.7,3.1,2.6,3.2],
},
sideChart: {
title: 'ROMI по услугам',
type: 'bar-h',
data: [
{ name: 'SEO-контент', val: 420 },
{ name: 'Рефералы', val: 380 },
{ name: 'Консалтинг', val: 290 },
{ name: 'Дизайн', val: 240 },
{ name: 'Таргет', val: 160 },
{ name: 'Холодные', val: 85 },
],
},
romiChart: {
type: 'bar-group',
labels: ['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'],
romi: [120,145,160,180,200,190,220,250,235,260,215,280],
},
funnel: {
data: [
{ name: 'Лиды', value: 320 },
{ name: 'Квалификация', value: 224 },
{ name: 'Предложение', value: 128 },
{ name: 'Сделка', value: 77 },
],
},
metrics: [
{ label: 'Пайплайн (план)', value: '12.8М', max: '15М', pct: 85, color: 'var(--blue)' },
{ label: 'Конверсия (план)', value: '24%', max: '25%', pct: 96, color: 'var(--green)' },
{ label: 'CAC (план)', value: '4.2К', max: '4К', pct: 95, color: 'var(--green)' },
{ label: 'ROMI (план)', value: '245%', max: '200%', pct: 123, color: 'var(--green)' },
{ label: 'Загрузка (план)', value: '87%', max: '90%', pct: 97, color: 'var(--green)' },
],
},
production: {
kpis: [
{ name: 'Выпуск', val: '15 420 шт', trend: 8, trendTxt: '+8%', per: 'к плану' },
{ name: 'CPC (трафик)', val: '18 ₽', trend: -20, trendTxt: '-20%', per: 'SEO оптимизация' },
{ name: 'OEE', val: '78%', trend: 4, trendTxt: '+4%', per: 'загрузка мощностей' },
{ name: 'Брак', val: '2.1%', trend: -0.5, trendTxt: '-0.5%', per: 'снижение потерь' },
{ name: 'Себестоимость', val: '3 200 ₽', trend: -3, trendTxt: '-3%', per: 'экономия сырья' },
{ name: 'ROMI рекламы', val: '310%', trend: 45, trendTxt: '+45%', per: 'контекст + SEO' },
],
mainChart: {
title: 'План/факт выпуска',
sub: '12 месяцев · план (пунктир) vs факт (сплошная)',
type: 'bar-compare',
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],
},
sideChart: {
title: 'ROMI по каналам',
type: 'bar-h',
data: [
{ name: 'SEO', val: 380 },
{ name: 'Прямые', val: 310 },
{ name: 'Контекст', val: 210 },
{ name: 'Каталоги', val: 170 },
{ name: 'Таргет', val: 140 },
{ name: 'Выставки', val: 110 },
],
},
romiChart: {
type: 'bar-group',
labels: ['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'],
romi: [180,200,220,250,280,260,290,310,300,330,290,350],
},
funnel: {
data: [
{ name: 'Заказ', value: 15420 },
{ name: 'Контроль', value: 15200 },
{ name: 'Брак', value: 324 },
{ name: 'Доработка', value: 180 },
],
},
metrics: [
{ label: 'Выпуск (план)', value: '15.4К', max: '16К', pct: 96, color: 'var(--green)' },
{ label: 'OEE (план)', value: '78%', max: '85%', pct: 92, color: 'var(--amber)' },
{ label: 'Брак (план)', value: '2.1%', max: '2.0%', pct: 95, color: 'var(--amber)' },
{ label: 'ROMI (план)', value: '310%', max: '280%', pct: 111, color: 'var(--green)' },
{ label: 'Себест. (план)', value: '3.2К', max: '3.0К', pct: 94, color: 'var(--blue)' },
],
},
};
let charts = {};
let current = 'ecom';
// ============================================
// RENDER KPIs
// ============================================
function renderKpis(seg) {
const g = document.getElementById('kpi-grid');
const k = seg.kpis;
g.innerHTML = k.map((d, i) => {
const tClass = d.trend > 0 ? 'up' : (d.trend < 0 && i === 0 ? 'up' : 'neutral');
const dir = d.trend > 0 ? '↑' : (d.trend < 0 && i === 0 ? '↓' : '→');
return `<div class="kpi" style="transition-delay:${i*80}ms">
<div class="kpi-accent" style="background:${i<3?'var(--green)':i<5?'var(--blue)':'var(--purple)'}"></div>
<div class="kpi-name">${d.name}</div>
<div class="kpi-val">${d.val}</div>
<div class="kpi-meta">
<span class="kpi-trend ${tClass}">${dir} ${d.trendTxt}</span>
<span class="kpi-period">${d.per}</span>
</div>
</div>`;
}).join('');
requestAnimationFrame(() => {
g.querySelectorAll('.kpi').forEach(c => c.classList.add('visible'));
});
}
// ============================================
// MAIN CHART
// ============================================
function renderMain(seg) {
const c = seg.mainChart;
const el = document.getElementById('mc');
if (!charts.mc) charts.mc = echarts.init(el);
document.getElementById('mc-title').textContent = c.title;
document.getElementById('mc-sub').textContent = c.sub;
let opt;
if (c.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:c.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:c.pipeline||c.revenue, smooth:0.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:c.closed||c.adSpend, smooth:0.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 (c.type === 'bar-compare') {
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:c.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:c.plan, barWidth:'35%', itemStyle:{color:'rgba(59,130,246,0.15)',borderRadius:[4,4,0,0]} },
{ name:'Факт', type:'bar', data:c.fact, barWidth:'35%', itemStyle:{color:'#3b82f6',borderRadius:[4,4,0,0]} },
],
animationDuration: 600,
};
}
charts.mc.setOption(opt, true);
}
// ============================================
// SIDE CHART (bar horizontal)
// ============================================
function renderSide(seg) {
const c = seg.sideChart;
const el = document.getElementById('sc');
if (!charts.sc) charts.sc = echarts.init(el);
document.getElementById('sc-title').textContent = c.title;
const colors = ['#22c55e','#06b6d4','#a855f7','#3b82f6','#f59e0b','#ef4444'];
const opt = {
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:c.data.map(d=>d.name).reverse(), axisLine:{lineStyle:{color:'#1e1e22'}}, axisLabel:{color:'#a1a1aa',fontSize:10} },
series: [{
type:'bar',
data: c.data.map(d=>d.value).reverse(),
barWidth:38,
itemStyle: {
borderRadius:[0,5,5,0],
color: function(params) {
const c = colors[params.dataIndex % colors.length];
return new echarts.graphic.LinearGradient(0,0,1,0,[
{offset:0, color: c+'88'},
{offset:1, color: c}
]);
}
}
}],
animationDuration: 600,
};
charts.sc.setOption(opt, true);
}
// ============================================
// ROMI CHART (bar group)
// ============================================
function renderROMI(seg) {
const el = document.getElementById('ch1');
if (!charts.ch1) charts.ch1 = echarts.init(el);
const c = seg.romiChart;
const opt = {
tooltip: { trigger:'axis', backgroundColor:'#111', borderColor:'#222', textStyle:{color:'#fafafa',fontSize:11} },
grid: { left:44, right:16, top:16, bottom:28 },
xAxis: { type:'category', data:c.labels, axisLine:{lineStyle:{color:'#1e1e22'}}, axisLabel:{color:'#52525b',fontSize:10} },
yAxis: { type:'value', splitLine:{lineStyle:{color:'#1a1a1e'}}, axisLabel:{color:'#52525b',fontSize:9,formatter:'{value}%'} },
series: [{
name:'ROMI %',
type:'bar',
data:c.romi,
barWidth:'55%',
itemStyle: {
borderRadius:[4,4,0,0],
color: function(params) {
return params.value >= 250 ? '#22c55e' : (params.value >= 200 ? '#06b6d4' : '#f59e0b');
}
}
}],
animationDuration: 600,
};
charts.ch1.setOption(opt, true);
}
// ============================================
// FUNNEL
// ============================================
function renderFunnel(seg) {
const el = document.getElementById('ch2');
if (!charts.ch2) charts.ch2 = echarts.init(el);
const colors = ['#22c55e','#06b6d4','#a855f7','#ef4444'];
const opt = {
tooltip: { trigger:'item', backgroundColor:'#111', borderColor:'#222', textStyle:{color:'#fafafa',fontSize:11} },
series: [{
type:'funnel',
left:'10%', bottom:5, width:'80%',
min:0, max: seg.funnel.data[0].value,
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: seg.funnel.data.map((d,i)=>({
name:d.name, value:d.value,
itemStyle: { color: colors[i%colors.length] }
})),
}],
animationDuration: 600,
};
charts.ch2.setOption(opt, true);
}
// ============================================
// METRICS LIST
// ============================================
function renderMetrics(seg) {
const el = document.getElementById('mlist');
el.innerHTML = seg.metrics.map(m => {
return `<div class="metric-item">
<span class="metric-label">${m.label}</span>
<span class="metric-val">${m.value}</span>
<div class="metric-bar"><div class="metric-fill" data-pct="${m.pct}" style="background:${m.color}"></div></div>
</div>`;
}).join('');
requestAnimationFrame(() => {
setTimeout(() => {
el.querySelectorAll('.metric-fill').forEach(f => f.style.width = f.dataset.pct + '%');
}, 100);
});
}
// ============================================
// MASTER SWITCH
// ============================================
function seg(key) {
current = key;
const s = D[key];
// Buttons
document.querySelectorAll('.sw-btn').forEach(b => {
b.classList.toggle('active', b.dataset.seg === key);
});
renderKpis(s);
renderMain(s);
renderSide(s);
renderROMI(s);
renderFunnel(s);
renderMetrics(s);
}
// ============================================
// INIT
// ============================================
document.addEventListener('DOMContentLoaded', () => {
// Segment switcher — event delegation
document.getElementById('switcher').addEventListener('click', (e) => {
const btn = e.target.closest('.sw-btn');
if (!btn) return;
seg(btn.dataset.seg);
});
// Initial render
seg('ecom');
// 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
const ro = new ResizeObserver(() => {
Object.values(charts).forEach(c => c.resize());
});
ro.observe(document.getElementById('dash'));
});
</script>
</body>
</html>