- 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
723 lines
32 KiB
HTML
723 lines
32 KiB
HTML
<!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>
|