(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 '';
}).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();
})();