Merge remote-tracking branch 'origin/main'

This commit is contained in:
Anuar 2026-05-22 16:58:19 +05:00
commit 285409ba92

View File

@ -1,188 +1,603 @@
<!doctype html>
<html lang="ru" data-theme="dark">
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Дашборд функций рекрутинга — telecom.quest</title>
<link rel="stylesheet" href="./styles.css">
<script src="https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js"></script>
<script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Дашборд Текучести Персонала 2025</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
:root {
--navy: #1B2A4A;
--navy2: #243358;
--red: #C0392B;
--white: #FFFFFF;
--bg: #F0F4F9;
--card: #FFFFFF;
--text: #1E293B;
--muted: #64748B;
--border: #E2E8F0;
--teal: #0D9488;
--purple: #7C3AED;
--orange: #D97706;
--green: #059669;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', Arial, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
}
/* ── Header ── */
header {
background: var(--navy);
padding: 0 32px;
display: flex;
align-items: center;
justify-content: space-between;
height: 64px;
position: sticky;
top: 0;
z-index: 100;
border-bottom: 4px solid var(--red);
box-shadow: 0 2px 16px rgba(0,0,0,.25);
}
header h1 {
color: #fff;
font-size: 18px;
font-weight: 700;
letter-spacing: .5px;
}
header .period {
color: #94A8CC;
font-size: 13px;
background: rgba(255,255,255,.07);
border: 1px solid rgba(255,255,255,.12);
padding: 5px 14px;
border-radius: 20px;
}
/* ── Layout ── */
main { padding: 28px 32px 40px; max-width: 1440px; margin: 0 auto; }
/* ── KPI Strip ── */
.kpi-row {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.kpi {
border-radius: 12px;
padding: 20px 18px 16px;
display: flex;
flex-direction: column;
gap: 4px;
position: relative;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,.10);
transition: transform .15s, box-shadow .15s;
cursor: default;
}
.kpi:hover { transform: translateY(-3px); box-shadow: 0 6px 20px rgba(0,0,0,.15); }
.kpi::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 4px;
background: rgba(255,255,255,.35);
border-radius: 12px 12px 0 0;
}
.kpi-label { font-size: 10px; font-weight: 700; letter-spacing: 1.2px; text-transform: uppercase; opacity: .75; color: #fff; }
.kpi-value { font-size: 36px; font-weight: 800; color: #fff; line-height: 1.1; }
.kpi-sub { font-size: 12px; color: rgba(255,255,255,.65); margin-top: 2px; }
.kpi-icon { position: absolute; right: 14px; top: 14px; font-size: 28px; opacity: .2; }
.kpi-total { background: linear-gradient(135deg, #1B2A4A, #2D4070); }
.kpi-vol { background: linear-gradient(135deg, #B83232, #E74C3C); }
.kpi-agr { background: linear-gradient(135deg, #243358, #1A5276); }
.kpi-dept { background: linear-gradient(135deg, #065F46, #059669); }
.kpi-peak { background: linear-gradient(135deg, #6C3483, #8E44AD); }
/* ── Section ── */
.section-title {
font-size: 11px;
font-weight: 700;
letter-spacing: 1.4px;
text-transform: uppercase;
color: var(--muted);
margin: 28px 0 14px;
display: flex;
align-items: center;
gap: 10px;
}
.section-title::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
/* ── Grid ── */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.grid-3 { display: grid; grid-template-columns: 2fr 1fr 1fr; gap: 20px; }
.grid-4 { display: grid; grid-template-columns: repeat(4,1fr); gap: 20px; }
.col-span2{ grid-column: span 2; }
/* ── Card ── */
.card {
background: var(--card);
border-radius: 14px;
padding: 22px 24px 18px;
box-shadow: 0 1px 6px rgba(0,0,0,.07);
border: 1px solid var(--border);
transition: box-shadow .15s;
}
.card:hover { box-shadow: 0 4px 18px rgba(0,0,0,.10); }
.card-title {
font-size: 13px;
font-weight: 700;
color: var(--navy);
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.card-title .dot {
width: 8px; height: 8px;
border-radius: 50%;
display: inline-block;
}
.chart-wrap { position: relative; }
.chart-wrap canvas { max-height: 280px; }
/* ── Table ── */
table { width: 100%; border-collapse: collapse; font-size: 13px; }
thead th {
background: var(--navy);
color: #fff;
padding: 9px 12px;
text-align: left;
font-size: 11px;
font-weight: 600;
letter-spacing: .5px;
}
thead th:first-child { border-radius: 8px 0 0 0; }
thead th:last-child { border-radius: 0 8px 0 0; }
tbody tr { border-bottom: 1px solid var(--border); transition: background .1s; }
tbody tr:hover { background: #F8FAFF; }
tbody td { padding: 9px 12px; color: var(--text); }
tbody tr:last-child td:first-child { border-radius: 0 0 0 8px; }
tbody tr:last-child td:last-child { border-radius: 0 0 8px 0; }
/* Progress bar in table */
.bar-wrap { display: flex; align-items: center; gap: 8px; }
.bar-track { flex: 1; height: 7px; background: var(--border); border-radius: 4px; min-width: 60px; }
.bar-fill { height: 7px; border-radius: 4px; }
.bar-pct { font-size: 11px; color: var(--muted); width: 38px; text-align: right; }
/* Badge */
.badge {
display: inline-block;
padding: 2px 9px;
border-radius: 20px;
font-size: 11px;
font-weight: 600;
}
.badge-blue { background: #EEF2FF; color: #3730A3; }
.badge-red { background: #FEF2F2; color: #B91C1C; }
.badge-green { background: #F0FDF4; color: #15803D; }
.badge-grey { background: #F1F5F9; color: #475569; }
/* ── Gender donut label ── */
.gender-wrap { display: flex; flex-direction: column; gap: 12px; }
.gender-legend { display: flex; flex-direction: column; gap: 8px; margin-top: 8px; }
.gl-item { display: flex; align-items: center; gap: 10px; font-size: 13px; }
.gl-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
.gl-label { color: var(--text); flex: 1; }
.gl-val { font-weight: 700; color: var(--navy); }
.gl-pct { color: var(--muted); font-size: 12px; }
/* ── Footer ── */
footer {
text-align: center;
padding: 20px;
color: var(--muted);
font-size: 12px;
border-top: 1px solid var(--border);
margin-top: 36px;
}
/* ── Responsive ── */
@media (max-width: 1100px) {
.kpi-row { grid-template-columns: repeat(3,1fr); }
.grid-3 { grid-template-columns: 1fr 1fr; }
.grid-4 { grid-template-columns: repeat(2,1fr); }
.col-span2{ grid-column: span 1; }
}
@media (max-width: 700px) {
main { padding: 16px; }
.kpi-row { grid-template-columns: repeat(2,1fr); }
.grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; }
header h1 { font-size: 14px; }
}
</style>
</head>
<body>
<header>
<img src="./assets/logo.svg" alt="telecom.quest" class="logo">
<h1>🚀 Дашборд функций рекрутинга</h1>
<button id="theme-toggle" aria-label="Тема">🌙</button>
</header>
<section class="toolbar">
<label class="btn-primary">
📁 Загрузить .xlsx
<input type="file" id="file-input" accept=".xlsx" hidden>
</label>
<select id="group-filter" multiple size="3" hidden></select>
<input type="search" id="search-input" placeholder="🔍 Поиск функции..." hidden>
</section>
<header>
<h1>📊 Дашборд Текучести Персонала</h1>
<span class="period">Январь — Ноябрь 2025 · 11 месяцев</span>
</header>
<section class="hero">
<div class="hero-text">
<h2>🔬 Аналитика рекрутинга</h2>
<p>Время функций → KPI, нагрузка, узкие места, симулятор «что если». Без сервера: данные парсятся прямо в браузере.</p>
<main>
<!-- KPI -->
<div class="kpi-row">
<div class="kpi kpi-total">
<span class="kpi-icon">👥</span>
<span class="kpi-label">Всего уволено</span>
<span class="kpi-value">705</span>
<span class="kpi-sub">за 11 месяцев 2025</span>
</div>
<div class="kpi kpi-vol">
<span class="kpi-icon">🚪</span>
<span class="kpi-label">По собств. желанию</span>
<span class="kpi-value">348</span>
<span class="kpi-sub">49% от общего</span>
</div>
<div class="kpi kpi-agr">
<span class="kpi-icon">🤝</span>
<span class="kpi-label">По соглашению сторон</span>
<span class="kpi-value">302</span>
<span class="kpi-sub">43% от общего</span>
</div>
<div class="kpi kpi-dept">
<span class="kpi-icon">🏢</span>
<span class="kpi-label">Топ направление</span>
<span class="kpi-value">ПРОДАЖИ</span>
<span class="kpi-sub">343 чел · 49%</span>
</div>
<div class="kpi kpi-peak">
<span class="kpi-icon">📈</span>
<span class="kpi-label">Пиковый месяц</span>
<span class="kpi-value">Сен</span>
<span class="kpi-sub">86 увольнений</span>
</div>
<img src="./assets/hero.svg" alt="" class="hero-img" loading="lazy">
</section>
<main id="dashboard" hidden>
<section class="block">
<h2>🔢 Базовые показатели</h2>
<div id="kpi" class="kpi-grid"></div>
</section>
<section class="block">
<h2>🏹 Стратегические KPI</h2>
<div class="inputs">
<label>Вакансий/мес <input type="number" id="inp-vacancies" value="15" min="0"></label>
<label>Ставка ₸/мес <input type="number" id="inp-salary" value="500000" min="0" step="10000"></label>
<label title="Время функций, помеченных 🏢, умножается на это число">
Структурных подразделений (СП)
<input type="number" id="inp-units" value="1" min="1" step="1">
</label>
</div>
<div id="strategic-kpi" class="kpi-grid"></div>
</section>
<section class="block with-mascot">
<img src="./assets/recruiter.svg" alt="" class="mascot" loading="lazy">
<div class="block-body">
<h2>🧑‍💼 Нагрузка на сотрудника (дни и недели)</h2>
<div id="workload-kpi" class="kpi-grid"></div>
</div>
</section>
<section class="block with-mascot mascot-right">
<img src="./assets/candidate.svg" alt="" class="mascot" loading="lazy">
<div class="block-body">
<h2>🛠️ Ручные правки времени (без изменения xlsx)</h2>
<p class="hint">Заменить минуты конкретной функции по №. Влияет на все KPI и графики. Пусто = убрать правку.</p>
<div class="ovr-form">
<label>№ функции <input type="number" id="ovr-num" min="1" placeholder="например 39"></label>
<label>Время, мин <input type="number" id="ovr-min" min="0" placeholder="например 60"></label>
<button type="button" id="ovr-apply">💾 Применить</button>
<button type="button" id="ovr-clear" class="secondary">🧹 Очистить все</button>
</div>
<div id="ovr-list" class="ovr-list"></div>
</div>
</section>
<section class="block">
<h2>👥 Распределение на 2 рекрутеров</h2>
<p class="hint">Равномерное распределение текущей нагрузки без изменения данных xlsx.</p>
<div id="two-recruiter-kpi" class="kpi-grid"></div>
<h3 class="section-subhead">📅 Понедельный план месяца</h3>
<div id="chart-weekly-split"></div>
</section>
<section class="block with-mascot">
<img src="./assets/thinker.svg" alt="" class="mascot" loading="lazy">
<div class="block-body">
<h2>🎮 Симулятор «что если»</h2>
<p class="hint">Не меняет исходные данные. Покажет, как изменятся KPI при заданных условиях.</p>
<label class="sim-toggle"><input type="checkbox" id="sim-toggle"> Включить симулятор</label>
<div id="sim-controls" class="sim-controls" hidden>
<div class="sim-row">
<label>Оптимизация всех функций: <span id="sim-optimize-val">0%</span>
<input type="range" id="sim-optimize" min="0" max="80" step="5" value="0">
</label>
</div>
<div class="sim-row">
<label>Исключить топ-N длинных функций: <span id="sim-exclude-top-val">0</span>
<input type="range" id="sim-exclude-top" min="0" max="10" step="1" value="0">
</label>
</div>
<div class="sim-row">
<label>Исключить этап (авто-классификатор):
<select id="sim-stage"></select>
</label>
</div>
<div class="sim-row">
<label>Рекрутеров (FTE): <span id="sim-recruiters-val">1</span>
<input type="range" id="sim-recruiters" min="1" max="10" step="1" value="1">
</label>
<label>Вакансий/мес: <span id="sim-vacancies-val">15</span>
<input type="range" id="sim-vacancies" min="0" max="50" step="1" value="15">
</label>
</div>
<button id="sim-reset" type="button">↺ Сбросить</button>
</div>
<div id="sim-output" hidden></div>
</div>
</section>
<section class="block">
<h2>📡 Сценарий: FTE % vs число вакансий</h2>
<p class="hint">3 линии — 1, 2 и 3 рекрутера. Ромбы ◆ — максимум вакансий при 100% норме. Фиолетовый пунктир — текущий план. Зоны: зелёная ≤100%, жёлтая ≤150%, красная — критично.</p>
<div id="chart-fte-scenario"></div>
</section>
<section class="block">
<h2>⏳ Топ-10 самых долгих функций</h2>
<div id="chart-top10"></div>
</section>
<section class="block">
<h2>🎯 Pareto: накопительный % времени</h2>
<p class="hint">Какие функции дают 80% всей нагрузки. Жёлтая линия — порог 80%.</p>
<div id="chart-pareto"></div>
</section>
<section class="charts-row">
<div class="block">
<h2>📶 Гистограмма длительности</h2>
<div id="chart-histogram"></div>
</div>
<div class="block">
<h2>🗂️ Категории функций</h2>
<div id="chart-duration"></div>
</div>
</section>
<section class="block">
<h2>🔄 Авто-этапы (по ключевым словам в названии)</h2>
<p class="hint">Эвристика — не правит данные. Привлечение / Скрининг / Интервью / Оффер / Адаптация / Документы-СПП / Коммуникация.</p>
<div id="chart-stages"></div>
</section>
<section class="block">
<h2>🌳 Treemap: группа → функция (площадь = время)</h2>
<div id="chart-treemap"></div>
</section>
<section class="block with-mascot mascot-right mascot-wide">
<img src="./assets/team.svg" alt="" class="mascot" loading="lazy">
<div class="block-body">
<h2>📑 Все функции <span class="hint">(клик по заголовку — сортировка)</span></h2>
<table id="table">
<thead><tr>
<th data-col="num">№</th>
<th data-col="name">Функция</th>
<th data-col="rawTime">Исх. время</th>
<th data-col="minutes">Мин</th>
<th data-col="stage">Этап</th>
<th data-col="group">Группа</th>
</tr></thead>
<tbody></tbody>
</table>
</div>
</section>
</main>
<div id="placeholder">
<img src="./assets/empty.svg" alt="" loading="lazy">
<p>Загрузка данных… Если дашборд не появился — нажмите «📁 Загрузить .xlsx».</p>
</div>
<script src="./app.js?v=11"></script>
<!-- Row 1: Monthly dynamics -->
<div class="section-title">Динамика по месяцам</div>
<div class="card">
<div class="card-title"><span class="dot" style="background:var(--navy)"></span>Количество увольнений по месяцам · 2025</div>
<div class="chart-wrap"><canvas id="chartMonthly"></canvas></div>
</div>
<!-- Row 2: Reasons + Departments -->
<div class="section-title">Причины увольнений и направления</div>
<div class="grid-2">
<div class="card">
<div class="card-title"><span class="dot" style="background:var(--red)"></span>Причины увольнений</div>
<table>
<thead><tr><th>#</th><th>Причина</th><th>Кол-во</th><th>Доля</th></tr></thead>
<tbody id="tbReasons"></tbody>
</table>
</div>
<div class="card">
<div class="card-title"><span class="dot" style="background:var(--teal)"></span>По направлениям</div>
<div class="chart-wrap"><canvas id="chartDept"></canvas></div>
</div>
</div>
<!-- Row 3: Region + Gender + Grade -->
<div class="section-title">Регионы · Гендер · Грейды</div>
<div class="grid-3">
<div class="card">
<div class="card-title"><span class="dot" style="background:var(--orange)"></span>По регионам</div>
<div class="chart-wrap"><canvas id="chartRegion"></canvas></div>
</div>
<div class="card">
<div class="card-title"><span class="dot" style="background:var(--purple)"></span>Гендерный состав</div>
<div class="gender-wrap">
<div class="chart-wrap"><canvas id="chartGender" style="max-height:180px"></canvas></div>
<div class="gender-legend">
<div class="gl-item">
<span class="gl-dot" style="background:#1B2A4A"></span>
<span class="gl-label">Женщины</span>
<span class="gl-val">363</span>
<span class="gl-pct">51.5%</span>
</div>
<div class="gl-item">
<span class="gl-dot" style="background:#C0392B"></span>
<span class="gl-label">Мужчины</span>
<span class="gl-val">341</span>
<span class="gl-pct">48.4%</span>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-title"><span class="dot" style="background:var(--green)"></span>По грейдам (топ-8)</div>
<div class="chart-wrap"><canvas id="chartGrade"></canvas></div>
</div>
</div>
<!-- Row 4: Top positions -->
<div class="section-title">Топ должностей по увольнениям</div>
<div class="card">
<div class="card-title"><span class="dot" style="background:var(--purple)"></span>Топ-10 должностей</div>
<table>
<thead><tr><th>#</th><th>Должность</th><th>Кол-во</th><th>Доля от общего</th></tr></thead>
<tbody id="tbPositions"></tbody>
</table>
</div>
</main>
<footer>Данные: Текучесть персонала за 11 месяцев 2025 &nbsp;|&nbsp; Всего записей: 705 &nbsp;|&nbsp; 01.01.2025 – 29.11.2025</footer>
<script>
// ── Data ────────────────────────────────────────────────────────────────────
const DATA = {
monthly: {
labels: ['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя'],
values: [55, 57, 46, 71, 65, 72, 77, 75, 86, 67, 33]
},
reasons: [
{ label: 'По собственному желанию', count: 348 },
{ label: 'По соглашению сторон', count: 302 },
{ label: 'Уход на пенсию', count: 27 },
{ label: 'Преждевременный уход на пенсию', count: 12 },
{ label: 'Истечение срока договора', count: 8 },
{ label: 'Смерть сотрудника', count: 3 },
{ label: 'Нарушение труд. дисципл.', count: 2 },
{ label: 'Перевод в филиал / Сокращение', count: 2 },
],
depts: {
labels: ['ПРОДАЖИ','РиПС','Customer\nservice','ФИНАНСЫ','МАРКЕТИНГ','ЕХ','РУКОВОДСТВО'],
values: [343, 169, 138, 40, 12, 2, 1]
},
regions: {
labels: ['АЛМАТЫ','ЮГ','ЦЕНТР','ЗАПАД','ВОСТОК','СЕВЕР'],
values: [535, 60, 57, 23, 21, 9]
},
grades: {
labels: ['B1','A10','A9','B2','B3','B4','B5','B6'],
values: [207, 191, 119, 108, 28, 16, 13, 10]
},
gender: { labels: ['Женщины','Мужчины'], values: [363, 341] },
positions: [
'Специалист по обслуживанию',
'Хантер',
'Инсталлятор',
'Техник по работе с абонентами',
'Специалист',
'Вед. специалист по обслуживанию',
'Электромонтер Р4',
'Электромонтер Р7',
'Инженер',
'Главный специалист',
],
positionCounts: [193, 102, 77, 58, 49, 28, 21, 18, 14, 11]
};
const TOTAL = 705;
const NAVY = '#1B2A4A';
const RED = '#C0392B';
const TEAL = '#0D9488';
const ORANGE = '#D97706';
const PURPLE = '#7C3AED';
const GREEN = '#059669';
const BLUE = '#2563EB';
Chart.defaults.font.family = "'Segoe UI', Arial, sans-serif";
Chart.defaults.color = '#475569';
// ── Helpers ─────────────────────────────────────────────────────────────────
function pct(n) { return (n / TOTAL * 100).toFixed(1) + '%'; }
function makeBar(el, labels, values, color, opts = {}) {
return new Chart(el, {
type: 'bar',
data: {
labels,
datasets: [{ data: values, backgroundColor: color,
borderRadius: opts.horizontal ? 4 : 6,
borderSkipped: false }]
},
options: {
indexAxis: opts.horizontal ? 'y' : 'x',
responsive: true, maintainAspectRatio: true,
plugins: {
legend: { display: false },
tooltip: { callbacks: {
label: ctx => ` ${ctx.parsed[opts.horizontal ? 'x' : 'y']} чел. (${pct(ctx.parsed[opts.horizontal ? 'x' : 'y'])})`
}}
},
scales: {
x: { grid: { color: '#E2E8F0' }, ticks: { font: { size: 11 } } },
y: { grid: { color: opts.horizontal ? '#E2E8F0' : 'transparent' },
ticks: { font: { size: 11 } } }
}
}
});
}
// ── Monthly chart ────────────────────────────────────────────────────────────
new Chart(document.getElementById('chartMonthly'), {
type: 'bar',
data: {
labels: DATA.monthly.labels,
datasets: [{
label: 'Уволено',
data: DATA.monthly.values,
backgroundColor: DATA.monthly.values.map((v, i) =>
v === Math.max(...DATA.monthly.values) ? RED : NAVY),
borderRadius: 6,
borderSkipped: false,
}, {
type: 'line',
label: 'Тренд',
data: DATA.monthly.values,
borderColor: ORANGE,
backgroundColor: 'transparent',
pointBackgroundColor: ORANGE,
pointRadius: 4,
tension: 0.35,
borderWidth: 2,
borderDash: [6, 4],
pointHoverRadius: 6,
}]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: {
legend: { position: 'top', labels: { boxWidth: 14, font: { size: 12 } } },
tooltip: { callbacks: {
label: ctx => ` ${ctx.parsed.y} чел.`
}}
},
scales: {
x: { grid: { display: false } },
y: { grid: { color: '#E2E8F0' },
ticks: { font: { size: 11 } } }
}
}
});
document.querySelector('#chartMonthly').parentElement.style.height = '260px';
// ── Dept chart ────────────────────────────────────────────────────────────
makeBar(
document.getElementById('chartDept'),
DATA.depts.labels,
DATA.depts.values,
[NAVY, TEAL, BLUE, ORANGE, PURPLE, RED, GREEN],
{ horizontal: true }
);
// ── Region chart ────────────────────────────────────────────────────────────
new Chart(document.getElementById('chartRegion'), {
type: 'bar',
data: {
labels: DATA.regions.labels,
datasets: [{
data: DATA.regions.values,
backgroundColor: [NAVY, TEAL, BLUE, ORANGE, PURPLE, RED],
borderRadius: 6,
borderSkipped: false,
}]
},
options: {
responsive: true, maintainAspectRatio: true,
plugins: {
legend: { display: false },
tooltip: { callbacks: {
label: ctx => ` ${ctx.parsed.y} чел. (${pct(ctx.parsed.y)})`
}}
},
scales: {
x: { grid: { display: false }, ticks: { font: { size: 11 } } },
y: { grid: { color: '#E2E8F0' }, ticks: { font: { size: 11 } } }
}
}
});
// ── Gender donut ─────────────────────────────────────────────────────────────
new Chart(document.getElementById('chartGender'), {
type: 'doughnut',
data: {
labels: DATA.gender.labels,
datasets: [{
data: DATA.gender.values,
backgroundColor: [NAVY, RED],
borderWidth: 3,
borderColor: '#fff',
hoverOffset: 8
}]
},
options: {
responsive: true, maintainAspectRatio: true,
cutout: '60%',
plugins: {
legend: { display: false },
tooltip: { callbacks: {
label: ctx => ` ${ctx.parsed} чел. (${pct(ctx.parsed)})`
}}
}
}
});
// ── Grade chart ────────────────────────────────────────────────────────────
new Chart(document.getElementById('chartGrade'), {
type: 'bar',
data: {
labels: DATA.grades.labels,
datasets: [{
data: DATA.grades.values,
backgroundColor: [NAVY,RED,TEAL,BLUE,ORANGE,PURPLE,GREEN,'#94A3B8'],
borderRadius: 5,
borderSkipped: false,
}]
},
options: {
indexAxis: 'y',
responsive: true, maintainAspectRatio: true,
plugins: {
legend: { display: false },
tooltip: { callbacks: {
label: ctx => ` ${ctx.parsed.x} чел. (${pct(ctx.parsed.x)})`
}}
},
scales: {
x: { grid: { color: '#E2E8F0' }, ticks: { font: { size: 11 } } },
y: { grid: { display: false }, ticks: { font: { size: 12 } } }
}
}
});
// ── Reasons table ────────────────────────────────────────────────────────────
const reasonColors = [RED, NAVY, TEAL, ORANGE, BLUE, '#6B7280', '#6B7280', '#6B7280'];
const tbody = document.getElementById('tbReasons');
DATA.reasons.forEach((r, i) => {
const p = (r.count / TOTAL * 100).toFixed(1);
const tr = document.createElement('tr');
tr.innerHTML = `
<td style="color:var(--muted);font-weight:700;width:28px">${i+1}</td>
<td>${r.label}</td>
<td style="font-weight:700;color:${reasonColors[i]};white-space:nowrap">${r.count}</td>
<td>
<div class="bar-wrap">
<div class="bar-track">
<div class="bar-fill" style="width:${p}%;background:${reasonColors[i]}"></div>
</div>
<span class="bar-pct">${p}%</span>
</div>
</td>`;
tbody.appendChild(tr);
});
// ── Positions table ───────────────────────────────────────────────────────────
const posColors = [RED, NAVY, TEAL, ORANGE, BLUE, PURPLE, GREEN, '#0891B2', '#B45309', '#6B7280'];
const tbody2 = document.getElementById('tbPositions');
DATA.positions.forEach((pos, i) => {
const cnt = DATA.positionCounts[i];
const p = (cnt / TOTAL * 100).toFixed(1);
const tr = document.createElement('tr');
tr.innerHTML = `
<td style="color:var(--muted);font-weight:700;width:28px">${i+1}</td>
<td>${pos}</td>
<td style="font-weight:700;color:${posColors[i]};white-space:nowrap">${cnt}</td>
<td>
<div class="bar-wrap">
<div class="bar-track">
<div class="bar-fill" style="width:${(p/27.4*100)}%;background:${posColors[i]}"></div>
</div>
<span class="bar-pct">${p}%</span>
</div>
</td>`;
tbody2.appendChild(tr);
});
</script>
</body>
</html>