(function() { var data = null; var mainChart = null; var serviceChart = null; var currentMode = 'absolute'; function formatBillion(val, decimals) { if (val === null || val === undefined) return '—'; var bn = val / 1000; return bn.toFixed(decimals || 1).replace(/\B(?=(\d{3})+(?!\d))/g, '\u00A0') + ' млрд ₸'; } function formatPercent(val) { if (val === null || val === undefined) return '—'; var sign = val > 0 ? '+' : ''; return sign + val.toFixed(1).replace('.', ',') + '%'; } function cagr(startVal, endVal, years) { if (startVal <= 0 || endVal <= 0) return 0; return (Math.pow(endVal / startVal, 1 / years) - 1) * 100; } function yoy(curr, prev) { if (!prev || prev === 0) return 0; return ((curr - prev) / prev) * 100; } async function loadData() { var resp = await fetch('data.json'); data = await resp.json(); init(); } function init() { populateServiceFilter(); renderKPIs(); renderMainChart(); renderServicesTable(); renderServiceChart(); renderConclusions(); bindEvents(); } function populateServiceFilter() { var sel = document.getElementById('filterService'); data.services.forEach(function(s) { var opt = document.createElement('option'); opt.value = s.id; opt.textContent = s.name; sel.appendChild(opt); }); } function getFilteredYear() { var v = document.getElementById('filterYear').value; return v === 'all' ? '2025' : v; } function getFilteredSegment() { return document.getElementById('filterSegment').value; } function getFilteredService() { return document.getElementById('filterService').value; } function renderKPIs() { var year = getFilteredYear(); var total = data.total[year]; var b2c = data.b2c[year]; var b2b = data.b2b[year]; var total2022 = data.total['2022']; var total2025 = data.total['2025']; var totalPrev = data.total['2024']; var b2cShare = (b2c / total * 100); var b2bShare = (b2b / total * 100); var yoyGrowth = yoy(total, totalPrev); var cagrVal = cagr(total2022, total2025, 3); var absGrowth = (total2025 - total2022) / 1000; var kpis = [ { label: 'Общий рынок (' + year + ')', value: formatBillion(total), cls: '' }, { label: 'B2C — население', value: formatBillion(b2c), cls: '' }, { label: 'B2B — бизнес', value: formatBillion(b2b), cls: '' }, { label: 'Доля B2C', value: b2cShare.toFixed(1) + '%', cls: '' }, { label: 'Доля B2B', value: b2bShare.toFixed(1) + '%', cls: '' }, { label: 'Рост к пред. году', value: formatPercent(yoyGrowth), cls: yoyGrowth >= 0 ? 'positive' : 'negative' }, { label: 'CAGR (4 года)', value: formatPercent(cagrVal), cls: cagrVal >= 0 ? 'positive' : 'negative' }, { label: 'Прирост за 4 года', value: (absGrowth >= 0 ? '+' : '') + absGrowth.toFixed(1) + ' млрд ₸', cls: absGrowth >= 0 ? 'positive' : 'negative' } ]; var grid = document.getElementById('kpiGrid'); grid.innerHTML = kpis.map(function(k) { return '
' + k.label + '
' + k.value + '
'; }).join(''); } function renderMainChart() { var ctx = document.getElementById('mainChart').getContext('2d'); var years = data.years; var totalData = years.map(function(y) { return data.total[String(y)] / 1000; }); var b2cData = years.map(function(y) { return data.b2c[String(y)] / 1000; }); var b2bData = years.map(function(y) { return data.b2b[String(y)] / 1000; }); if (mainChart) mainChart.destroy(); var isGrowth = currentMode === 'growth'; var datasets = []; if (isGrowth) { var totalGrowth = [null]; var b2cGrowth = [null]; var b2bGrowth = [null]; for (var i = 1; i < years.length; i++) { totalGrowth.push(yoy(data.total[String(years[i])], data.total[String(years[i-1])])); b2cGrowth.push(yoy(data.b2c[String(years[i])], data.b2c[String(years[i-1])])); b2bGrowth.push(yoy(data.b2b[String(years[i])], data.b2b[String(years[i-1])])); } datasets = [ { label: 'Общий рынок', data: totalGrowth, borderColor: '#ea4335', backgroundColor: 'rgba(234,67,53,0.1)', fill: false, tension: 0.3 }, { label: 'B2C', data: b2cGrowth, borderColor: '#4285f4', backgroundColor: 'rgba(64,133,244,0.1)', fill: false, tension: 0.3 }, { label: 'B2B', data: b2bGrowth, borderColor: '#34a853', backgroundColor: 'rgba(52,168,83,0.1)', fill: false, tension: 0.3 } ]; } else { datasets = [ { label: 'Общий рынок', data: totalData, borderColor: '#ea4335', backgroundColor: 'rgba(234,67,53,0.1)', fill: true, tension: 0.3 }, { label: 'B2C', data: b2cData, borderColor: '#4285f4', backgroundColor: 'rgba(64,133,244,0.05)', fill: true, tension: 0.3 }, { label: 'B2B', data: b2bData, borderColor: '#34a853', backgroundColor: 'rgba(52,168,83,0.05)', fill: true, tension: 0.3 } ]; } mainChart = new Chart(ctx, { type: 'line', data: { labels: years.map(String), datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, plugins: { legend: { position: 'top', labels: { usePointStyle: true, padding: 20 } }, tooltip: { callbacks: { label: function(ctx) { var label = ctx.dataset.label || ''; if (isGrowth) { return label + ': ' + formatPercent(ctx.parsed.y); } return label + ': ' + ctx.parsed.y.toFixed(1) + ' млрд ₸'; }, afterBody: function(tooltipItems) { if (tooltipItems.length === 0 || isGrowth) return []; var idx = tooltipItems[0].dataIndex; var year = String(years[idx]); var t = data.total[year] / 1000; var c = data.b2c[year] / 1000; var b = data.b2b[year] / 1000; var lines = [ '', 'Рост к пред. году: ' + formatPercent(idx > 0 ? yoy(data.total[year], data.total[String(years[idx-1])]) : 0), 'Доля B2C: ' + (data.b2c[year] / data.total[year] * 100).toFixed(1) + '%', 'Доля B2B: ' + (data.b2b[year] / data.total[year] * 100).toFixed(1) + '%' ]; return lines; } } } }, scales: { x: { grid: { display: false } }, y: { beginAtZero: !isGrowth, ticks: { callback: function(v) { return isGrowth ? formatPercent(v) : v.toFixed(0) + ' млрд'; } }, grid: { color: 'rgba(0,0,0,0.05)' } } } } }); } function renderServicesTable() { var year = document.getElementById('serviceYearFilter').value; var totalMarket = data.total[year]; var prevYear = String(parseInt(year) - 1); var tbody = document.getElementById('servicesBody'); var rows = data.services.map(function(s) { var total = s.total[year] / 1000; var b2c = (s.b2c[year] || 0) / 1000; var b2b = (s.b2b[year] || 0) / 1000; var share = s.total[year] / totalMarket * 100; var growth = 0; if (data.total[prevYear] && s.total[prevYear]) { growth = yoy(s.total[year], s.total[prevYear]); } var svcCagr = cagr(s.total['2022'], s.total['2025'], 3); var growthCls = growth > 0 ? 'cell-positive' : growth < 0 ? 'cell-negative' : 'cell-neutral'; var cagrCls = svcCagr > 0 ? 'cell-positive' : svcCagr < 0 ? 'cell-negative' : 'cell-neutral'; return '' + '' + s.name + '' + '' + total.toFixed(1) + '' + '' + b2c.toFixed(1) + '' + '' + b2b.toFixed(1) + '' + '' + share.toFixed(1) + '' + '' + formatPercent(growth) + '' + '' + formatPercent(svcCagr) + '' + ''; }); tbody.innerHTML = rows.join(''); } window.selectService = function(serviceId) { document.getElementById('filterService').value = serviceId; renderServiceChart(); }; function renderServiceChart() { var ctx = document.getElementById('serviceChart').getContext('2d'); var serviceId = getFilteredService(); var years = data.years; if (serviceChart) serviceChart.destroy(); var labels = years.map(String); var dTotal, dB2c, dB2b; if (serviceId !== 'all') { var svc = data.services.find(function(s) { return s.id === serviceId; }); if (svc) { dTotal = years.map(function(y) { return svc.total[String(y)] / 1000; }); dB2c = years.map(function(y) { return (svc.b2c[String(y)] || 0) / 1000; }); dB2b = years.map(function(y) { return (svc.b2b[String(y)] || 0) / 1000; }); } } else { dTotal = years.map(function(y) { return data.total[String(y)] / 1000; }); dB2c = years.map(function(y) { return data.b2c[String(y)] / 1000; }); dB2b = years.map(function(y) { return data.b2b[String(y)] / 1000; }); } var title = serviceId !== 'all' ? (data.services.find(function(s) { return s.id === serviceId; }) || {}).name || '' : 'Все услуги'; serviceChart = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [ { label: 'Общий рынок', data: dTotal, backgroundColor: 'rgba(234,67,53,0.7)', borderRadius: 4 }, { label: 'B2C', data: dB2c, backgroundColor: 'rgba(64,133,244,0.7)', borderRadius: 4 }, { label: 'B2B', data: dB2b, backgroundColor: 'rgba(52,168,83,0.7)', borderRadius: 4 } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: !!title, text: title, font: { size: 14, weight: '500' } }, legend: { position: 'top', labels: { usePointStyle: true } }, tooltip: { callbacks: { label: function(ctx) { return ctx.dataset.label + ': ' + ctx.parsed.y.toFixed(1) + ' млрд ₸'; } } } }, scales: { x: { grid: { display: false } }, y: { beginAtZero: true, ticks: { callback: function(v) { return v.toFixed(0) + ' млрд'; } }, grid: { color: 'rgba(0,0,0,0.05)' } } } } }); } function renderConclusions() { var t22 = data.total['2022'] / 1000; var t25 = data.total['2025'] / 1000; var cagrVal = cagr(data.total['2022'], data.total['2025'], 3); var totalGrowth = ((t25 - t22) / t22 * 100); var b2cShare25 = data.b2c['2025'] / data.total['2025'] * 100; var b2bShare25 = data.b2b['2025'] / data.total['2025'] * 100; var servicesWithCagr = data.services.map(function(s) { return { name: s.name, cagr: cagr(s.total['2022'], s.total['2025'], 3), growth25: yoy(s.total['2025'], s.total['2024']) }; }); var fastestCagr = servicesWithCagr.sort(function(a, b) { return b.cagr - a.cagr; })[0]; var fastestGrowth = servicesWithCagr.sort(function(a, b) { return b.growth25 - a.growth25; })[0]; var internetSvc = data.services.find(function(s) { return s.id === 'internet'; }); var internetShare22 = internetSvc.total['2022'] / data.total['2022'] * 100; var internetShare25 = internetSvc.total['2025'] / data.total['2025'] * 100; var conclusions = [ 'Рынок вырос с ' + t22.toFixed(1) + ' млрд ₸ в 2022 году до ' + t25.toFixed(1) + ' млрд ₸ в 2025 году', 'Совокупный рост за 4 года составил ' + formatPercent(totalGrowth), 'CAGR рынка составил ' + formatPercent(cagrVal), 'Сегмент B2C занимает ' + b2cShare25.toFixed(1) + '% рынка (' + formatBillion(data.b2c['2025']) + ')', 'Сегмент B2B занимает ' + b2bShare25.toFixed(1) + '% рынка (' + formatBillion(data.b2b['2025']) + ')', 'Наиболее быстрорастущий сегмент по CAGR — ' + fastestCagr.name + ' (' + formatPercent(fastestCagr.cagr) + ')', 'Максимальный рост в 2025 году — ' + fastestGrowth.name + ' (' + formatPercent(fastestGrowth.growth25) + ')', 'Доля интернет-услуг изменилась с ' + internetShare22.toFixed(1) + '% в 2022 до ' + internetShare25.toFixed(1) + '% в 2025' ]; var list = document.getElementById('conclusionsList'); list.innerHTML = conclusions.map(function(c) { return '
' + c + '
'; }).join(''); } function bindEvents() { document.getElementById('filterYear').addEventListener('change', function() { renderKPIs(); }); document.getElementById('filterSegment').addEventListener('change', function() { renderKPIs(); renderServicesTable(); renderServiceChart(); }); document.getElementById('filterService').addEventListener('change', function() { renderServiceChart(); }); document.getElementById('serviceYearFilter').addEventListener('change', function() { renderServicesTable(); }); document.querySelectorAll('.toggle-btn').forEach(function(btn) { btn.addEventListener('click', function() { document.querySelectorAll('.toggle-btn').forEach(function(b) { b.classList.remove('active'); }); btn.classList.add('active'); currentMode = btn.dataset.mode; renderMainChart(); }); }); document.getElementById('resetFilters').addEventListener('click', function() { document.getElementById('filterYear').value = 'all'; document.getElementById('filterSegment').value = 'all'; document.getElementById('filterService').value = 'all'; document.getElementById('serviceYearFilter').value = '2025'; renderKPIs(); renderServicesTable(); renderServiceChart(); }); } loadData(); })();