Files
roi24/dashboard-mockup.html
T
neo 0b49915420 feat: Bloomberg Refresh 2024 table style for E-commerce tab
- Added Bloomberg-style table with Follow buttons, ticker IDs, values, and change arrows
- Two sections: Revenue by channel + Marketing metrics
- SVG arrows (12x10) with fill=currentcolor for up/down/neutral states
- Monospace typography for IDs, values, and change amounts
- Responsive grid layout (desktop → tablet → mobile)
- Follow button toggle with blue active state
2026-06-21 18:01:04 +05:00

723 lines
32 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 GRID — Bloomberg ticker style */
.kpi-grid {
display: grid; grid-template-columns: repeat(6, 1fr);
gap: 14px; padding: 0;
max-width: 1280px; margin: 0 auto;
}
.kpi {
background: #0e0e0e;
border: 1px solid #1e1e1e;
border-radius: 8px;
padding: 16px 18px 14px;
position: relative;
display: flex; flex-direction: column;
gap: 6px;
transition: all 0.25s var(--ease);
opacity: 0; transform: translateY(8px);
}
.kpi.visible { opacity: 1; transform: translateY(0); }
.kpi:hover {
border-color: #2e2e2e;
background: #131313;
transform: translateY(-2px);
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
}
.kpi-top { display: flex; align-items: center; justify-content: space-between; }
.kpi-name {
font-size: 10px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.1em;
color: #666;
}
.kpi-badge {
font-family: var(--mono); font-size: 10px; font-weight: 600;
padding: 2px 7px; border-radius: 4px;
display: inline-flex; align-items: center; gap: 2px;
letter-spacing: 0;
}
.kpi-badge.up { color: #22c55e; background: rgba(34,197,94,0.08); }
.kpi-badge.down { color: #ef4444; background: rgba(239,68,68,0.08); }
.kpi-badge.neutral { color: #3b82f6; background: rgba(59,130,246,0.08); }
.kpi-value {
font-family: var(--mono);
font-size: 24px; font-weight: 700;
letter-spacing: -0.5px; line-height: 1.1;
color: #f4f4f4;
}
.sparkline-wrap {
width: 100%;
height: 40px;
flex-shrink: 0;
}
.kpi-context { font-size: 10px; color: #4a4a4a; line-height: 1.4; }
/* 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
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="sparkline-wrap" id="spark-${i}"></div>
<div class="kpi-context">${d.p}</div>
</div>`;
}).join('');
requestAnimationFrame(() => g.querySelectorAll('.kpi').forEach(c => c.classList.add('visible')));
// Bloomberg table — channels + marketing
const arrowUp = '<svg class="b-arrow" viewBox="0 0 24 20"><path fill="currentColor" d="M12 0L24 20L0 20L12 0Z"/></svg>';
const arrowDown = '<svg class="b-arrow" viewBox="0 0 24 20"><path fill="currentColor" d="M12 20L0 0L24 0L12 20Z"/></svg>';
const arrowNeutral = '<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' ? arrowUp : t === 'down' ? arrowDown : arrowNeutral;
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);
// Sparkline charts
const sparkColors = [];
s.kpis.forEach(d => { sparkColors.push(d.t === 'up' ? '#22c55e' : d.t === 'down' ? '#ef4444' : '#3b82f6'); });
s.kpis.forEach((d, i) => {
if (d.spark) {
const sparkEl = document.getElementById(`spark-${i}`);
if (!sparkEl) return;
const sparkChart = echarts.init(sparkEl, null, { renderer: 'canvas' });
const isDownTrend = d.t === 'down' && d.p.includes('стоим') || d.p.includes('дефект') || d.p.includes('за единиц') || d.p.includes('стоим');
sparkChart.setOption({
grid: { left: 0, right: 0, top: 4, bottom: 4 },
xAxis: { type: 'category', show: false, data: d.spark },
yAxis: { type: 'value', show: false },
series: [{
type: 'line', data: d.spark, smooth: 0.4, symbol: 'none',
lineStyle: { width: 2, color: sparkColors[i] },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: sparkColors[i] + '30' },
{ offset: 1, color: sparkColors[i] + '05' }
])
},
}],
animationDuration: 800,
});
}
});
// 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>