dashboard: updated with new Excel file data and added trend charts
This commit is contained in:
parent
77de4b9d5c
commit
8ddf5a4d7e
BIN
car_sales_kz_dashboard-1.xlsx
Normal file
BIN
car_sales_kz_dashboard-1.xlsx
Normal file
Binary file not shown.
260
index.html
260
index.html
@ -391,7 +391,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<header>
|
<header>
|
||||||
<h1>📊 Дашборд продаж автомобилей</h1>
|
<h1>📊 Дашборд продаж автомобилей</h1>
|
||||||
<p class="subtitle">Анализ продаж по регионам Казахстана</p>
|
<p class="subtitle">Kaisar KZ-7 · Казахстан · <span id="periodLabel">последние 30 дней</span></p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="kpi-grid" id="kpiGrid">
|
<div class="kpi-grid" id="kpiGrid">
|
||||||
@ -458,22 +458,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="charts-grid">
|
<div class="charts-grid">
|
||||||
|
<div class="chart-card">
|
||||||
|
<div class="chart-title">📈 Динамика продаж по дням</div>
|
||||||
|
<div class="chart-container">
|
||||||
|
<canvas id="salesTrendChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="chart-card">
|
<div class="chart-card">
|
||||||
<div class="chart-title">📍 Продажи по регионам (Топ-10)</div>
|
<div class="chart-title">📍 Продажи по регионам (Топ-10)</div>
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<canvas id="regionChart"></canvas>
|
<canvas id="regionChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-card">
|
<div class="chart-card full-width">
|
||||||
<div class="chart-title">🎯 Продажи по комплектациям</div>
|
<div class="chart-title">🎯 Продажи по комплектациям и цветам</div>
|
||||||
<div class="chart-container">
|
<div class="chart-container" style="height: 350px;">
|
||||||
<canvas id="trimChart"></canvas>
|
<canvas id="trimColorChart"></canvas>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="chart-card">
|
|
||||||
<div class="chart-title">🎨 Предпочтения по цветам</div>
|
|
||||||
<div class="chart-container">
|
|
||||||
<canvas id="colorChart"></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart-card">
|
<div class="chart-card">
|
||||||
@ -482,6 +482,12 @@
|
|||||||
<canvas id="revenueChart"></canvas>
|
<canvas id="revenueChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="chart-card">
|
||||||
|
<div class="chart-title">🏆 Рейтинг регионов (Топ-10)</div>
|
||||||
|
<div class="chart-container">
|
||||||
|
<canvas id="ratingChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-section">
|
<div class="table-section">
|
||||||
@ -531,22 +537,32 @@
|
|||||||
|
|
||||||
async function loadExcelData() {
|
async function loadExcelData() {
|
||||||
try {
|
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 arrayBuffer = await response.arrayBuffer();
|
||||||
const workbook = XLSX.read(arrayBuffer, { type: 'array' });
|
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];
|
const rawSheet = workbook.Sheets['Сырые данные'];
|
||||||
allData = rawData.slice(1).map(row => ({
|
const rawData = XLSX.utils.sheet_to_json(rawSheet, { header: 1 });
|
||||||
regionCode: row[0],
|
|
||||||
|
allData = rawData.slice(3).map(row => ({
|
||||||
|
date: row[0],
|
||||||
region: row[1],
|
region: row[1],
|
||||||
trim: row[2],
|
trim: row[2],
|
||||||
color: row[3],
|
color: row[3],
|
||||||
units: row[4],
|
units: row[4],
|
||||||
price: row[5],
|
price: row[5],
|
||||||
total: row[6]
|
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];
|
filteredData = [...allData];
|
||||||
initDashboard();
|
initDashboard();
|
||||||
@ -580,17 +596,75 @@
|
|||||||
const avgCheck = totalUnits > 0 ? totalRevenue / totalUnits : 0;
|
const avgCheck = totalUnits > 0 ? totalRevenue / totalUnits : 0;
|
||||||
const regions = new Set(filteredData.map(d => d.region)).size;
|
const regions = new Set(filteredData.map(d => d.region)).size;
|
||||||
|
|
||||||
document.getElementById('totalUnits').textContent = formatNumber(totalUnits);
|
document.getElementById('totalUnits').textContent = formatNumber(window.kpiData?.totalUnits || totalUnits);
|
||||||
document.getElementById('totalRevenue').textContent = formatCurrency(totalRevenue).replace('KZT', '₸');
|
document.getElementById('totalRevenue').textContent = formatCurrency(window.kpiData?.totalRevenue || totalRevenue).replace('KZT', '₸');
|
||||||
document.getElementById('avgCheck').textContent = formatCurrency(avgCheck).replace('KZT', '₸');
|
document.getElementById('avgCheck').textContent = formatCurrency(avgCheck).replace('KZT', '₸');
|
||||||
document.getElementById('totalRegions').textContent = regions;
|
document.getElementById('totalRegions').textContent = regions;
|
||||||
|
|
||||||
|
if (window.kpiData?.period) {
|
||||||
|
document.getElementById('periodLabel').textContent = window.kpiData.period;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCharts() {
|
function updateCharts() {
|
||||||
|
updateSalesTrendChart();
|
||||||
updateRegionChart();
|
updateRegionChart();
|
||||||
updateTrimChart();
|
updateTrimColorChart();
|
||||||
updateColorChart();
|
|
||||||
updateRevenueChart();
|
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() {
|
function updateRegionChart() {
|
||||||
@ -639,74 +713,57 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTrimChart() {
|
function updateTrimColorChart() {
|
||||||
const trimData = {};
|
const trimData = {};
|
||||||
|
const colorData = {};
|
||||||
|
const trimColorData = {};
|
||||||
|
|
||||||
filteredData.forEach(d => {
|
filteredData.forEach(d => {
|
||||||
trimData[d.trim] = (trimData[d.trim] || 0) + d.units;
|
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;
|
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, {
|
const sortedData = Object.entries(trimColorData)
|
||||||
type: 'pie',
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, 9);
|
||||||
|
|
||||||
|
charts.trimColor = new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
data: {
|
data: {
|
||||||
labels: Object.keys(colorData),
|
labels: sortedData.map(([key]) => key),
|
||||||
datasets: [{
|
datasets: [{
|
||||||
data: Object.values(colorData),
|
label: 'Продано авто',
|
||||||
backgroundColor: [
|
data: sortedData.map(([, value]) => value),
|
||||||
'rgba(255, 255, 255, 0.9)',
|
backgroundColor: sortedData.map(([key]) => {
|
||||||
'rgba(26, 32, 44, 0.9)',
|
const trim = key.split(' - ')[0];
|
||||||
'rgba(203, 213, 224, 0.9)'
|
if (trim === 'Standard') return 'rgba(102, 126, 234, 0.8)';
|
||||||
],
|
if (trim === 'Comfort') return 'rgba(118, 75, 162, 0.8)';
|
||||||
borderWidth: 2,
|
return 'rgba(237, 137, 54, 0.8)';
|
||||||
borderColor: 'rgba(102, 126, 234, 1)'
|
}),
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 1)',
|
||||||
|
borderRadius: 8
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: { display: false }
|
||||||
position: 'bottom'
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: { color: 'rgba(0, 0, 0, 0.05)' }
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: { display: false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -716,7 +773,7 @@
|
|||||||
function updateRevenueChart() {
|
function updateRevenueChart() {
|
||||||
const revenueData = {};
|
const revenueData = {};
|
||||||
filteredData.forEach(d => {
|
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)
|
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() {
|
function updateTable() {
|
||||||
const startIndex = (currentPage - 1) * rowsPerPage;
|
const startIndex = (currentPage - 1) * rowsPerPage;
|
||||||
const endIndex = startIndex + rowsPerPage;
|
const endIndex = startIndex + rowsPerPage;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user