370 lines
16 KiB
JavaScript
370 lines
16 KiB
JavaScript
(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 '<div class="kpi-card"><div class="kpi-label">' + k.label + '</div><div class="kpi-value ' + k.cls + '">' + k.value + '</div></div>';
|
||
}).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 '<tr data-service="' + s.id + '">' +
|
||
'<td class="service-name" onclick="window.selectService(\'' + s.id + '\')">' + s.name + '</td>' +
|
||
'<td>' + total.toFixed(1) + '</td>' +
|
||
'<td>' + b2c.toFixed(1) + '</td>' +
|
||
'<td>' + b2b.toFixed(1) + '</td>' +
|
||
'<td>' + share.toFixed(1) + '</td>' +
|
||
'<td class="' + growthCls + '">' + formatPercent(growth) + '</td>' +
|
||
'<td class="' + cagrCls + '">' + formatPercent(svcCagr) + '</td>' +
|
||
'</tr>';
|
||
});
|
||
|
||
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 '<div class="conclusion-item"><div class="conclusion-bullet"></div><span>' + c + '</span></div>';
|
||
}).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();
|
||
})();
|