+
+
📈 Динамика продаж по дням
+
+
+
+
📍 Продажи по регионам (Топ-10)
-
-
🎯 Продажи по комплектациям
-
-
-
-
-
-
🎨 Предпочтения по цветам
-
-
+
+
🎯 Продажи по комплектациям и цветам
+
+
@@ -482,6 +482,12 @@
+
+
🏆 Рейтинг регионов (Топ-10)
+
+
+
+
@@ -531,22 +537,32 @@
async function loadExcelData() {
try {
- const response = await fetch('car_sales_kz_dashboard.xlsx');
+ const response = await fetch('car_sales_kz_dashboard-1.xlsx');
const arrayBuffer = await response.arrayBuffer();
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
- const firstSheet = workbook.Sheets[workbook.SheetNames[0]];
- const rawData = XLSX.utils.sheet_to_json(firstSheet, { header: 1 });
- const headers = rawData[0];
- allData = rawData.slice(1).map(row => ({
- regionCode: row[0],
+ const rawSheet = workbook.Sheets['Сырые данные'];
+ const rawData = XLSX.utils.sheet_to_json(rawSheet, { header: 1 });
+
+ allData = rawData.slice(3).map(row => ({
+ date: row[0],
region: row[1],
trim: row[2],
color: row[3],
units: row[4],
price: row[5],
total: row[6]
- })).filter(row => row.region && row.trim);
+ })).filter(row => row.region && row.trim && row.units);
+
+ const overviewSheet = workbook.Sheets['Обзор'];
+ const overviewData = XLSX.utils.sheet_to_json(overviewSheet, { header: 1 });
+
+ const kpiRow = overviewData[4];
+ window.kpiData = {
+ totalUnits: kpiRow?.[1] || 0,
+ totalRevenue: kpiRow?.[4] || 0,
+ period: overviewData[3]?.[1] || ''
+ };
filteredData = [...allData];
initDashboard();
@@ -580,17 +596,75 @@
const avgCheck = totalUnits > 0 ? totalRevenue / totalUnits : 0;
const regions = new Set(filteredData.map(d => d.region)).size;
- document.getElementById('totalUnits').textContent = formatNumber(totalUnits);
- document.getElementById('totalRevenue').textContent = formatCurrency(totalRevenue).replace('KZT', '₸');
+ document.getElementById('totalUnits').textContent = formatNumber(window.kpiData?.totalUnits || totalUnits);
+ document.getElementById('totalRevenue').textContent = formatCurrency(window.kpiData?.totalRevenue || totalRevenue).replace('KZT', '₸');
document.getElementById('avgCheck').textContent = formatCurrency(avgCheck).replace('KZT', '₸');
document.getElementById('totalRegions').textContent = regions;
+
+ if (window.kpiData?.period) {
+ document.getElementById('periodLabel').textContent = window.kpiData.period;
+ }
}
function updateCharts() {
+ updateSalesTrendChart();
updateRegionChart();
- updateTrimChart();
- updateColorChart();
+ updateTrimColorChart();
updateRevenueChart();
+ updateRatingChart();
+ }
+
+ function updateSalesTrendChart() {
+ const daysData = {};
+ filteredData.forEach(d => {
+ if (d.date) {
+ daysData[d.date] = (daysData[d.date] || 0) + d.units;
+ }
+ });
+
+ const sortedDates = Object.keys(daysData).sort((a, b) => a - b);
+ const labels = sortedDates.map(date => {
+ const day = new Date((date - 25569) * 86400 * 1000);
+ return day.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit' });
+ });
+
+ const ctx = document.getElementById('salesTrendChart').getContext('2d');
+
+ if (charts.trend) charts.trend.destroy();
+
+ charts.trend = new Chart(ctx, {
+ type: 'line',
+ data: {
+ labels: labels,
+ datasets: [{
+ label: 'Продано авто',
+ data: sortedDates.map(date => daysData[date]),
+ borderColor: 'rgba(102, 126, 234, 1)',
+ backgroundColor: 'rgba(102, 126, 234, 0.1)',
+ borderWidth: 3,
+ fill: true,
+ tension: 0.4,
+ pointRadius: 4,
+ pointBackgroundColor: 'rgba(102, 126, 234, 1)'
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: { display: false }
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ grid: { color: 'rgba(0, 0, 0, 0.05)' }
+ },
+ x: {
+ grid: { display: false }
+ }
+ }
+ }
+ });
}
function updateRegionChart() {
@@ -639,74 +713,57 @@
});
}
- function updateTrimChart() {
+ function updateTrimColorChart() {
const trimData = {};
+ const colorData = {};
+ const trimColorData = {};
+
filteredData.forEach(d => {
trimData[d.trim] = (trimData[d.trim] || 0) + d.units;
- });
-
- const ctx = document.getElementById('trimChart').getContext('2d');
-
- if (charts.trim) charts.trim.destroy();
-
- charts.trim = new Chart(ctx, {
- type: 'doughnut',
- data: {
- labels: Object.keys(trimData),
- datasets: [{
- data: Object.values(trimData),
- backgroundColor: [
- 'rgba(102, 126, 234, 0.8)',
- 'rgba(118, 75, 162, 0.8)',
- 'rgba(237, 137, 54, 0.8)'
- ],
- borderWidth: 2,
- borderColor: 'rgba(255, 255, 255, 1)'
- }]
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- plugins: {
- legend: {
- position: 'bottom'
- }
- }
- }
- });
- }
-
- function updateColorChart() {
- const colorData = {};
- filteredData.forEach(d => {
colorData[d.color] = (colorData[d.color] || 0) + d.units;
+ const key = `${d.trim} - ${d.color}`;
+ trimColorData[key] = (trimColorData[key] || 0) + d.units;
});
- const ctx = document.getElementById('colorChart').getContext('2d');
+ const ctx = document.getElementById('trimColorChart').getContext('2d');
- if (charts.color) charts.color.destroy();
+ if (charts.trimColor) charts.trimColor.destroy();
- charts.color = new Chart(ctx, {
- type: 'pie',
+ const sortedData = Object.entries(trimColorData)
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, 9);
+
+ charts.trimColor = new Chart(ctx, {
+ type: 'bar',
data: {
- labels: Object.keys(colorData),
+ labels: sortedData.map(([key]) => key),
datasets: [{
- data: Object.values(colorData),
- backgroundColor: [
- 'rgba(255, 255, 255, 0.9)',
- 'rgba(26, 32, 44, 0.9)',
- 'rgba(203, 213, 224, 0.9)'
- ],
- borderWidth: 2,
- borderColor: 'rgba(102, 126, 234, 1)'
+ label: 'Продано авто',
+ data: sortedData.map(([, value]) => value),
+ backgroundColor: sortedData.map(([key]) => {
+ const trim = key.split(' - ')[0];
+ if (trim === 'Standard') return 'rgba(102, 126, 234, 0.8)';
+ if (trim === 'Comfort') return 'rgba(118, 75, 162, 0.8)';
+ return 'rgba(237, 137, 54, 0.8)';
+ }),
+ borderWidth: 1,
+ borderColor: 'rgba(255, 255, 255, 1)',
+ borderRadius: 8
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
- legend: {
- position: 'bottom'
+ legend: { display: false }
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ grid: { color: 'rgba(0, 0, 0, 0.05)' }
+ },
+ x: {
+ grid: { display: false }
}
}
}
@@ -716,7 +773,7 @@
function updateRevenueChart() {
const revenueData = {};
filteredData.forEach(d => {
- revenueData[d.region] = (revenueData[d.region] || 0) + d.total;
+ revenueData[d.region] = (revenueData[d.region] || 0) + (d.total || 0);
});
const sorted = Object.entries(revenueData)
@@ -762,6 +819,59 @@
});
}
+ function updateRatingChart() {
+ const regionData = {};
+ filteredData.forEach(d => {
+ regionData[d.region] = (regionData[d.region] || 0) + d.units;
+ });
+
+ const sorted = Object.entries(regionData)
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, 10);
+
+ const total = Object.values(regionData).reduce((a, b) => a + b, 0);
+
+ const ctx = document.getElementById('ratingChart').getContext('2d');
+
+ if (charts.rating) charts.rating.destroy();
+
+ charts.rating = new Chart(ctx, {
+ type: 'bar',
+ data: {
+ labels: sorted.map(([name]) => name),
+ datasets: [{
+ label: 'Доля рынка (%)',
+ data: sorted.map(([, value]) => ((value / total) * 100).toFixed(1)),
+ backgroundColor: sorted.map(([, , i], idx) => {
+ if (idx === 0) return 'rgba(237, 137, 54, 0.9)';
+ if (idx === 1) return 'rgba(118, 75, 162, 0.9)';
+ if (idx === 2) return 'rgba(102, 126, 234, 0.9)';
+ return 'rgba(203, 213, 224, 0.9)';
+ }),
+ borderWidth: 1,
+ borderColor: 'rgba(255, 255, 255, 1)',
+ borderRadius: 8
+ }]
+ },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: { display: false }
+ },
+ scales: {
+ y: {
+ beginAtZero: true,
+ grid: { color: 'rgba(0, 0, 0, 0.05)' }
+ },
+ x: {
+ grid: { display: false }
+ }
+ }
+ }
+ });
+ }
+
function updateTable() {
const startIndex = (currentPage - 1) * rowsPerPage;
const endIndex = startIndex + rowsPerPage;