Files
roi24/dashboard-mockup.html
T

1081 lines
31 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ROI24 — Интерактивный дашборд</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
/* ============================================
DESIGN TOKENS
============================================ */
:root {
--color-bg: #0a0a0a;
--color-surface: #111111;
--color-surface-hover: #1a1a1a;
--color-border: rgba(255,255,255,0.06);
--color-border-hover: rgba(255,255,255,0.12);
--color-text-primary: #f5f5f5;
--color-text-secondary: #a1a1aa;
--color-text-muted: #52525b;
--accent: #22c55e;
--accent-hover: #16a34a;
--accent-dim: rgba(34,197,94,0.12);
--accent-secondary: #3b82f6;
--accent-secondary-dim: rgba(59,130,246,0.12);
--warning: #f59e0b;
--warning-dim: rgba(245,158,11,0.12);
--error: #ef4444;
--error-dim: rgba(239,68,68,0.12);
--font-primary: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
--ease-out: cubic-bezier(0.4, 0, 0.2, 1);
--duration-normal: 300ms;
}
/* ============================================
RESET & BASE
============================================ */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: var(--font-primary);
background: var(--color-bg);
color: var(--color-text-primary);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
min-height: 100vh;
}
/* Noise texture overlay */
body::before {
content: '';
position: fixed;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.03'/%3E%3C/svg%3E");
pointer-events: none;
z-index: 0;
}
/* ============================================
HEADER BAR
============================================ */
.dash-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 24px;
background: rgba(17,17,17,0.8);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--color-border);
position: sticky;
top: 0;
z-index: 100;
}
.dash-logo {
font-family: var(--font-mono);
font-weight: 700;
font-size: 16px;
letter-spacing: -0.5px;
display: flex;
align-items: center;
gap: 8px;
}
.dash-logo-dot {
width: 8px;
height: 8px;
background: var(--accent);
border-radius: 50%;
animation: pulse-dot 2s ease-in-out infinite;
}
@keyframes pulse-dot {
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(34,197,94,0.4); }
50% { opacity: 0.8; box-shadow: 0 0 0 6px rgba(34,197,94,0); }
}
.dash-status {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--color-text-secondary);
}
.dash-status-dot {
width: 6px;
height: 6px;
background: var(--accent);
border-radius: 50%;
animation: pulse-dot 2s ease-in-out infinite;
}
.dash-time {
font-family: var(--font-mono);
font-size: 11px;
color: var(--color-text-muted);
}
/* ============================================
SEGMENT SWITCHER
============================================ */
.segment-switcher {
display: flex;
gap: 4px;
padding: 6px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 12px;
margin: 24px auto;
max-width: 600px;
position: relative;
z-index: 1;
}
.segment-btn {
flex: 1;
padding: 10px 16px;
border: none;
background: transparent;
color: var(--color-text-secondary);
font-family: var(--font-primary);
font-size: 13px;
font-weight: 500;
border-radius: 8px;
cursor: pointer;
transition: all var(--duration-normal) var(--ease-out);
position: relative;
z-index: 1;
white-space: nowrap;
}
.segment-btn:hover {
color: var(--color-text-primary);
}
.segment-btn.active {
background: var(--accent-dim);
color: var(--accent);
}
.segment-btn .seg-icon {
margin-right: 6px;
font-size: 14px;
}
/* ============================================
KPI CARDS
============================================ */
.kpi-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
padding: 0 24px;
max-width: 1200px;
margin: 0 auto;
}
.kpi-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 12px;
padding: 16px;
transition: all var(--duration-normal) var(--ease-out);
position: relative;
overflow: hidden;
}
.kpi-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 3px;
height: 100%;
opacity: 0;
transition: opacity var(--duration-normal) var(--ease-out);
}
.kpi-card:hover {
border-color: var(--color-border-hover);
transform: translateY(-1px);
}
.kpi-card:hover::before {
opacity: 1;
}
.kpi-card.positive::before { background: var(--accent); }
.kpi-card.negative::before { background: var(--error); }
.kpi-card.neutral::before { background: var(--accent-secondary); }
.kpi-label {
font-size: 11px;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 600;
margin-bottom: 8px;
}
.kpi-value {
font-family: var(--font-mono);
font-size: 24px;
font-weight: 700;
margin-bottom: 6px;
line-height: 1.1;
}
.kpi-trend {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
font-weight: 600;
padding: 2px 8px;
border-radius: 6px;
font-family: var(--font-mono);
}
.kpi-trend.up {
color: var(--accent);
background: var(--accent-dim);
}
.kpi-trend.down {
color: var(--error);
background: var(--error-dim);
}
.kpi-trend.stable {
color: var(--accent-secondary);
background: var(--accent-secondary-dim);
}
/* ============================================
CHART SECTIONS
============================================ */
.charts-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 12px;
padding: 12px 24px;
max-width: 1200px;
margin: 0 auto;
}
.chart-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 12px;
padding: 20px;
transition: all var(--duration-normal) var(--ease-out);
}
.chart-card:hover {
border-color: var(--color-border-hover);
}
.chart-title {
font-size: 13px;
font-weight: 600;
margin-bottom: 4px;
color: var(--color-text-primary);
}
.chart-subtitle {
font-size: 11px;
color: var(--color-text-muted);
margin-bottom: 16px;
}
.chart-container {
width: 100%;
height: 260px;
}
/* ============================================
BOTTOM ROW
============================================ */
.bottom-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 12px;
padding: 12px 24px;
max-width: 1200px;
margin: 0 auto;
}
.bottom-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 12px;
padding: 20px;
transition: all var(--duration-normal) var(--ease-out);
}
.bottom-card:hover {
border-color: var(--color-border-hover);
}
.bottom-card .chart-container {
height: 200px;
}
/* ============================================
TOP CHANNELS TABLE
============================================ */
.top-channels {
margin-top: 12px;
}
.channel-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid var(--color-border);
font-size: 12px;
}
.channel-row:last-child {
border-bottom: none;
}
.channel-name {
color: var(--color-text-secondary);
flex: 1;
}
.channel-value {
font-family: var(--font-mono);
font-weight: 600;
color: var(--color-text-primary);
margin-right: 12px;
}
.channel-bar-wrap {
width: 80px;
height: 4px;
background: var(--color-border);
border-radius: 2px;
overflow: hidden;
}
.channel-bar {
height: 100%;
border-radius: 2px;
background: var(--accent);
transition: width 0.8s var(--ease-out);
}
/* ============================================
SECTION TITLE (outside mockup)
============================================ */
.dash-section-title {
text-align: center;
padding: 48px 24px 24px;
}
.dash-section-title h2 {
font-size: clamp(1.8rem, 4vw, 2.5rem);
font-weight: 800;
margin-bottom: 8px;
letter-spacing: -0.5px;
}
.dash-section-title p {
color: var(--color-text-secondary);
font-size: 15px;
}
/* ============================================
RESPONSIVE
============================================ */
@media (max-width: 768px) {
.kpi-grid {
grid-template-columns: repeat(2, 1fr);
}
.charts-grid {
grid-template-columns: 1fr;
}
.bottom-grid {
grid-template-columns: 1fr;
}
.dash-header {
padding: 10px 16px;
}
.kpi-value {
font-size: 20px;
}
.segment-switcher {
margin: 16px;
}
.segment-btn {
padding: 8px 10px;
font-size: 12px;
}
.segment-btn .seg-icon {
display: none;
}
}
@media (max-width: 480px) {
.kpi-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<!-- Section Title -->
<div class="dash-section-title">
<h2>Все ключевые цифры — в одном месте</h2>
<p>Выберите тип бизнеса, чтобы увидеть, как выглядит дашборд под ваш бизнес</p>
</div>
<!-- Segment Switcher -->
<div class="segment-switcher" id="segment-switcher">
<button class="segment-btn active" data-segment="ecommerce" onclick="switchSegment('ecommerce')">
<span class="seg-icon">🛒</span>Интернет-магазин
</button>
<button class="segment-btn" data-segment="services" onclick="switchSegment('services')">
<span class="seg-icon">💼</span>Услуги
</button>
<button class="segment-btn" data-segment="production" onclick="switchSegment('production')">
<span class="seg-icon">🏭</span>Производство
</button>
</div>
<!-- Dashboard Mockup -->
<div id="dashboard-mockup">
<!-- Header -->
<div class="dash-header">
<div class="dash-logo">
<span class="dash-logo-dot"></span>
ROI24
</div>
<div class="dash-status">
<span class="dash-status-dot"></span>
<span>Live</span>
<span style="margin: 0 8px; color: var(--color-border);">|</span>
<span class="dash-time" id="dash-time"></span>
</div>
</div>
<!-- KPI Cards -->
<div class="kpi-grid" id="kpi-grid">
<!-- Populated by JS -->
</div>
<!-- Charts -->
<div class="charts-grid">
<div class="chart-card">
<div class="chart-title" id="main-chart-title">Выручка</div>
<div class="chart-subtitle" id="main-chart-subtitle">12 месяцев с трендом</div>
<div class="chart-container" id="main-chart"></div>
</div>
<div class="chart-card">
<div class="chart-title" id="side-chart-title">Ключевые метрики</div>
<div class="chart-subtitle">Текущий период</div>
<div class="chart-container" id="side-chart"></div>
</div>
</div>
<!-- Bottom Row -->
<div class="bottom-grid">
<div class="bottom-card">
<div class="chart-title">Каналы привлечения</div>
<div class="chart-subtitle">Доля в общем объёме</div>
<div class="chart-container" id="bottom-chart-1"></div>
</div>
<div class="bottom-card">
<div class="chart-title">Воронка</div>
<div class="chart-subtitle">Этапы конверсии</div>
<div class="chart-container" id="bottom-chart-2"></div>
</div>
<div class="bottom-card">
<div class="chart-title">Топ-5 метрик</div>
<div class="chart-subtitle">Выполнение плана</div>
<div class="top-channels" id="top-metrics"></div>
</div>
</div>
</div>
<!-- ECharts CDN -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
<script>
// ============================================
// DATA FOR EACH SEGMENT
// ============================================
const segments = {
ecommerce: {
kpis: [
{ label: 'Выручка', value: '4.2М ₽', trend: '+12%', direction: 'up' },
{ label: 'Конверсия', value: '3.8%', trend: '+0.5%', direction: 'up' },
{ label: 'Ср. чек', value: '4 820 ₽', trend: '+3%', direction: 'up' },
{ label: 'Возвраты', value: '4.2%', trend: '-0.8%', direction: 'up' }, // negative is good for returns
],
mainChart: {
title: 'Выручка',
subtitle: '12 месяцев с трендом',
type: 'line',
data: [2.8, 3.1, 2.9, 3.4, 3.6, 3.2, 3.8, 4.0, 3.9, 4.2, 3.7, 4.2],
labels: ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек'],
},
sideChart: {
title: 'Маржинальность по категориям',
type: 'bar-vertical',
data: [
{ name: 'Электроника', value: 18 },
{ name: 'Одежда', value: 35 },
{ name: 'Дом', value: 28 },
{ name: 'Спорт', value: 22 },
{ name: 'Красота', value: 42 },
{ name: 'Детский', value: 30 },
],
},
bottomCharts: {
channels: {
title: 'Каналы привлечения',
type: 'pie',
data: [
{ name: 'SEO', value: 35 },
{ name: 'Контекст', value: 28 },
{ name: 'Прямой', value: 22 },
{ name: 'Email', value: 8 },
{ name: 'Соцсети', value: 7 },
],
},
funnel: {
title: 'Воронка продаж',
data: [
{ name: 'Посетители', value: 45000 },
{ name: 'Корзина', value: 8100 },
{ name: 'Оформление', value: 4950 },
{ name: 'Оплата', value: 2723 },
],
},
topMetrics: [
{ name: 'Выручка (план)', value: 4.2, max: 4.5, pct: 93 },
{ name: 'Конверсия (план)', value: 3.8, max: 4.0, pct: 95 },
{ name: 'Ср. чек (план)', value: 4820, max: 5000, pct: 96 },
{ name: 'Повторные (план)', value: 32, max: 35, pct: 91 },
{ name: 'NPS (план)', value: 68, max: 75, pct: 91 },
],
},
},
services: {
kpis: [
{ label: 'Пайплайн', value: '12.8М ₽', trend: '+18%', direction: 'up' },
{ label: 'Конверсия', value: '24%', trend: '+3%', direction: 'up' },
{ label: 'Маржа проекта', value: '34%', trend: '+2%', direction: 'up' },
{ label: 'Загрузка', value: '87%', trend: '0%', direction: 'stable' },
],
mainChart: {
title: 'Пайплайн',
subtitle: '12 месяцев, млн ₽',
type: 'area',
data: [6.2, 7.0, 7.5, 8.1, 8.8, 9.2, 9.8, 10.5, 11.0, 11.5, 12.2, 12.8],
labels: ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек'],
},
sideChart: {
title: 'Маржинальность по проектам',
type: 'bar-vertical',
data: [
{ name: 'Консалтинг', value: 42 },
{ name: 'Дизайн', value: 38 },
{ name: 'Разработка', value: 31 },
{ name: 'Маркетинг', value: 35 },
{ name: 'SEO', value: 45 },
{ name: 'Поддержка', value: 55 },
],
},
bottomCharts: {
channels: {
title: 'Источники клиентов',
type: 'pie',
data: [
{ name: 'Рефералы', value: 38 },
{ name: 'Сайт', value: 27 },
{ name: 'Telegram', value: 18 },
{ name: 'Конференции', value: 10 },
{ name: 'Холодные', value: 7 },
],
},
funnel: {
title: 'Воронка продаж',
data: [
{ name: 'Лиды', value: 320 },
{ name: 'Квалификация', value: 224 },
{ name: 'Предложение', value: 128 },
{ name: 'Сделка', value: 77 },
],
},
topMetrics: [
{ name: 'Пайплайн (план)', value: 12.8, max: 15.0, pct: 85 },
{ name: 'Конверсия (план)', value: 24, max: 25, pct: 96 },
{ name: 'Маржа (план)', value: 34, max: 35, pct: 97 },
{ name: 'Загрузка (план)', value: 87, max: 90, pct: 97 },
{ name: 'NPS (план)', value: 72, max: 75, pct: 96 },
],
},
},
production: {
kpis: [
{ label: 'Объём выпуска', value: '15 420 шт', trend: '+8%', direction: 'up' },
{ label: 'OEE', value: '78%', trend: '+4%', direction: 'up' },
{ label: 'Брак', value: '2.1%', trend: '-0.5%', direction: 'up' },
{ label: 'Запасы', value: '18.5М ₽', trend: '-5%', direction: 'down' },
],
mainChart: {
title: 'План/факт выпуска',
subtitle: '12 месяцев, тыс. шт',
type: 'bar-compare',
planData: [12, 13, 12.5, 14, 14.5, 13, 15, 15.5, 15, 15.5, 14, 16],
factData: [11.5, 12.8, 12.2, 13.8, 14.2, 12.5, 14.8, 15.2, 14.7, 15.0, 13.8, 15.4],
labels: ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек'],
},
sideChart: {
title: 'Себестоимость по цехам',
type: 'bar-horizontal',
data: [
{ name: 'Цех №1', value: 2.4 },
{ name: 'Цех №2', value: 3.1 },
{ name: 'Цех №3', value: 1.8 },
{ name: 'Цех №4', value: 2.7 },
{ name: 'Цех №5', value: 1.5 },
],
},
bottomCharts: {
channels: {
title: 'Структура затрат',
type: 'pie',
data: [
{ name: 'Сырьё', value: 45 },
{ name: 'Зарплата', value: 25 },
{ name: 'Энергия', value: 12 },
{ name: 'Логистика', value: 10 },
{ name: 'Прочее', value: 8 },
],
},
funnel: {
title: 'Качество (воронка)',
data: [
{ name: 'Выпуск', value: 15420 },
{ name: 'Контроль', value: 15200 },
{ name: 'Брак', value: 324 },
{ name: 'На доработку', value: 180 },
],
},
topMetrics: [
{ name: 'Выпуск (план)', value: 15.4, max: 16.0, pct: 96 },
{ name: 'OEE (план)', value: 78, max: 85, pct: 92 },
{ name: 'Брак (план)', value: 2.1, max: 2.0, pct: 105 },
{ name: 'Запасы (план)', value: 18.5, max: 20.0, pct: 93 },
{ name: 'OEE (план)', value: 78, max: 80, pct: 98 },
],
},
},
};
// ============================================
// STATE
// ============================================
let currentSegment = 'ecommerce';
let mainChartInstance = null;
let sideChartInstance = null;
let bottomChart1Instance = null;
let bottomChart2Instance = null;
// ============================================
// RENDER FUNCTIONS
// ============================================
function renderKPIs(segment) {
const grid = document.getElementById('kpi-grid');
const kpis = segment.kpis;
grid.innerHTML = kpis.map((kpi, i) => {
const trendClass = kpi.direction === 'up' ? 'up' : (kpi.direction === 'down' ? 'down' : 'stable');
const cardClass = kpi.direction === 'up' ? 'positive' : (kpi.direction === 'down' ? 'negative' : 'neutral');
const arrow = kpi.direction === 'up' ? '↑' : (kpi.direction === 'down' ? '↓' : '→');
return `
<div class="kpi-card ${cardClass}" style="opacity:0; transform: translateY(20px);">
<div class="kpi-label">${kpi.label}</div>
<div class="kpi-value">${kpi.value}</div>
<span class="kpi-trend ${trendClass}">${arrow} ${kpi.trend}</span>
</div>
`;
}).join('');
// Animate in
requestAnimationFrame(() => {
const cards = grid.querySelectorAll('.kpi-card');
cards.forEach((card, i) => {
setTimeout(() => {
card.style.transition = 'opacity 0.5s var(--ease-out), transform 0.5s var(--ease-out)';
card.style.opacity = '1';
card.style.transform = 'translateY(0)';
}, i * 80);
});
});
}
function renderMainChart(segment) {
const cfg = segment.mainChart;
const dom = document.getElementById('main-chart');
if (!mainChartInstance) {
mainChartInstance = echarts.init(dom);
}
document.getElementById('main-chart-title').textContent = cfg.title;
document.getElementById('main-chart-subtitle').textContent = cfg.subtitle;
let option;
if (cfg.type === 'line' || cfg.type === 'area') {
option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(17,17,17,0.95)',
borderColor: 'rgba(255,255,255,0.1)',
textStyle: { color: '#f5f5f5', fontSize: 12 },
},
grid: { left: 40, right: 20, top: 10, bottom: 30 },
xAxis: {
type: 'category',
data: cfg.labels,
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.08)' } },
axisLabel: { color: '#52525b', fontSize: 10 },
axisTick: { show: false },
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.04)' } },
axisLabel: { color: '#52525b', fontSize: 10 },
},
series: [{
data: cfg.data,
type: cfg.type === 'area' ? 'line' : 'line',
smooth: 0.4,
symbol: 'none',
lineStyle: { width: 2.5, color: '#22c55e' },
areaStyle: cfg.type === 'area' ? {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(34,197,94,0.25)' },
{ offset: 1, color: 'rgba(34,197,94,0.01)' },
])
} : undefined,
}],
animationDuration: 600,
animationEasing: 'cubicOut',
};
} else if (cfg.type === 'bar-compare') {
option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(17,17,17,0.95)',
borderColor: 'rgba(255,255,255,0.1)',
textStyle: { color: '#f5f5f5', fontSize: 12 },
},
legend: {
data: ['План', 'Факт'],
textStyle: { color: '#52525b', fontSize: 10 },
top: 0,
right: 0,
},
grid: { left: 40, right: 20, top: 30, bottom: 30 },
xAxis: {
type: 'category',
data: cfg.labels,
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.08)' } },
axisLabel: { color: '#52525b', fontSize: 10 },
axisTick: { show: false },
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.04)' } },
axisLabel: { color: '#52525b', fontSize: 10 },
},
series: [
{
name: 'План',
data: cfg.planData,
type: 'bar',
barWidth: '30%',
itemStyle: { color: 'rgba(59,130,246,0.2)', borderRadius: [4, 4, 0, 0] },
},
{
name: 'Факт',
data: cfg.factData,
type: 'bar',
barWidth: '30%',
itemStyle: { color: '#3b82f6', borderRadius: [4, 4, 0, 0] },
},
],
animationDuration: 600,
animationEasing: 'cubicOut',
};
}
mainChartInstance.setOption(option, true);
}
function renderSideChart(segment) {
const cfg = segment.sideChart;
const dom = document.getElementById('side-chart');
if (!sideChartInstance) {
sideChartInstance = echarts.init(dom);
}
document.getElementById('side-chart-title').textContent = cfg.title;
const data = cfg.data.map(d => d.value);
const names = cfg.data.map(d => d.name);
const colors = ['#22c55e', '#3b82f6', '#22c55e', '#3b82f6', '#22c55e', '#3b82f6'];
let option;
if (cfg.type === 'bar-vertical') {
option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(17,17,17,0.95)',
borderColor: 'rgba(255,255,255,0.1)',
textStyle: { color: '#f5f5f5', fontSize: 12 },
},
grid: { left: 10, right: 10, top: 10, bottom: 40 },
xAxis: {
type: 'category',
data: names,
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.08)' } },
axisLabel: { color: '#52525b', fontSize: 9, rotate: 0 },
axisTick: { show: false },
},
yAxis: {
type: 'value',
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.04)' } },
axisLabel: { color: '#52525b', fontSize: 9, formatter: '{value}%' },
},
series: [{
data: data.map((v, i) => ({
value: v,
itemStyle: { color: v >= 30 ? '#22c55e' : '#f59e0b', borderRadius: [3, 3, 0, 0] },
})),
type: 'bar',
barWidth: '45%',
}],
animationDuration: 600,
animationEasing: 'cubicOut',
};
} else if (cfg.type === 'bar-horizontal') {
option = {
tooltip: {
trigger: 'axis',
backgroundColor: 'rgba(17,17,17,0.95)',
borderColor: 'rgba(255,255,255,0.1)',
textStyle: { color: '#f5f5f5', fontSize: 12 },
},
grid: { left: 60, right: 30, top: 10, bottom: 10 },
xAxis: {
type: 'value',
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.04)' } },
axisLabel: { color: '#52525b', fontSize: 9, formatter: '{value}М' },
},
yAxis: {
type: 'category',
data: names.reverse(),
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.08)' } },
axisLabel: { color: '#a1a1aa', fontSize: 10 },
axisTick: { show: false },
},
series: [{
data: cfg.data.map(d => d.value).reverse(),
type: 'bar',
barWidth: '40%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
{ offset: 0, color: '#3b82f6' },
{ offset: 1, color: '#22c55e' },
]),
borderRadius: [0, 4, 4, 0],
},
}],
animationDuration: 600,
animationEasing: 'cubicOut',
};
}
sideChartInstance.setOption(option, true);
}
function renderBottomCharts(segment) {
const cfg = segment.bottomCharts;
// Channel pie chart
const pieDom = document.getElementById('bottom-chart-1');
if (!bottomChart1Instance) {
bottomChart1Instance = echarts.init(pieDom);
}
const pieData = cfg.channels.data.map(d => ({
name: d.name,
value: d.value,
}));
const pieColors = ['#22c55e', '#3b82f6', '#f59e0b', '#a855f7', '#ef4444'];
bottomChart1Instance.setOption({
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(17,17,17,0.95)',
borderColor: 'rgba(255,255,255,0.1)',
textStyle: { color: '#f5f5f5', fontSize: 12 },
},
series: [{
type: 'pie',
radius: ['45%', '70%'],
center: ['50%', '55%'],
avoidLabelOverlap: false,
itemStyle: { borderRadius: 6, borderColor: '#111111', borderWidth: 3 },
label: { show: false },
labelLine: { show: false },
data: pieData.map((d, i) => ({ ...d, itemStyle: { color: pieColors[i % pieColors.length] } })),
emphasis: {
itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0,0,0,0.3)' },
},
}],
animationDuration: 600,
}, true);
// Funnel chart
const funnelDom = document.getElementById('bottom-chart-2');
if (!bottomChart2Instance) {
bottomChart2Instance = echarts.init(funnelDom);
}
const funnelData = cfg.funnel.data.map(d => ({ name: d.name, value: d.value }));
const funnelColors = ['#22c55e', '#3b82f6', '#f59e0b', '#ef4444'];
bottomChart2Instance.setOption({
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(17,17,17,0.95)',
borderColor: 'rgba(255,255,255,0.1)',
textStyle: { color: '#f5f5f5', fontSize: 12 },
},
series: [{
type: 'funnel',
left: '10%',
top: 5,
bottom: 5,
width: '80%',
min: 0,
max: funnelData[0].value,
minSize: '0%',
maxSize: '100%',
sort: 'descending',
gap: 4,
label: {
show: true,
position: 'inside',
color: '#fff',
fontSize: 11,
formatter: '{b}\n{c}',
},
labelLine: { show: false },
itemStyle: { borderColor: '#111', borderWidth: 2 },
data: funnelData.map((d, i) => ({
...d,
itemStyle: { color: funnelColors[i % funnelColors.length] },
})),
}],
animationDuration: 600,
}, true);
// Top metrics bars
const metricsDom = document.getElementById('top-metrics');
metricsDom.innerHTML = cfg.topMetrics.map(m => {
const pctColor = m.pct >= 95 ? 'var(--accent)' : (m.pct >= 85 ? 'var(--accent-secondary)' : 'var(--warning)');
const valDisplay = m.name.includes('Брак') ? m.value + '%' :
(typeof m.value === 'number' && m.value > 100 ? m.value.toLocaleString('ru-RU') : m.value);
return `
<div class="channel-row">
<span class="channel-name">${m.name}</span>
<span class="channel-value">${valDisplay}</span>
<div class="channel-bar-wrap">
<div class="channel-bar" style="width:0%; background:${pctColor};" data-target="${m.pct}"></div>
</div>
</div>
`;
}).join('');
// Animate bars
requestAnimationFrame(() => {
setTimeout(() => {
document.querySelectorAll('.channel-bar').forEach(bar => {
bar.style.width = bar.dataset.target + '%';
});
}, 200);
});
}
function updateDashTime() {
const now = new Date();
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
document.getElementById('dash-time').textContent = `${h}:${m}:${s}`;
}
// ============================================
// MAIN SWITCH FUNCTION
// ============================================
function switchSegment(segmentKey) {
const segment = segments[segmentKey];
if (!segment) return;
currentSegment = segmentKey;
renderKPIs(segment);
renderMainChart(segment);
renderSideChart(segment);
renderBottomCharts(segment);
// Update switcher buttons
document.querySelectorAll('.segment-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.segment === segmentKey);
});
}
// ============================================
// INIT
// ============================================
document.addEventListener('DOMContentLoaded', () => {
// Expose to global scope for onclick handlers
window.switchSegment = switchSegment;
// Initial render
switchSegment('ecommerce');
// Update time
updateDashTime();
setInterval(updateDashTime, 1000);
// Resize handler
window.addEventListener('resize', () => {
mainChartInstance?.resize();
sideChartInstance?.resize();
bottomChart1Instance?.resize();
bottomChart2Instance?.resize();
});
});
</script>
</body>
</html>