sozday-stranitsu-rynok-telek/script.js

370 lines
16 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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