Files
roi24/dashboard-mockup.html
T

707 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=IBM+Plex+Sans:400,500,600,700&family=IBM+Plex+Mono:400,500,600&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
/* IBM Carbon Design System — Dark 10 */
--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;
--accent-dim: rgba(15,111,255,0.1);
--green: #16a34a;
--green-dim: rgba(22,163,74,0.12);
--red: #e21d1d;
--red-dim: rgba(226,29,29,0.12);
--yellow: #eac518;
--yellow-dim: rgba(234,197,24,0.12);
--mono: 'IBM Plex Mono', monospace;
--sans: 'IBM Plex Sans', -apple-system, sans-serif;
--ease-out: cubic-bezier(0.2, 0, 0, 1);
--ease-standard: 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;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
}
/* ============================================
TOP BAR — IBM style
============================================ */
.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; color: var(--text-primary);
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); border-radius: 0; 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 — IBM heading style
============================================ */
.page-header {
max-width: 1280px; margin: 0 auto;
padding: 32px 32px 0;
}
.page-title {
font-family: var(--sans);
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; line-height: 1.5;
}
/* ============================================
IBM TABS — Horizontal navigation tabs
============================================ */
.tabs-section {
max-width: 1280px; margin: 0 auto;
padding: 24px 32px 0;
}
.tabs {
display: flex; align-items: center; gap: 0;
border-bottom: 1px solid var(--border-strong);
overflow-x: auto;
scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }
.tab-btn {
padding: 12px 20px;
border: none; background: transparent;
color: var(--text-secondary);
font-family: var(--sans);
font-size: 14px;
font-weight: 600;
cursor: pointer;
position: relative;
white-space: nowrap;
transition: color 0.15s var(--ease-standard);
}
.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-standard);
}
.tab-btn.active {
color: var(--accent);
}
.tab-btn.active::after {
transform: scaleX(1);
}
.tab-label { display: flex; align-items: center; gap: 8px; }
.tab-icon {
width: 16px; height: 16px; border-radius: 0;
display: inline-flex; align-items: center; justify-content: center;
font-size: 13px;
}
/* ============================================
KPI GRID — IBM cards, brutal
============================================ */
.kpi-grid {
display: grid; grid-template-columns: repeat(6, 1fr);
gap: 1px; padding: 24px 32px; max-width: 1280px; margin: 0 auto;
background: var(--border); border: 1px solid var(--border);
border-radius: 4px; overflow: hidden;
}
.kpi {
background: var(--surface);
padding: 20px 24px;
position: relative;
transition: background 0.15s;
opacity: 0; transform: translateY(8px);
}
.kpi.visible { opacity: 1; transform: translateY(0); }
.kpi:hover { background: var(--surface-hover); }
.kpi-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.kpi-name {
font-size: 11px; font-weight: 600;
text-transform: uppercase; letter-spacing: 0.06em;
color: var(--text-secondary);
}
.kpi-badge {
font-family: var(--mono); font-size: 10px; font-weight: 500;
padding: 2px 6px; border-radius: 0;
text-transform: uppercase; letter-spacing: 0.05em;
}
.kpi-badge.up { color: var(--green); background: var(--green-dim); }
.kpi-badge.down { color: var(--red); background: var(--red-dim); }
.kpi-badge.neutral { color: var(--accent); background: var(--accent-dim); }
.kpi-value {
font-family: var(--mono);
font-size: 24px; font-weight: 600;
letter-spacing: -0.5px;
line-height: 1.1;
color: var(--text-primary);
margin-bottom: 8px;
}
.kpi-context { font-size: 11px; color: var(--text-tertiary); }
/* ============================================
CHART PANELS — IBM structure
============================================ */
.charts-wrap {
max-width: 1280px; margin: 0 auto;
padding: 0 32px 32px;
display: grid; gap: 1px;
background: var(--border);
border: 1px solid var(--border);
border-radius: 4px; overflow: hidden;
}
.charts-row-main { grid-template-columns: 2fr 1fr; }
.charts-row-sub { grid-template-columns: 1fr 1fr 1fr; }
.chart-panel {
background: var(--surface);
padding: 24px;
min-height: 280px;
}
.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 inside chart panel */
.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; }
.metric-item:first-child { padding-top: 0; }
.metric-item:last-child { padding-bottom: 0; }
.m-label { font-size: 12px; color: var(--text-secondary); flex: 1; font-weight: 400; }
.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); border-radius: 0; overflow: hidden; }
.m-fill { height: 100%; width: 0; transition: width 0.8s var(--ease-standard); }
.m-fill.green { background: var(--green); }
.m-fill.red { background: var(--red); }
.m-fill.yellow { background: var(--yellow); }
.m-fill.blue { background: var(--accent); }
/* ============================================
RESPONSIVE
============================================ */
@media(max-width: 1024px) {
.kpi-grid { grid-template-columns: repeat(3, 1fr); }
.charts-row-main, .charts-row-sub { grid-template-columns: 1fr; }
.page-title { font-size: 22px; }
}
@media(max-width: 640px) {
.kpi-grid { grid-template-columns: repeat(2, 1fr); }
.topbar { padding: 0 16px; }
.page-header, .tabs-section, .kpi-grid, .charts-wrap { padding-left: 16px; padding-right: 16px; }
.tab-btn { padding: 12px 14px; font-size: 13px; }
}
</style>
</head>
<body>
<!-- TOP BAR -->
<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>
<!-- PAGE HEADER -->
<div class="page-header">
<h1 class="page-title">Аналитика бизнеса</h1>
<p class="page-desc">Ключевые показатели эффективности в реальном времени</p>
</div>
<!-- IBM-STYLE TABS -->
<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>
<!-- KPI GRID -->
<div class="kpi-grid" id="kpi-grid"></div>
<!-- CHARTS -->
<div class="charts-wrap">
<div class="charts-row-main" id="charts-main"></div>
<div class="charts-row-sub" id="charts-sub"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
<script>
const D = {
ecom: {
kpis: [
{ n:'Выручка', v:'4.2М ₽', d:'+12%', t:'up', p:'vs прошл. мес' },
{ n:'AOV', v:'4 820 ₽', d:'+3%', t:'up', p:'ср. чек' },
{ n:'CVR', v:'3.8%', d:'+0.5%', t:'up', p:'клики→покупка' },
{ n:'CAC', v:'680 ₽', d:'−8%', t:'down', p:'стоим. клиента' },
{ n:'CLV', v:'12 400 ₽', d:'+18%', t:'up', p:'за 6 мес' },
{ n:'ROAS', v:'4.2×', d:'+15%', t:'up', p:'возврат рекламы' },
],
mc: { title:'Выручка и расходы на рекламу', sub:'12 месяцев · выручка (зелёный) vs маркетинг-бюджет (пунктир)', type:'dual',
labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'],
rev:[2.8,3.1,2.9,3.4,3.6,3.2,3.8,4.0,3.9,4.2,3.7,4.2],
ads:[0.6,0.7,0.65,0.72,0.68,0.75,0.7,0.65,0.6,0.58,0.62,0.55],
},
sc: { title:'ROAS по каналам', sub:'Возврат инвестиций в каждый канал', type:'bar-h',
data:[
{n:'SEO',v:520},{n:'Email',v:380},{n:'Прямой',v:280},
{n:'Контекст',v:150},{n:'Соцсети',v:120},{n:'Рефералы',v:95},
],
},
c1: { title:'Конверсия воронки', sub:'Просмотр → Корзина → Оплата → Покупка',
data:[
{n:'Посетители',v:45000},{n:'Корзина',v:8100},
{n:'Оформление',v:4950},{n:'Оплата',v:2723},
],
},
c2: { title:'Топ-5 товаров', sub:'По выручке за месяц',
labels:['Товар А','Товар Б','Товар В','Товар Г','Товар Д'],
data:[120,98,76,54,42],
},
metrics: [
{l:'Выручка (план)',v:'4.2М',p:93,c:'green'},
{l:'CVR (план)',v:'3.8%',p:95,c:'green'},
{l:'ROAS (план)',v:'4.2×',p:105,c:'green'},
{l:'CAC (план)',v:'680₽',p:85,c:'yellow'},
{l:'CLV (план)',v:'12.4К',p:104,c:'green'},
],
},
services: {
kpis: [
{ n:'Пайплайн', v:'12.8М ₽', d:'+18%', t:'up', p:'открытых сделок' },
{ n:'Win Rate', v:'24%', d:'+3%', t:'up', p:'закрыто/всего' },
{ n:'Sales Cycle', v:'32 дн', d:'−5%', t:'down', p:'длительность' },
{ n:'Pipeline Cov.', v:'4.1×', d:'+0.6', t:'up', p:'покрытие квоты' },
{ n:'CPL', v:'2 400 ₽', d:'−12%', t:'down', p:'стоимость лида' },
{ n:'ROMI', v:'245%', d:'+35%', t:'up', p:'маркетинг ROI' },
],
mc: { title:'Пайплайн и закрытые сделки', sub:'12 месяцев · пайплайн (синий) vs закрытие (зелёный)', type:'dual',
labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'],
rev:[6.2,7.0,7.5,8.1,8.8,9.2,9.8,10.5,11.0,11.5,12.2,12.8],
ads:[1.8,2.1,2.0,2.3,2.5,2.2,2.8,3.0,2.7,3.1,2.6,3.2],
},
sc: { title:'ROMI по услугам', sub:'Возврат инвестиций в каждую услугу', type:'bar-h',
data:[
{n:'SEO-контент',v:420},{n:'Рефералы',v:380},{n:'Консалтинг',v:290},
{n:'Дизайн',v:240},{n:'Таргет',v:160},{n:'Холодные',v:85},
],
},
c1: { title:'Воронка продаж', sub:'Лид → Квалификация → Предложение → Сделка',
data:[
{n:'Лиды',v:320},{n:'Квалиф.',v:224},
{n:'Предложение',v:128},{n:'Сделка',v:77},
],
},
c2: { title:'Quota Attainment', sub:'Выполнение квоты менеджерами',
labels:['Иванов','Петров','Сидоров','Козлов','Морозов'],
data:[112,95,88,105,78],
},
metrics: [
{l:'Пайплайн (план)',v:'12.8М',p:85,c:'blue'},
{l:'Win Rate (план)',v:'24%',p:96,c:'green'},
{l:'Sales Cycle (план)',v:'32д',p:90,c:'green'},
{l:'Pipeline Cov. (план)',v:'4.1×',p:103,c:'green'},
{l:'ROMI (план)',v:'245%',p:123,c:'green'},
],
},
production: {
kpis: [
{ n:'Выпуск', v:'15 420 шт', d:'+8%', t:'up', p:'к плану' },
{ n:'OEE', v:'78%', d:'+4%', t:'up', p:'загрузка мощностей' },
{ n:'Брак', v:'2.1%', d:'−0.5%', t:'down', p:'дефекты' },
{ n:'Себест.', v:'3 200 ₽', d:'−3%', t:'down', p:'за единицу' },
{ n:'Оборачив.', v:'5.2×', d:'+0.3', t:'up', p:'запасов в год' },
{ n:'ROMI рекламы', v:'310%', d:'+45%', t:'up', p:'маркетинг ROI' },
],
mc: { title:'План / факт выпуска', sub:'12 месяцев · план (пунктир) vs фактический выпуск (сплошная)', type:'bar-cmp',
labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'],
plan:[12,13,12.5,14,14.5,13,15,15.5,15,15.5,14,16],
fact:[11.5,12.8,12.2,13.8,14.2,12.5,14.8,15.2,14.7,15.0,13.8,15.4],
},
sc: { title:'ROMI по каналам', sub:'Возврат инвестиций в привлечение заказов', type:'bar-h',
data:[
{n:'SEO',v:380},{n:'Прямые',v:310},{n:'Контекст',v:210},
{n:'Каталоги',v:170},{n:'Таргет',v:140},{n:'Выставки',v:110},
],
},
c1: { title:'Качество (воронка)', sub:'Выпуск → Контроль → Брак → Доработка',
data:[
{n:'Выпуск',v:15420},{n:'Контроль',v:15200},
{n:'Брак',v:324},{n:'Доработка',v:180},
],
},
c2: { title:'Структура затрат', sub:'Доля в себестоимости',
labels:['Сырьё','Зарплата','Энергия','Логистика','Прочее'],
data:[45,25,12,10,8],
},
metrics: [
{l:'Выпуск (план)',v:'15.4К',p:96,c:'green'},
{l:'OEE (план)',v:'78%',p:92,c:'yellow'},
{l:'Брак (план)',v:'2.1%',p:105,c:'green'},
{l:'ROMI (план)',v:'310%',p:111,c:'green'},
{l:'Себест. (план)',v:'3.2К',p:94,c:'blue'},
],
},
};
let charts = {};
function seg(key) {
const s = D[key];
// Tabs
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.seg === key));
// KPIs
const g = document.getElementById('kpi-grid');
g.innerHTML = s.kpis.map((d,i) => {
const badgeClass = d.t;
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 ${badgeClass}">${arrow} ${d.d}</span>
</div>
<div class="kpi-value">${d.v}</div>
<div class="kpi-context">${d.p}</div>
</div>`;
}).join('');
requestAnimationFrame(() => g.querySelectorAll('.kpi').forEach(c => c.classList.add('visible')));
// Main chart area
const mcDom = document.getElementById('charts-main');
const mc = s.mc;
if (!charts.mc) charts.mc = echarts.init(document.createElement('div'));
mcDom.innerHTML = `<div class="chart-panel" id="cp-main">
<div class="chart-header">
<div class="chart-title">${mc.title}</div>
<div class="chart-subtitle">${mc.sub}</div>
</div>
<div class="chart-canvas" id="mc-canvas"></div>
</div>
<div class="chart-panel" id="cp-side">
<div class="chart-header">
<div class="chart-title">${s.sc.title}</div>
<div class="chart-subtitle">${s.sc.sub}</div>
</div>
<div class="chart-canvas" id="sc-canvas"></div>
</div>`;
// Main chart
const mcEl = document.getElementById('mc-canvas');
charts.mc.dispose();
charts.mc = echarts.init(mcEl);
let mcOpt;
if (mc.type === 'dual') {
mcOpt = {
tooltip: { trigger: 'axis', backgroundColor: '#161616', borderColor: '#262626', textStyle: { color: '#f4f4f4', fontSize: 12, fontFamily: 'IBM Plex Mono' } },
legend: { data: ['Доход', 'Расход'], textStyle: { color: '#6f6f6f', fontSize: 11, fontFamily: 'IBM Plex Sans' }, top: 0, itemWidth: 12, itemHeight: 2 },
grid: { left: 48, right: 20, top: 32, bottom: 28 },
xAxis: {
type: 'category', data: mc.labels,
axisLine: { lineStyle: { color: '#262626' } },
axisLabel: { color: '#6f6f6f', fontSize: 11, fontFamily: 'IBM Plex Mono' },
axisTick: { show: false },
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: '#1f1f1f' } },
axisLabel: { color: '#6f6f6f', fontSize: 11, fontFamily: 'IBM Plex Mono', formatter: '{value}М' },
},
series: [
{
name: 'Доход', type: 'line', data: 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: mc.ads, smooth: 0.4, symbol: 'none',
lineStyle: { width: 1.5, color: '#0f6fff', type: 'dashed' },
},
],
animationDuration: 500,
animationEasing: 'cubicOut',
};
} else {
mcOpt = {
tooltip: { trigger: 'axis', backgroundColor: '#161616', borderColor: '#262626', textStyle: { color: '#f4f4f4', fontSize: 12, fontFamily: 'IBM Plex Mono' } },
legend: { data: ['План', 'Факт'], textStyle: { color: '#6f6f6f', fontSize: 11, fontFamily: 'IBM Plex Sans' }, top: 0, itemWidth: 12, itemHeight: 14 },
grid: { left: 48, right: 20, top: 32, bottom: 28 },
xAxis: {
type: 'category', data: mc.labels,
axisLine: { lineStyle: { color: '#262626' } },
axisLabel: { color: '#6f6f6f', fontSize: 11, fontFamily: 'IBM Plex Mono' },
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: '#1f1f1f' } },
axisLabel: { color: '#6f6f6f', fontSize: 11, fontFamily: 'IBM Plex Mono', formatter: '{value}К' },
},
series: [
{ name: 'План', type: 'bar', data: mc.plan, barWidth: '30%', itemStyle: { color: 'rgba(15,111,255,0.1)' } },
{ name: 'Факт', type: 'bar', data: mc.fact, barWidth: '30%', itemStyle: { color: '#0f6fff' } },
],
animationDuration: 500,
animationEasing: 'cubicOut',
};
}
charts.mc.setOption(mcOpt, true);
// Side chart
const sc = s.sc;
const scEl = document.getElementById('sc-canvas');
if (!charts.sc) charts.sc = echarts.init(scEl);
charts.sc.dispose();
charts.sc = echarts.init(scEl);
const scData = sc.data.reverse();
const scColors = ['#0f6fff', '#3d8eff', '#16a34a', '#06b6d4', '#a855f7', '#eac518'];
const scOpt = {
tooltip: { trigger: 'axis', backgroundColor: '#161616', borderColor: '#262626', textStyle: { color: '#f4f4f4', fontSize: 11 } },
grid: { left: 90, right: 28, top: 8, bottom: 8 },
xAxis: {
type: 'value',
splitLine: { lineStyle: { color: '#1f1f1f' } },
axisLabel: { color: '#6f6f6f', fontSize: 10, fontFamily: 'IBM Plex Mono', formatter: '{value}%' },
},
yAxis: {
type: 'category', data: scData.map(d => d.n),
axisLine: { lineStyle: { color: '#262626' } },
axisLabel: { color: '#a6a6a6', fontSize: 11, fontFamily: 'IBM Plex Sans' },
},
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,
};
charts.sc.setOption(scOpt, true);
// Sub row charts
const subEl = document.getElementById('charts-sub');
const c1 = s.c1;
const c2 = s.c2;
// Funnel chart
let funnelHtml = `<div class="chart-panel">
<div class="chart-header">
<div class="chart-title">${c1.title}</div>
<div class="chart-subtitle">${c1.sub}</div>
</div>
<div class="chart-canvas small" id="c1-canvas"></div>
</div>`;
// Second chart
let secondHtml;
if (c2.labels) {
const isPie = c2.data.length && c2.data.reduce((a, b) => a + b, 0) <= 100;
const pc = ['#0f6fff', '#16a34a', '#eac518', '#a855f7', '#e21d1d'];
if (isPie) {
secondHtml = `<div class="chart-panel">
<div class="chart-header">
<div class="chart-title">${c2.title}</div>
<div class="chart-subtitle">${c2.sub}</div>
</div>
<div class="chart-canvas small" id="c2-canvas"></div>
</div>`;
} else {
secondHtml = `<div class="chart-panel">
<div class="chart-header">
<div class="chart-title">${c2.title}</div>
<div class="chart-subtitle">${c2.sub}</div>
</div>
<div class="chart-canvas small" id="c2-canvas"></div>
</div>`;
}
}
// Metrics panel
const metricsHtml = `<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>`;
subEl.innerHTML = funnelHtml + secondHtml + metricsHtml;
// Funnel
const fc = ['#0f6fff', '#3d8eff', '#16a34a', '#e21d1d'];
const c1Dom = document.getElementById('c1-canvas');
if (!charts.c1) charts.c1 = echarts.init(c1Dom);
charts.c1.dispose();
charts.c1 = echarts.init(c1Dom);
charts.c1.setOption({
tooltip: { trigger: 'item', backgroundColor: '#161616', borderColor: '#262626', textStyle: { color: '#f4f4f4', fontSize: 11 } },
series: [{
type: 'funnel', left: '8%', bottom: 5, width: '84%', min: 0, max: c1.data[0].v,
minSize: '0%', maxSize: '100%', sort: 'descending', gap: 2,
label: { show: true, position: 'inside', color: '#f4f4f4', fontSize: 11, fontFamily: 'IBM Plex Mono', formatter: '{b}\n{d}%' },
labelLine: { show: false },
itemStyle: { borderColor: '#161616', borderWidth: 2 },
data: c1.data.map((d, i) => ({ name: d.n, value: d.v, itemStyle: { color: fc[i % fc.length] } })),
}],
animationDuration: 500,
}, true);
// Second chart
const c2Dom = document.getElementById('c2-canvas');
if (!charts.c2) charts.c2 = echarts.init(c2Dom);
charts.c2.dispose();
charts.c2 = echarts.init(c2Dom);
if (c2.labels) {
const isPie = c2.data.length && c2.data.reduce((a, b) => a + b, 0) <= 100;
const pc = ['#0f6fff', '#16a34a', '#eac518', '#a855f7', '#e21d1d'];
if (isPie) {
charts.c2.setOption({
tooltip: { trigger: 'item', backgroundColor: '#161616', borderColor: '#262626', textStyle: { color: '#f4f4f4', fontSize: 11 } },
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,
}, true);
} else {
charts.c2.setOption({
tooltip: { trigger: 'axis', backgroundColor: '#161616', borderColor: '#262626', textStyle: { color: '#f4f4f4', fontSize: 11 } },
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, fontFamily: 'IBM Plex Sans' },
},
series: [{
type: 'bar', data: c2.data.map(v => ({
value: v,
itemStyle: { color: v >= 100 ? '#16a34a' : '#eac518' }
})),
barWidth: 24,
}],
animationDuration: 500,
}, true);
}
}
// 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.getElementById('charts-wrap'));
});
</script>
</body>
</html>