diff --git a/car_sales_kz_dashboard-1.xlsx b/car_sales_kz_dashboard-1.xlsx new file mode 100644 index 0000000..eeb22ef Binary files /dev/null and b/car_sales_kz_dashboard-1.xlsx differ diff --git a/index.html b/index.html index 4743dda..b322fca 100644 --- a/index.html +++ b/index.html @@ -391,7 +391,7 @@

📊 Дашборд продаж автомобилей

-

Анализ продаж по регионам Казахстана

+

Kaisar KZ-7 · Казахстан · последние 30 дней

@@ -458,22 +458,22 @@
+
+
📈 Динамика продаж по дням
+
+ +
+
📍 Продажи по регионам (Топ-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;