- Fix deteksi transisi bulan untuk semua bulan (tidak hanya 1 Januari) - Perbaikan validasi data dengan threshold 20% untuk transisi bulan/tahun - Auto-reset chart jika data hourly tidak valid - Setup force local mode untuk testing API lokal - Perbaikan normalisasi dan validasi tanggal - Enhanced logging untuk debugging transisi
235 lines
6.4 KiB
JavaScript
235 lines
6.4 KiB
JavaScript
// public/dashboard/js/charts.js
|
|
// Chart.js helpers: create & update charts without recreating canvases.
|
|
|
|
let dailyLineChart = null;
|
|
let categoryChart = null;
|
|
|
|
// Export chart instances untuk akses dari dashboard.js
|
|
export function getDailyChart() {
|
|
return dailyLineChart;
|
|
}
|
|
|
|
export function getCategoryChart() {
|
|
return categoryChart;
|
|
}
|
|
|
|
export function initDailyChart(ctx) {
|
|
if (dailyLineChart) return dailyLineChart;
|
|
|
|
dailyLineChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: [],
|
|
datasets: [
|
|
{
|
|
label: 'Jumlah',
|
|
data: [],
|
|
borderColor: '#111827',
|
|
backgroundColor: 'rgba(17, 24, 39, 0.06)',
|
|
borderWidth: 2,
|
|
tension: 0.35,
|
|
fill: true,
|
|
pointRadius: 2.5,
|
|
pointBackgroundColor: '#111827'
|
|
},
|
|
{
|
|
label: 'Pendapatan',
|
|
data: [],
|
|
borderColor: '#6b7280',
|
|
backgroundColor: 'rgba(156, 163, 175, 0.08)',
|
|
borderWidth: 2,
|
|
tension: 0.35,
|
|
fill: true,
|
|
pointRadius: 2.5,
|
|
pointBackgroundColor: '#6b7280',
|
|
yAxisID: 'y1'
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
interaction: {
|
|
mode: 'index',
|
|
intersect: false
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
grid: { color: '#e5e7eb' },
|
|
ticks: { font: { size: 11 } }
|
|
},
|
|
y1: {
|
|
beginAtZero: true,
|
|
position: 'right',
|
|
grid: { drawOnChartArea: false },
|
|
ticks: {
|
|
font: { size: 11 },
|
|
callback: value => 'Rp ' + new Intl.NumberFormat('id-ID').format(value)
|
|
}
|
|
},
|
|
x: {
|
|
grid: { display: false },
|
|
ticks: { font: { size: 11 } }
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom',
|
|
labels: {
|
|
boxWidth: 14,
|
|
boxHeight: 4
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
return dailyLineChart;
|
|
}
|
|
|
|
export function updateDailyChart({ labels, counts, amounts }) {
|
|
if (!dailyLineChart) {
|
|
console.warn('[Charts] Daily chart belum di-init, skip update');
|
|
// Try to init if canvas exists
|
|
const canvas = document.getElementById('daily-chart');
|
|
if (canvas) {
|
|
console.log('[Charts] Attempting to init daily chart from update function...');
|
|
initDailyChart(canvas.getContext('2d'));
|
|
} else {
|
|
console.error('[Charts] Daily chart canvas not found!');
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!dailyLineChart) {
|
|
console.error('[Charts] Failed to init daily chart');
|
|
return;
|
|
}
|
|
|
|
// Pastikan data arrays memiliki length yang sama
|
|
const safeLabels = labels || [];
|
|
const safeCounts = counts || [];
|
|
const safeAmounts = amounts || [];
|
|
|
|
// Pastikan semua array memiliki length yang sama (24 jam)
|
|
const maxLength = Math.max(safeLabels.length, safeCounts.length, safeAmounts.length, 24);
|
|
const finalLabels = Array.from({ length: maxLength }, (_, i) => {
|
|
if (i < safeLabels.length) return safeLabels[i];
|
|
return `${String(i).padStart(2, '0')}:00`;
|
|
});
|
|
const finalCounts = Array.from({ length: maxLength }, (_, i) => {
|
|
if (i < safeCounts.length) return Number(safeCounts[i]) || 0;
|
|
return 0;
|
|
});
|
|
const finalAmounts = Array.from({ length: maxLength }, (_, i) => {
|
|
if (i < safeAmounts.length) return Number(safeAmounts[i]) || 0;
|
|
return 0;
|
|
});
|
|
|
|
// Update chart data
|
|
dailyLineChart.data.labels = finalLabels;
|
|
dailyLineChart.data.datasets[0].data = finalCounts;
|
|
dailyLineChart.data.datasets[1].data = finalAmounts;
|
|
|
|
// Update chart dengan mode 'none' untuk menghindari animasi yang tidak perlu
|
|
dailyLineChart.update('none');
|
|
|
|
console.log('[Charts] Daily chart updated:', {
|
|
labelsCount: finalLabels.length,
|
|
countsCount: finalCounts.length,
|
|
amountsCount: finalAmounts.length,
|
|
totalCount: finalCounts.reduce((a, b) => a + b, 0),
|
|
totalAmount: finalAmounts.reduce((a, b) => a + b, 0)
|
|
});
|
|
}
|
|
|
|
export function initCategoryChart(ctx) {
|
|
if (categoryChart) return categoryChart;
|
|
|
|
categoryChart = new Chart(ctx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: [],
|
|
datasets: [
|
|
{
|
|
label: 'Per Kategori',
|
|
data: [],
|
|
backgroundColor: ['#111827', '#4b5563', '#9ca3af'],
|
|
borderWidth: 0
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
cutout: '60%',
|
|
plugins: {
|
|
legend: {
|
|
position: 'bottom',
|
|
labels: {
|
|
boxWidth: 14,
|
|
boxHeight: 6
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
return categoryChart;
|
|
}
|
|
|
|
export function updateCategoryChart({ labels, values }) {
|
|
if (!categoryChart) {
|
|
console.warn('[Charts] Category chart belum di-init, skip update');
|
|
// Try to init if canvas exists
|
|
const canvas = document.getElementById('category-chart');
|
|
if (canvas) {
|
|
console.log('[Charts] Attempting to init category chart from update function...');
|
|
initCategoryChart(canvas.getContext('2d'));
|
|
} else {
|
|
console.error('[Charts] Category chart canvas not found!');
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!categoryChart) {
|
|
console.error('[Charts] Failed to init category chart');
|
|
return;
|
|
}
|
|
|
|
// Pastikan labels dan values tidak kosong
|
|
// Default categories: Orang, Motor, Mobil
|
|
const defaultLabels = ['Orang', 'Motor', 'Mobil'];
|
|
const defaultValues = [0, 0, 0];
|
|
|
|
const finalLabels = labels && labels.length > 0 ? labels : defaultLabels;
|
|
const finalValues = values && values.length > 0 ? values : defaultValues;
|
|
|
|
// Pastikan length sama (minimal 3 untuk 3 kategori)
|
|
const maxLength = Math.max(finalLabels.length, finalValues.length, 3);
|
|
const safeLabels = Array.from({ length: maxLength }, (_, i) => {
|
|
if (i < finalLabels.length) return finalLabels[i];
|
|
return defaultLabels[i] || `Kategori ${i + 1}`;
|
|
});
|
|
const safeValues = Array.from({ length: maxLength }, (_, i) => {
|
|
if (i < finalValues.length) return Number(finalValues[i]) || 0;
|
|
return 0;
|
|
});
|
|
|
|
// Update chart data
|
|
categoryChart.data.labels = safeLabels;
|
|
categoryChart.data.datasets[0].data = safeValues;
|
|
|
|
// Update chart dengan mode 'none' untuk menghindari animasi yang tidak perlu
|
|
categoryChart.update('none');
|
|
|
|
console.log('[Charts] Category chart updated:', {
|
|
labels: safeLabels,
|
|
values: safeValues,
|
|
total: safeValues.reduce((a, b) => a + b, 0)
|
|
});
|
|
}
|
|
|
|
|