Files
roi24/dashboard-mockup.html
T

732 lines
33 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 href="https://fonts.cdnfonts.com/css/ibm-plex-sans" rel="stylesheet">
<link href="https://fonts.cdnfonts.com/css/ibm-plex-mono" 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: 40px 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: 32px 32px 0;
}
.tabs {
display: flex; align-items: center; gap: 0;
border-bottom: 1px solid var(--border-strong);
overflow-x: auto; scrollbar-width: none;
margin-bottom: 28px;
}
.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 CARDS — Bloomberg stock ticker style */
.kpi-grid {
display: grid; grid-template-columns: repeat(6, 1fr);
gap: 12px; padding: 0;
max-width: 1280px; margin: 0 auto;
}
.kpi {
background: #0e0e0e;
border: 1px solid #1e1e1e;
border-radius: 8px;
padding: 14px 16px;
transition: all 0.2s var(--ease);
opacity: 0; transform: translateY(8px);
cursor: default;
}
.kpi.visible { opacity: 1; transform: translateY(0); }
.kpi:hover {
border-color: #2e2e2e;
background: #131313;
box-shadow: 0 2px 12px rgba(0,0,0,0.25);
}
.kpi-header {
display: flex; align-items: baseline; justify-content: space-between;
margin-bottom: 8px;
}
.kpi-ticker {
font-family: var(--mono); font-size: 10px; font-weight: 600;
color: #555; letter-spacing: 0.03em;
}
.kpi-change {
font-family: var(--mono); font-size: 12px; font-weight: 700;
display: flex; align-items: center; gap: 3px;
}
.kpi-change.up { color: #16a34a; }
.kpi-change.down { color: #e21d1d; }
.kpi-change.neutral { color: #3b82f6; }
.kpi-trend { display: flex; align-items: center; gap: 6px; }
.kpi-trend-line {
width: 52px; height: 22px; display: block; flex-shrink: 0;
}
.kpi-value {
font-family: var(--mono);
font-size: 28px; font-weight: 700;
letter-spacing: -0.5px; line-height: 1.1;
color: #f4f4f4;
}
.kpi-desc {
font-size: 10px; color: #4a4a4a;
}
/* Arrow SVG inline */
.kpi-arrow { width: 10px; height: 8px; fill: currentColor; flex-shrink: 0; }
/* CHARTS WRAPPER */
.charts-wrap {
max-width: 1280px; margin: 0 auto;
padding: 28px 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); }
/* BLOOMBERG TABLE (E-commerce) */
.b-table-section { display: none; }
.b-table-section.active { display: block; }
.b-table {
width: 100%; border-collapse: collapse; font-size: 13px;
background: #0e0e0e; border: 1px solid #1e1e1e; border-radius: 12px; overflow: hidden;
}
.b-table-header { display: grid; grid-template-columns: 44px 110px 1fr 120px 120px; padding: 10px 18px; border-bottom: 1px solid #1e1e1e; }
.b-th { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: #555; }
.b-th.right { text-align: right; }
.b-row { display: grid; grid-template-columns: 44px 110px 1fr 120px 120px; padding: 10px 18px; transition: background 0.15s; }
.b-row:hover { background: #131313; }
.b-row + .b-row { border-top: 1px solid #141414; }
.b-row:last-child { border-radius: 0 0 11px 11px; }
.b-follow { display: flex; align-items: center; justify-content: center; cursor: pointer; }
.b-follow-btn {
width: 22px; height: 22px; border: 1px solid #2a2a2a; border-radius: 6px;
background: transparent; color: #444; font-size: 12px; display: flex;
align-items: center; justify-content: center; transition: all 0.15s;
}
.b-follow-btn:hover { color: #f4f4f4; border-color: #444; background: #1a1a1a; }
.b-follow-btn.active { color: #0f6fff; border-color: #0f6fff; background: rgba(15,111,255,0.08); }
.b-id { font-family: 'IBM Plex Mono', monospace; font-size: 11px; color: #555; white-space: nowrap; }
.b-name { font-size: 13px; color: #d4d4d4; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.b-value { font-family: 'IBM Plex Mono', monospace; font-size: 14px; font-weight: 600; color: #f4f4f4; text-align: right; letter-spacing: -0.3px; }
.b-change { display: flex; align-items: center; justify-content: flex-end; gap: 4px; font-family: 'IBM Plex Mono', monospace; font-size: 12px; font-weight: 400; }
.b-change.up { color: #16a34a; }
.b-change.down { color: #e21d1d; }
.b-change.neutral { color: #3b82f6; }
.b-arrow { width: 12px; height: 10px; fill: currentColor; flex-shrink: 0; }
.b-section-title {
font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em;
color: #444; padding: 20px 0 8px; margin-top: 8px; border-top: 1px solid #1a1a1a;
}
.b-section-title:first-child { border-top: none; margin-top: 0; }
.b-table-caption {
display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px;
}
.b-table-caption-title { font-size: 14px; font-weight: 600; color: #d4d4d4; }
.b-table-caption-sub { font-size: 12px; color: #555; }
/* RESPONSIVE */
@media(max-width: 1024px) {
.kpi-grid { grid-template-columns: repeat(3, 1fr); }
.b-table-header, .b-row { grid-template-columns: 44px 80px 1fr 90px 90px; }
}
@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; }
.b-table-header, .b-row { grid-template-columns: 36px 70px 1fr 70px 70px; padding: 8px 12px; }
.b-name { font-size: 12px; }
.b-value { font-size: 12px; }
.b-change { font-size: 11px; }
}
</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>
<!-- Bloomberg-style table (E-commerce only) -->
<div class="charts-wrap">
<div class="b-table-section active" id="b-table-section">
<div class="b-table-caption">
<span class="b-table-caption-title">Каналы и метрики</span>
<span class="b-table-caption-sub">Обновлено только что</span>
</div>
<div class="b-table-caption-sub b-section-title">Выручка по каналам</div>
<table class="b-table" id="b-table-channels">
<thead><tr><th class="b-th"></th><th class="b-th">ID</th><th class="b-th">Канал</th><th class="b-th right">Выручка</th><th class="b-th right">Изменение</th></tr></thead>
<tbody></tbody>
</table>
<div class="b-table-caption-sub b-section-title">Маркетинговые метрики</div>
<table class="b-table" id="b-table-marketing">
<thead><tr><th class="b-th"></th><th class="b-th">ID</th><th class="b-th">Метрика</th><th class="b-th right">Значение</th><th class="b-th right">Изменение</th></tr></thead>
<tbody></tbody>
</table>
</div>
</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 прошл. мес', spark:[2.8,3.1,2.9,3.4,3.6,3.2,3.8,4.0,3.9,4.2,3.7,4.2] },
{ n:'AOV', v:'4 820 ₽', d:'+3%', t:'up', p:'ср. чек', spark:[4200,4500,4350,4600,4700,4550,4800,4750,4650,4820,4700,4820] },
{ n:'CVR', v:'3.8%', d:'+0.5%', t:'up', p:'клики→покупка', spark:[3.2,3.3,3.1,3.4,3.5,3.3,3.6,3.7,3.5,3.8,3.6,3.8] },
{ n:'CAC', v:'680 ₽', d:'−8%', t:'down', p:'стоим. клиента', spark:[800,750,780,720,700,730,710,690,670,680,690,680] },
{ n:'CLV', v:'12 400 ₽', d:'+18%', t:'up', p:'за 6 мес', spark:[10000,10500,10200,11000,11200,10800,11500,11800,12000,12200,12100,12400] },
{ n:'ROAS', v:'4.2×', d:'+15%', t:'up', p:'возврат рекламы', spark:[3.5,3.6,3.7,3.8,3.9,3.7,4.0,4.1,4.0,4.2,4.1,4.2] },
],
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:'открытых сделок', spark:[6.2,7.0,7.5,8.1,8.8,9.2,9.8,10.5,11.0,11.5,12.2,12.8] },
{ n:'Win Rate', v:'24%', d:'+3%', t:'up', p:'закрыто/всего', spark:[20,21,22,21,22,23,22,23,24,23,24,24] },
{ n:'Sales Cycle', v:'32 дн', d:'−5%', t:'down', p:'длительность', spark:[35,34,34,33,34,33,33,32,33,32,33,32] },
{ n:'Pipeline Cov.', v:'4.1×', d:'+0.6', t:'up', p:'покрытие квоты', spark:[3.2,3.4,3.5,3.6,3.7,3.8,3.9,3.9,4.0,4.0,4.1,4.1] },
{ n:'CPL', v:'2 400 ₽', d:'−12%', t:'down', p:'стоимость лида', spark:[3200,3000,3100,2900,2800,2900,2700,2600,2500,2400,2500,2400] },
{ n:'ROMI', v:'245%', d:'+35%', t:'up', p:'маркетинг ROI', spark:[200,210,220,225,230,228,235,240,238,245,242,245] },
],
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:'к плану', spark:[11500,12800,12200,13800,14200,12500,14800,15200,14700,15000,13800,15420] },
{ n:'OEE', v:'78%', d:'+4%', t:'up', p:'загрузка мощностей', spark:[72,73,74,73,74,75,76,76,77,77,78,78] },
{ n:'Брак', v:'2.1%', d:'−0.5%', t:'down', p:'дефекты', spark:[2.8,2.7,2.6,2.5,2.5,2.6,2.4,2.3,2.3,2.2,2.2,2.1] },
{ n:'Себест.', v:'3 200 ₽', d:'−3%', t:'down', p:'за единицу', spark:[3500,3450,3400,3400,3350,3380,3320,3280,3250,3220,3250,3200] },
{ n:'Оборачив.', v:'5.2×', d:'+0.3', t:'up', p:'запасов в год', spark:[4.5,4.6,4.7,4.8,4.9,4.9,5.0,5.1,5.1,5.2,5.1,5.2] },
{ n:'ROMI рекламы', v:'310%', d:'+45%', t:'up', p:'маркетинг ROI', spark:[250,260,265,270,275,280,285,290,295,300,305,310] },
],
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 — Bloomberg stock ticker card style
const g = document.getElementById('kpi-grid');
const arrowUp = '<svg class="kpi-arrow" viewBox="0 0 24 20"><path fill="currentColor" d="M12 0L24 20L0 20L12 0Z"/></svg>';
const arrowDown = '<svg class="kpi-arrow" viewBox="0 0 24 20"><path fill="currentColor" d="M12 20L0 0L24 0L12 20Z"/></svg>';
function tickerId(name) {
// Convert "Выручка" → "REVN:KPI", "AOV" → "AOV:KPI"
const short = name.toUpperCase().replace(/[^A-ZА-ЯЁ0-9]/g, '').substring(0, 6);
return `${short}:KPI`;
}
g.innerHTML = s.kpis.map((d, i) => {
const isUp = d.t === 'up';
const isDown = d.t === 'down';
const changeColor = isUp ? 'up' : isDown ? 'down' : 'neutral';
const arrow = isUp ? arrowUp : arrowDown;
const sign = isUp ? '+' : isDown ? '−' : '±';
const trendColor = isUp ? '#16a34a' : isDown ? '#e21d1d' : '#3b82f6';
return `<div class="kpi" style="transition-delay:${i*60}ms">
<div class="kpi-header">
<span class="kpi-ticker">${tickerId(d.n)}</span>
<span class="kpi-change ${changeColor}">${arrow} ${sign}${d.d}</span>
</div>
<div class="kpi-value">${d.v}</div>
<div style="display:flex;align-items:baseline;justify-content:space-between;margin-top:4px;">
<div class="kpi-trend-line" id="trend-${i}"></div>
<div class="kpi-desc">${d.p}</div>
</div>
</div>`;
}).join('');
requestAnimationFrame(() => g.querySelectorAll('.kpi').forEach(c => c.classList.add('visible')));
// Bloomberg table — channels + marketing
const bArrowUp = '<svg class="b-arrow" viewBox="0 0 24 20"><path fill="currentColor" d="M12 0L24 20L0 20L12 0Z"/></svg>';
const bArrowDown = '<svg class="b-arrow" viewBox="0 0 24 20"><path fill="currentColor" d="M12 20L0 0L24 0L12 20Z"/></svg>';
const bArrowNeutral = '<svg class="b-arrow" viewBox="0 0 24 20"><path fill="currentColor" d="M12 0L24 20L0 20L12 0Z"/></svg>';
function changeCell(val, t) {
const cls = t === 'up' ? 'up' : t === 'down' ? 'down' : 'neutral';
const arrow = t === 'up' ? bArrowUp : t === 'down' ? bArrowDown : bArrowNeutral;
const sign = t === 'up' ? '+' : t === 'down' ? '−' : '±';
return `<div class="b-change ${cls}">${arrow} ${sign}${val}</div>`;
}
const channels = [
{ id:'SEO:CH', name:'SEO-трафик', value:'1.2М ₽', change:'18', t:'up' },
{ id:'PPC:CH', name:'Контекстная реклама', value:'980К ₽', change:'12', t:'up' },
{ id:'SMM:CH', name:'Соцсети', value:'640К ₽', change:'−3', t:'down' },
{ id:'EMAIL:CH',name:'Email-рассылки', value:'420К ₽', change:'8', t:'up' },
{ id:'ORG:CH', name:'Органический', value:'380К ₽', change:'2', t:'up' },
{ id:'REF:CH', name:'Реферальный', value:'156К ₽', change:'24', t:'up' },
{ id:'AFF:CH', name:'Партнёрские', value:'98К ₽', change:'−12', t:'down' },
];
const marketing = [
{ id:'CAC', name:'Стоимость клиента', value:'680 ₽', change:'8', t:'down' },
{ id:'AOV', name:'Ср. чек', value:'4 820 ₽', change:'3', t:'up' },
{ id:'CVR', name:'Конверсия', value:'3.8%', change:'0.5', t:'up' },
{ id:'CLV', name:'Пож. ценность', value:'12 400 ₽', change:'18', t:'up' },
{ id:'ROAS', name:'ROAS', value:'4.2×', change:'15', t:'up' },
{ id:'Bounce', name:'Показ. отказов', value:'32%', change:'1', t:'down' },
];
function renderTable(body, data) {
body.innerHTML = data.map((d, i) => `
<tr class="b-row" style="animation-delay:${i*40}ms">
<td class="b-follow"><button class="b-follow-btn" title="Follow">+</button></td>
<td class="b-id">${d.id}</td>
<td class="b-name">${d.name}</td>
<td class="b-value">${d.value}</td>
<td>${changeCell(d.change, d.t)}</td>
</tr>`).join('');
// Follow toggle
body.querySelectorAll('.b-follow-btn').forEach(btn => {
btn.addEventListener('click', () => btn.classList.toggle('active'));
});
}
const tbody1 = document.querySelector('#b-table-channels tbody');
const tbody2 = document.querySelector('#b-table-marketing tbody');
if (tbody1) renderTable(tbody1, channels);
if (tbody2) renderTable(tbody2, marketing);
// Mini trend lines inside KPI cards (Bloomberg ticker style)
const trendColors = s.kpis.map(d => d.t === 'up' ? '#16a34a' : d.t === 'down' ? '#e21d1d' : '#3b82f6');
s.kpis.forEach((d, i) => {
if (d.spark) {
const trendEl = document.getElementById(`trend-${i}`);
if (!trendEl) return;
const trendChart = echarts.init(trendEl, null, { width: 50, height: 20, renderer: 'canvas' });
trendChart.setOption({
grid: { left: 0, right: 0, top: 2, bottom: 2 },
xAxis: { type: 'category', show: false, data: d.spark },
yAxis: { type: 'value', show: false },
series: [{
type: 'line', data: d.spark, smooth: 0.5, symbol: 'none',
lineStyle: { width: 1.5, color: trendColors[i] },
}],
animationDuration: 600,
});
}
});
// 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>