redesign: IBM Carbon style — Plex fonts, brutal cards, tab navigation

This commit is contained in:
neo committed 2026-06-21 00:24:38 +05:00
1 parent d7ce367e75
commit f69b730169
1 file changed
+485 -298
+485 -298
View File
@@ -3,224 +3,300 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ROI24 — Интерактивный дашборд</title> <title>ROI24 — Аналитическая панель</title>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <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"> <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> <style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root { :root {
--bg: #09090b; /* IBM Carbon Design System — Dark 10 */
--card: #111113; --bg: #0a0a0a;
--border: rgba(255,255,255,0.06); --surface: #161616;
--border-h: rgba(255,255,255,0.1); --surface-hover: #1f1f1f;
--text: #fafafa; --border: #262626;
--sec: #a1a1aa; --border-strong: #393939;
--muted: #52525b; --text-primary: #f4f4f4;
--g: #22c55e; --gd: rgba(34,197,94,0.12); --text-secondary: #a6a6a6;
--b: #3b82f6; --bd: rgba(59,130,246,0.12); --text-tertiary: #6f6f6f;
--a: #f59e0b; --ad: rgba(245,158,11,0.12); --accent: #0f6fff;
--r: #ef4444; --rd: rgba(239,68,68,0.12); --accent-hover: #3d8eff;
--p: #a855f7; --pd: rgba(168,85,247,0.12); --accent-dim: rgba(15,111,255,0.1);
--c: #06b6d4; --cd: rgba(6,182,212,0.12); --green: #16a34a;
--mono: 'JetBrains Mono', monospace; --green-dim: rgba(22,163,74,0.12);
--ease: cubic-bezier(0.4, 0, 0.2, 1); --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 { body {
font-family: 'Inter', -apple-system, sans-serif; font-family: var(--sans);
background: var(--bg); color: var(--text); background: var(--bg);
line-height: 1.6; -webkit-font-smoothing: antialiased; color: var(--text-primary);
min-height: 100vh; overflow-x: hidden; line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
} }
/* ============================================ /* ============================================
HEADER TOP BAR — IBM style
============================================ */ ============================================ */
.header { .topbar {
display: flex; align-items: center; justify-content: space-between; display: flex; align-items: center; justify-content: space-between;
padding: 12px 28px; padding: 0 32px; height: 48px;
background: rgba(17,17,19,0.85); backdrop-filter: blur(16px); background: var(--surface);
border-bottom: 1px solid var(--border); position: sticky; top: 0; z-index: 100; border-bottom: 1px solid var(--border);
position: sticky; top: 0; z-index: 100;
} }
.logo { .topbar-brand {
font-family: var(--mono); font-weight: 700; font-size: 15px; font-family: var(--mono); font-size: 13px; font-weight: 600;
letter-spacing: -0.3px; display: flex; align-items: center; gap: 8px; letter-spacing: 0.5px; color: var(--text-primary);
display: flex; align-items: center; gap: 10px;
} }
.dot { .topbar-brand .icon {
width: 8px; height: 8px; background: var(--g); border-radius: 50%; width: 20px; height: 20px; background: var(--accent);
box-shadow: 0 0 8px rgba(34,197,94,0.4); display: flex; align-items: center; justify-content: center;
animation: pulse 2s ease-in-out infinite; font-size: 11px; font-weight: 700; color: #fff;
} }
@keyframes pulse { .topbar-right { display: flex; align-items: center; gap: 20px; font-size: 12px; }
0%,100%{box-shadow:0 0 8px rgba(34,197,94,0.4);} .status { display: flex; align-items: center; gap: 6px; color: var(--green); font-weight: 500; }
50%{box-shadow:0 0 16px rgba(34,197,94,0.15);} .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} }
.hdr-right { display:flex; align-items:center; gap:16px; font-size:12px; } .clock { font-family: var(--mono); color: var(--text-tertiary); font-size: 11px; letter-spacing: 0.3px; }
.live { display:flex; align-items:center; gap:6px; color:var(--g); font-weight:600; }
.live .dot { width:6px; height:6px; }
.clock { font-family:var(--mono); color:var(--muted); font-size:11px; }
/* ============================================ /* ============================================
SWITCHER PAGE HEADER — IBM heading style
============================================ */ ============================================ */
.sw-wrap { display:flex; justify-content:center; padding:32px 24px 0; } .page-header {
.sw { max-width: 1280px; margin: 0 auto;
display:flex; gap:4px; padding:5px; padding: 32px 32px 0;
background:var(--card); border:1px solid var(--border);
border-radius:14px;
} }
.sw-btn { .page-title {
padding:10px 20px; border:none; background:transparent; font-family: var(--sans);
color:var(--sec); font-family:var(--mono); font-size:12px; font-size: 28px; font-weight: 700;
font-weight:500; border-radius:10px; cursor:pointer; letter-spacing: -0.5px;
transition:all .25s var(--ease); white-space:nowrap; line-height: 1.2;
display:flex; align-items:center; gap:6px; color: var(--text-primary);
margin-bottom: 8px;
} }
.sw-btn:hover{color:var(--text);} .page-desc {
.sw-btn.active{ font-size: 14px; color: var(--text-secondary);
background:var(--gd); color:var(--g); font-weight: 400; line-height: 1.5;
box-shadow:0 0 0 1px rgba(34,197,94,0.2);
} }
/* ============================================ /* ============================================
KPI GRID — 6 cards, research-backed 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 { .kpi-grid {
display:grid; grid-template-columns:repeat(6,1fr); display: grid; grid-template-columns: repeat(6, 1fr);
gap:12px; padding:24px; max-width:1280px; margin:0 auto; 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 { .kpi {
background:var(--card); border:1px solid var(--border); background: var(--surface);
border-radius:12px; padding:16px; padding: 20px 24px;
position:relative; overflow:hidden; position: relative;
transition:all .25s var(--ease); transition: background 0.15s;
opacity:0; transform:translateY(12px); opacity: 0; transform: translateY(8px);
} }
.kpi.vis { opacity:1; transform:translateY(0); } .kpi.visible { opacity: 1; transform: translateY(0); }
.kpi:hover{border-color:var(--border-h); transform:translateY(-2px);} .kpi:hover { background: var(--surface-hover); }
.kpi-accent { .kpi-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
position:absolute; top:0;left:0; width:3px; height:100%; opacity:0; transition:opacity .25s;
}
.kpi:hover .kpi-accent{opacity:1;}
.kpi-name { .kpi-name {
font-size:10px; text-transform:uppercase; letter-spacing:.08em; font-size: 11px; font-weight: 600;
color:var(--muted); font-weight:600; margin-bottom:8px; text-transform: uppercase; letter-spacing: 0.06em;
color: var(--text-secondary);
} }
.kpi-val { .kpi-badge {
font-family:var(--mono); font-size:22px; font-weight:700; font-family: var(--mono); font-size: 10px; font-weight: 500;
margin-bottom:6px; line-height:1.1; padding: 2px 6px; border-radius: 0;
text-transform: uppercase; letter-spacing: 0.05em;
} }
.kpi-meta { display:flex; align-items:center; gap:6px; font-size:11px; } .kpi-badge.up { color: var(--green); background: var(--green-dim); }
.trend { .kpi-badge.down { color: var(--red); background: var(--red-dim); }
display:inline-flex; align-items:center; gap:3px; .kpi-badge.neutral { color: var(--accent); background: var(--accent-dim); }
padding:2px 6px; border-radius:5px; .kpi-value {
font-family:var(--mono); font-weight:600; font-size:10px; 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;
} }
.t.up{color:var(--g);background:var(--gd);} .kpi-context { font-size: 11px; color: var(--text-tertiary); }
.t.down{color:var(--r);background:var(--rd);}
.t.neu{color:var(--b);background:var(--bd);}
.per { color:var(--muted); font-size:10px; }
/* ============================================ /* ============================================
CHARTS CHART PANELS — IBM structure
============================================ */ ============================================ */
.cr { display:grid; gap:12px; padding:12px 24px; max-width:1280px; margin:0 auto; } .charts-wrap {
.cr2 { grid-template-columns:2fr 1fr; } max-width: 1280px; margin: 0 auto;
.cr3 { grid-template-columns:1fr 1fr 1fr; } padding: 0 32px 32px;
.cc { display: grid; gap: 1px;
background:var(--card); border:1px solid var(--border); background: var(--border);
border-radius:14px; padding:20px; transition:border-color .25s; border: 1px solid var(--border);
border-radius: 4px; overflow: hidden;
} }
.cc:hover{border-color:var(--border-h);} .charts-row-main { grid-template-columns: 2fr 1fr; }
.ch { margin-bottom:4px; } .charts-row-sub { grid-template-columns: 1fr 1fr 1fr; }
.ct { font-size:13px; font-weight:600; color:var(--text); }
.cs { font-size:11px; color:var(--muted); }
.cd { width:100%; height:280px; }
.cd.s { height:200px; }
.metric-list { display:flex; flex-direction:column; gap:10px; margin-top:12px; } .chart-panel {
.mi { display:flex; align-items:center; justify-content:space-between; padding:8px 0; border-bottom:1px solid var(--border); } background: var(--surface);
.mi:last-child{border-bottom:none;} padding: 24px;
.ml { font-size:12px; color:var(--sec); flex:1; } min-height: 280px;
.mv { font-family:var(--mono); font-size:12px; font-weight:600; color:var(--text); margin:0 12px; min-width:60px; text-align:right; }
.mb { width:70px; height:4px; background:var(--border); border-radius:2px; overflow:hidden; }
.mf { height:100%; border-radius:2px; width:0; transition:width 1s var(--ease); }
.section-hdr { text-align:center; padding:40px 24px 20px; }
.section-hdr h2 { font-size:clamp(1.6rem,4vw,2.2rem); font-weight:800; letter-spacing:-.5px; margin-bottom:6px; }
.section-hdr p { color:var(--sec); font-size:14px; }
@media(max-width:1024px){
.kpi-grid{grid-template-columns:repeat(3,1fr);}
.cr2,.cr3{grid-template-columns:1fr;}
} }
@media(max-width:640px){ .chart-header { margin-bottom: 20px; }
.kpi-grid{grid-template-columns:repeat(2,1fr);} .chart-title {
.sw-btn{padding:8px 12px;font-size:11px;} 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> </style>
</head> </head>
<body> <body>
<div class="header"> <!-- TOP BAR -->
<div class="logo"><span class="dot"></span>ROI24</div> <div class="topbar">
<div class="hdr-right"> <div class="topbar-brand">
<div class="live"><span class="dot"></span>Live</div> <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 class="clock" id="clock"></div>
</div> </div>
</div> </div>
<div class="section-hdr"> <!-- PAGE HEADER -->
<h2>Все ключевые цифры — в одном месте</h2> <div class="page-header">
<p>Переключите бизнес — увидите свои метрики</p> <h1 class="page-title">Аналитика бизнеса</h1>
<p class="page-desc">Ключевые показатели эффективности в реальном времени</p>
</div> </div>
<div class="sw-wrap"> <!-- IBM-STYLE TABS -->
<div class="sw" id="sw"> <div class="tabs-section">
<button class="sw-btn active" data-seg="ecom">🛒 Интернет-магазин</button> <div class="tabs" id="tabs">
<button class="sw-btn" data-seg="services">💼 Услуги / Агентство</button> <button class="tab-btn active" data-seg="ecom">
<button class="sw-btn" data-seg="production">🏭 Производство</button> <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> </div>
<div id="dash"> <!-- KPI GRID -->
<div class="kpi-grid" id="kpi-grid"></div> <div class="kpi-grid" id="kpi-grid"></div>
<div class="cr cr2">
<div class="cc"> <!-- CHARTS -->
<div class="ch"><div class="ct" id="mc-title"></div><div class="cs" id="mc-sub"></div></div> <div class="charts-wrap">
<div class="cd" id="mc"></div> <div class="charts-row-main" id="charts-main"></div>
</div> <div class="charts-row-sub" id="charts-sub"></div>
<div class="cc">
<div class="ch"><div class="ct" id="sc-title"></div><div class="cs" id="sc-sub"></div></div>
<div class="cd" id="sc"></div>
</div>
</div>
<div class="cr cr3">
<div class="cc">
<div class="ch"><div class="ct" id="c1-title"></div><div class="cs" id="c1-sub"></div></div>
<div class="cd s" id="c1"></div>
</div>
<div class="cc">
<div class="ch"><div class="ct" id="c2-title"></div><div class="cs" id="c2-sub"></div></div>
<div class="cd s" id="c2"></div>
</div>
<div class="cc">
<div class="ch"><div class="ct">Выполнение плана</div><div class="cs">Ключевые метрики</div></div>
<div class="metric-list" id="mlist"></div>
</div>
</div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
<script> <script>
/* ============================================
DATA — KPIs from research:
E-com: Revenue, AOV, CVR, CAC, CLV, ROAS (Shopify)
Services: Revenue, Win Rate, Sales Cycle, Pipeline Coverage, CPL, ROMI (Optifai)
Production: Volume, OEE, Defect Rate, Cost/Unit, Inventory Turnover, ROMI
============================================ */
const D = { const D = {
ecom: { ecom: {
kpis: [ kpis: [
@@ -231,12 +307,12 @@ const D = {
{ n:'CLV', v:'12 400 ₽', d:'+18%', t:'up', p:'за 6 мес' }, { n:'CLV', v:'12 400 ₽', d:'+18%', t:'up', p:'за 6 мес' },
{ n:'ROAS', v:'4.2×', d:'+15%', t:'up', p:'возврат рекламы' }, { n:'ROAS', v:'4.2×', d:'+15%', t:'up', p:'возврат рекламы' },
], ],
mc: { title:'Выручка и расходы на рекламу', sub:'12 месяцев · выручка (зелёный) vs маркетинг-бюджет (синий)', type:'dual', mc: { title:'Выручка и расходы на рекламу', sub:'12 месяцев · выручка (зелёный) vs маркетинг-бюджет (пунктир)', type:'dual',
labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'], 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], 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], 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', sc: { title:'ROAS по каналам', sub:'Возврат инвестиций в каждый канал', type:'bar-h',
data:[ data:[
{n:'SEO',v:520},{n:'Email',v:380},{n:'Прямой',v:280}, {n:'SEO',v:520},{n:'Email',v:380},{n:'Прямой',v:280},
{n:'Контекст',v:150},{n:'Соцсети',v:120},{n:'Рефералы',v:95}, {n:'Контекст',v:150},{n:'Соцсети',v:120},{n:'Рефералы',v:95},
@@ -253,11 +329,11 @@ const D = {
data:[120,98,76,54,42], data:[120,98,76,54,42],
}, },
metrics: [ metrics: [
{l:'Выручка (план)',v:'4.2М',p:93,c:'var(--g)'}, {l:'Выручка (план)',v:'4.2М',p:93,c:'green'},
{l:'CVR (план)',v:'3.8%',p:95,c:'var(--g)'}, {l:'CVR (план)',v:'3.8%',p:95,c:'green'},
{l:'ROAS (план)',v:'4.2×',p:105,c:'var(--g)'}, {l:'ROAS (план)',v:'4.2×',p:105,c:'green'},
{l:'CAC (план)',v:'680₽',p:85,c:'var(--a)'}, {l:'CAC (план)',v:'680₽',p:85,c:'yellow'},
{l:'CLV (план)',v:'12.4К',p:104,c:'var(--g)'}, {l:'CLV (план)',v:'12.4К',p:104,c:'green'},
], ],
}, },
services: { services: {
@@ -291,11 +367,11 @@ const D = {
data:[112,95,88,105,78], data:[112,95,88,105,78],
}, },
metrics: [ metrics: [
{l:'Пайплайн (план)',v:'12.8М',p:85,c:'var(--b)'}, {l:'Пайплайн (план)',v:'12.8М',p:85,c:'blue'},
{l:'Win Rate (план)',v:'24%',p:96,c:'var(--g)'}, {l:'Win Rate (план)',v:'24%',p:96,c:'green'},
{l:'Sales Cycle (план)',v:'32д',p:90,c:'var(--g)'}, {l:'Sales Cycle (план)',v:'32д',p:90,c:'green'},
{l:'Pipeline Cov. (план)',v:'4.1×',p:103,c:'var(--g)'}, {l:'Pipeline Cov. (план)',v:'4.1×',p:103,c:'green'},
{l:'ROMI (план)',v:'245%',p:123,c:'var(--g)'}, {l:'ROMI (план)',v:'245%',p:123,c:'green'},
], ],
}, },
production: { production: {
@@ -329,190 +405,301 @@ const D = {
data:[45,25,12,10,8], data:[45,25,12,10,8],
}, },
metrics: [ metrics: [
{l:'Выпуск (план)',v:'15.4К',p:96,c:'var(--g)'}, {l:'Выпуск (план)',v:'15.4К',p:96,c:'green'},
{l:'OEE (план)',v:'78%',p:92,c:'var(--a)'}, {l:'OEE (план)',v:'78%',p:92,c:'yellow'},
{l:'Брак (план)',v:'2.1%',p:105,c:'var(--g)'}, {l:'Брак (план)',v:'2.1%',p:105,c:'green'},
{l:'ROMI (план)',v:'310%',p:111,c:'var(--g)'}, {l:'ROMI (план)',v:'310%',p:111,c:'green'},
{l:'Себест. (план)',v:'3.2К',p:94,c:'var(--b)'}, {l:'Себест. (план)',v:'3.2К',p:94,c:'blue'},
], ],
}, },
}; };
let charts = {}; let charts = {};
function seg(key) { function seg(key) {
const s = D[key]; const s = D[key];
document.querySelectorAll('.sw-btn').forEach(b => b.classList.toggle('active', b.dataset.seg === key));
// Tabs
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.seg === key));
// KPIs // KPIs
const g = document.getElementById('kpi-grid'); const g = document.getElementById('kpi-grid');
g.innerHTML = s.kpis.map((d,i) => ` g.innerHTML = s.kpis.map((d,i) => {
<div class="kpi" style="transition-delay:${i*80}ms"> const badgeClass = d.t;
<div class="kpi-accent" style="background:${i<3?'var(--g)':i<5?'var(--b)':'var(--p)'}"></div> const arrow = d.t === 'up' ? '▲' : d.t === 'down' ? '▼' : '●';
<div class="kpi-name">${d.n}</div> return `<div class="kpi" style="transition-delay:${i*60}ms">
<div class="kpi-val">${d.v}</div> <div class="kpi-top">
<div class="kpi-meta"> <span class="kpi-name">${d.n}</span>
<span class="trend ${d.t}">${d.t==='up'?'↑':d.t==='down'?'↓':'→'} ${d.d}</span> <span class="kpi-badge ${badgeClass}">${arrow} ${d.d}</span>
<span class="per">${d.p}</span>
</div> </div>
</div>`).join(''); <div class="kpi-value">${d.v}</div>
requestAnimationFrame(() => g.querySelectorAll('.kpi').forEach(c=>c.classList.add('vis'))); <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 // Main chart
const mc = s.mc; const mcEl = document.getElementById('mc-canvas');
const mcDom = document.getElementById('mc'); charts.mc.dispose();
if (!charts.mc) charts.mc = echarts.init(mcDom); charts.mc = echarts.init(mcEl);
document.getElementById('mc-title').textContent = mc.title; let mcOpt;
document.getElementById('mc-sub').textContent = mc.sub; if (mc.type === 'dual') {
mcOpt = {
let opt; tooltip: { trigger: 'axis', backgroundColor: '#161616', borderColor: '#262626', textStyle: { color: '#f4f4f4', fontSize: 12, fontFamily: 'IBM Plex Mono' } },
if (mc.type==='dual') { legend: { data: ['Доход', 'Расход'], textStyle: { color: '#6f6f6f', fontSize: 11, fontFamily: 'IBM Plex Sans' }, top: 0, itemWidth: 12, itemHeight: 2 },
opt = { grid: { left: 48, right: 20, top: 32, bottom: 28 },
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}}, xAxis: {
legend:{data:['Доход','Расход'],textStyle:{color:'#52525b',fontSize:10},top:0}, type: 'category', data: mc.labels,
grid:{left:44,right:16,top:28,bottom:28}, axisLine: { lineStyle: { color: '#262626' } },
xAxis:{type:'category',data:mc.labels, axisLabel: { color: '#6f6f6f', fontSize: 11, fontFamily: 'IBM Plex Mono' },
axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#52525b',fontSize:10}}, axisTick: { show: false },
yAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:10,formatter:'{value}М'}}, },
series:[ yAxis: {
{name:'Доход',type:'line',data:mc.rev,smooth:.4,symbol:'none',lineStyle:{width:2.5,color:'#22c55e'}, type: 'value',
areaStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[ splitLine: { lineStyle: { color: '#1f1f1f' } },
{offset:0,color:'rgba(34,197,94,0.2)'},{offset:1,color:'rgba(34,197,94,0.01)'}])}}, axisLabel: { color: '#6f6f6f', fontSize: 11, fontFamily: 'IBM Plex Mono', formatter: '{value}М' },
{name:'Расход',type:'line',data:mc.ads,smooth:.4,symbol:'none', },
lineStyle:{width:2,color:'#3b82f6',type:'dashed'}, series: [
areaStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[ {
{offset:0,color:'rgba(59,130,246,0.08)'},{offset:1,color:'rgba(59,130,246,0.01)'}])}}, 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:600, animationDuration: 500,
animationEasing: 'cubicOut',
}; };
} else if (mc.type==='bar-cmp') { } else {
opt = { mcOpt = {
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}}, tooltip: { trigger: 'axis', backgroundColor: '#161616', borderColor: '#262626', textStyle: { color: '#f4f4f4', fontSize: 12, fontFamily: 'IBM Plex Mono' } },
legend:{data:['План','Факт'],textStyle:{color:'#52525b',fontSize:10},top:0}, legend: { data: ['План', 'Факт'], textStyle: { color: '#6f6f6f', fontSize: 11, fontFamily: 'IBM Plex Sans' }, top: 0, itemWidth: 12, itemHeight: 14 },
grid:{left:44,right:16,top:28,bottom:28}, grid: { left: 48, right: 20, top: 32, bottom: 28 },
xAxis:{type:'category',data:mc.labels, xAxis: {
axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#52525b',fontSize:10}}, type: 'category', data: mc.labels,
yAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:10,formatter:'{value}К'}}, axisLine: { lineStyle: { color: '#262626' } },
series:[ axisLabel: { color: '#6f6f6f', fontSize: 11, fontFamily: 'IBM Plex Mono' },
{name:'План',type:'bar',data:mc.plan,barWidth:'35%',itemStyle:{color:'rgba(59,130,246,0.15)',borderRadius:[4,4,0,0]}}, },
{name:'Факт',type:'bar',data:mc.fact,barWidth:'35%',itemStyle:{color:'#3b82f6',borderRadius:[4,4,0,0]}}, 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:600, animationDuration: 500,
animationEasing: 'cubicOut',
}; };
} }
charts.mc.setOption(opt, true); charts.mc.setOption(mcOpt, true);
// Side chart (bar horizontal) // Side chart
const sc = s.sc; const sc = s.sc;
const scDom = document.getElementById('sc'); const scEl = document.getElementById('sc-canvas');
if (!charts.sc) charts.sc = echarts.init(scDom); if (!charts.sc) charts.sc = echarts.init(scEl);
document.getElementById('sc-title').textContent = sc.title; charts.sc.dispose();
document.getElementById('sc-sub').textContent = sc.sub; charts.sc = echarts.init(scEl);
const cols = ['#22c55e','#06b6d4','#a855f7','#3b82f6','#f59e0b','#ef4444'];
const scData = sc.data.reverse(); const scData = sc.data.reverse();
const scColors = ['#0f6fff', '#3d8eff', '#16a34a', '#06b6d4', '#a855f7', '#eac518'];
const scOpt = { const scOpt = {
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}}, tooltip: { trigger: 'axis', backgroundColor: '#161616', borderColor: '#262626', textStyle: { color: '#f4f4f4', fontSize: 11 } },
grid:{left:80,right:30,top:10,bottom:10}, grid: { left: 90, right: 28, top: 8, bottom: 8 },
xAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:9,formatter:'{value}%'}}, xAxis: {
yAxis:{type:'category',data:scData.map(d=>d.n),axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#a1a1aa',fontSize:10}}, type: 'value',
series:[{ splitLine: { lineStyle: { color: '#1f1f1f' } },
type:'bar',data:scData.map(d=>d.v),barWidth:38, axisLabel: { color: '#6f6f6f', fontSize: 10, fontFamily: 'IBM Plex Mono', formatter: '{value}%' },
itemStyle:{ },
borderRadius:[0,5,5,0], yAxis: {
color:function(p){const c=cols[p.dataIndex%cols.length];return new echarts.graphic.LinearGradient(0,0,1,0,[{offset:0,color:c+'88'},{offset:1,color:c}]);} 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:600, animationDuration: 500,
}; };
charts.sc.setOption(scOpt, true); charts.sc.setOption(scOpt, true);
// Chart 1: Funnel // Sub row charts
const subEl = document.getElementById('charts-sub');
const c1 = s.c1; const c1 = s.c1;
const c1Dom = document.getElementById('c1');
if (!charts.c1) charts.c1 = echarts.init(c1Dom);
document.getElementById('c1-title').textContent = c1.title;
document.getElementById('c1-sub').textContent = c1.sub;
const fc = ['#22c55e','#06b6d4','#a855f7','#ef4444'];
const c1Opt = {
tooltip:{trigger:'item',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}},
series:[{
type:'funnel',left:'10%',bottom:5,width:'80%',min:0,max:c1.data[0].v,
minSize:'0%',maxSize:'100%',sort:'descending',gap:4,
label:{show:true,position:'inside',color:'#fff',fontSize:11,formatter:'{b}\n{c} ({d}%)'},
labelLine:{show:false},itemStyle:{borderColor:'#111',borderWidth:2},
data:c1.data.map((d,i)=>({name:d.n,value:d.v,itemStyle:{color:fc[i%fc.length]}})),
}],
animationDuration:600,
};
charts.c1.setOption(c1Opt, true);
// Chart 2: depends on segment (pie or bar)
const c2 = s.c2; const c2 = s.c2;
const c2Dom = document.getElementById('c2');
if (!charts.c2) charts.c2 = echarts.init(c2Dom); // Funnel chart
document.getElementById('c2-title').textContent = c2.title; let funnelHtml = `<div class="chart-panel">
document.getElementById('c2-sub').textContent = c2.sub; <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) { if (c2.labels) {
// bar or pie depending on data const isPie = c2.data.length && c2.data.reduce((a, b) => a + b, 0) <= 100;
const isPie = c2.data.length && c2.data.reduce((a,b)=>a+b,0) <= 100; const pc = ['#0f6fff', '#16a34a', '#eac518', '#a855f7', '#e21d1d'];
const pc = ['#22c55e','#3b82f6','#f59e0b','#a855f7','#ef4444']; 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) { if (isPie) {
charts.c2.setOption({ charts.c2.setOption({
tooltip:{trigger:'item',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}}, tooltip: { trigger: 'item', backgroundColor: '#161616', borderColor: '#262626', textStyle: { color: '#f4f4f4', fontSize: 11 } },
series:[{ series: [{
type:'pie',radius:['45%','70%'],center:['50%','55%'], type: 'pie', radius: ['40%', '65%'], center: ['50%', '55%'],
itemStyle:{borderRadius:6,borderColor:'#111',borderWidth:3}, itemStyle: { borderRadius: 0, borderColor: '#161616', borderWidth: 2 },
label:{show:false},labelLine:{show:false}, label: { show: false }, labelLine: { show: false },
data:c2.labels.map((l,i)=>({name:l,value:c2.data[i],itemStyle:{color:pc[i%pc.length]}})), data: c2.labels.map((l, i) => ({ name: l, value: c2.data[i], itemStyle: { color: pc[i % pc.length] } })),
emphasis:{itemStyle:{shadowBlur:10,shadowOffsetX:0,shadowColor:'rgba(0,0,0,0.3)'}},
}], }],
animationDuration:600, animationDuration: 500,
}, true); }, true);
} else { } else {
charts.c2.setOption({ charts.c2.setOption({
tooltip:{trigger:'axis',backgroundColor:'#111',borderColor:'#222',textStyle:{color:'#fafafa',fontSize:11}}, tooltip: { trigger: 'axis', backgroundColor: '#161616', borderColor: '#262626', textStyle: { color: '#f4f4f4', fontSize: 11 } },
grid:{left:60,right:16,top:10,bottom:28}, grid: { left: 60, right: 16, top: 8, bottom: 28 },
xAxis:{type:'value',splitLine:{lineStyle:{color:'#1a1a1e'}},axisLabel:{color:'#52525b',fontSize:9,formatter:'{value}%'}}, xAxis: {
yAxis:{type:'category',data:c2.labels, type: 'value',
axisLine:{lineStyle:{color:'#1e1e22'}},axisLabel:{color:'#a1a1aa',fontSize:10}}, splitLine: { lineStyle: { color: '#1f1f1f' } },
series:[{ axisLabel: { color: '#6f6f6f', fontSize: 10, formatter: '{value}%' },
type:'bar',data:c2.data,barWidth:30,
itemStyle:{
borderRadius:[4,4,0,0],
color:function(p){return p.data===undefined ? '#22c55e' : (p.data>=100?'#22c55e':'var(--a)');}
}, },
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:600, animationDuration: 500,
}, true); }, true);
} }
} }
// Metrics list // Metrics list
const ml = document.getElementById('mlist'); const ml = document.getElementById('metric-list');
ml.innerHTML = s.metrics.map(m => ` ml.innerHTML = s.metrics.map(m => `
<div class="mi"> <div class="metric-item">
<span class="ml">${m.l}</span> <span class="m-label">${m.l}</span>
<span class="mv">${m.v}</span> <span class="m-value">${m.v}</span>
<div class="mb"><div class="mf" data-p="${m.p}" style="background:${m.c}"></div></div> <div class="m-bar"><div class="m-fill ${m.c}" data-p="${m.p}"></div></div>
</div>`).join(''); </div>`).join('');
requestAnimationFrame(() => setTimeout(() => { requestAnimationFrame(() => setTimeout(() => {
ml.querySelectorAll('.mf').forEach(f=>f.style.width=f.dataset.p+'%'); ml.querySelectorAll('.m-fill').forEach(f => f.style.width = f.dataset.p + '%');
}, 100)); }, 100));
} }
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
document.getElementById('sw').addEventListener('click', e => { document.getElementById('tabs').addEventListener('click', e => {
const b = e.target.closest('.sw-btn'); const b = e.target.closest('.tab-btn');
if (!b) return; if (!b) return;
seg(b.dataset.seg); seg(b.dataset.seg);
}); });
seg('ecom'); seg('ecom');
function tick() { function tick() {
const n=new Date(); const n = new Date();
document.getElementById('clock').textContent = document.getElementById('clock').textContent =
String(n.getHours()).padStart(2,'0')+':'+String(n.getMinutes()).padStart(2,'0')+':'+String(n.getSeconds()).padStart(2,'0'); String(n.getHours()).padStart(2, '0') + ':' + String(n.getMinutes()).padStart(2, '0') + ':' + String(n.getSeconds()).padStart(2, '0');
} }
tick(); setInterval(tick, 1000); tick(); setInterval(tick, 1000);
new ResizeObserver(() => Object.values(charts).forEach(c=>c.resize())).observe(document.getElementById('dash')); new ResizeObserver(() => Object.values(charts).forEach(c => c.resize())).observe(document.getElementById('charts-wrap'));
}); });
</script> </script>
</body> </body>