Feat: Tambah animasi counter di KPI cards dan animasi naik turun di chart trending harian
- Counter animation dari angka terkecil ke target dengan easing smooth - Chart animation naik dari bawah tanpa animasi horizontal - Loading overlay dengan spinner animation - Fade animation untuk card values saat update
This commit is contained in:
@@ -49,6 +49,30 @@ export function initDailyChart(ctx) {
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: {
|
||||
duration: 1500,
|
||||
easing: 'linear',
|
||||
x: {
|
||||
duration: 0 // Tidak ada animasi horizontal (kanan ke kiri)
|
||||
},
|
||||
y: {
|
||||
type: 'number',
|
||||
easing: 'linear',
|
||||
duration: 1500,
|
||||
from: (ctx) => {
|
||||
// Animasi naik dari bawah (0) untuk efek naik turun yang smooth
|
||||
try {
|
||||
const chart = ctx.chart;
|
||||
if (chart && chart.scales && chart.scales.y) {
|
||||
return chart.scales.y.getPixelForValue(0);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Charts] Error getting scale for animation:', e);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
},
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
@@ -132,8 +156,8 @@ export function updateDailyChart({ labels, counts, amounts }) {
|
||||
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');
|
||||
// Update chart dengan animasi naik turun yang smooth
|
||||
dailyLineChart.update('active');
|
||||
|
||||
console.log('[Charts] Daily chart updated:', {
|
||||
labelsCount: finalLabels.length,
|
||||
@@ -222,7 +246,7 @@ export function updateCategoryChart({ labels, values }) {
|
||||
categoryChart.data.datasets[0].data = safeValues;
|
||||
|
||||
// Update chart dengan mode 'none' untuk menghindari animasi yang tidak perlu
|
||||
categoryChart.update('none');
|
||||
categoryChart.update('active');
|
||||
|
||||
console.log('[Charts] Category chart updated:', {
|
||||
labels: safeLabels,
|
||||
|
||||
@@ -294,16 +294,66 @@ async function loadGates() {
|
||||
}
|
||||
}
|
||||
|
||||
// Counter animation function
|
||||
function animateCounter(element, targetValue, formatter, duration = 1500) {
|
||||
if (!element) return;
|
||||
|
||||
// Get current value from element text
|
||||
const currentText = element.textContent || '0';
|
||||
let currentValue = 0;
|
||||
|
||||
// Parse current value based on formatter type
|
||||
if (formatter === formatCurrency) {
|
||||
// Remove "Rp ", dots, and spaces, then parse
|
||||
const cleaned = currentText.replace(/Rp\s?/g, '').replace(/\./g, '').replace(/\s/g, '');
|
||||
currentValue = parseInt(cleaned) || 0;
|
||||
} else {
|
||||
// Remove dots and spaces for number format
|
||||
const cleaned = currentText.replace(/\./g, '').replace(/\s/g, '');
|
||||
currentValue = parseInt(cleaned) || 0;
|
||||
}
|
||||
|
||||
const target = targetValue || 0;
|
||||
const startValue = currentValue;
|
||||
const difference = target - startValue;
|
||||
const startTime = performance.now();
|
||||
|
||||
// Add updating class for fade effect
|
||||
element.classList.add('updating');
|
||||
|
||||
function updateCounter(currentTime) {
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
|
||||
// Easing function (easeOutCubic) for smooth animation
|
||||
const easeOutCubic = 1 - Math.pow(1 - progress, 3);
|
||||
const current = Math.floor(startValue + (difference * easeOutCubic));
|
||||
|
||||
element.textContent = formatter(current);
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(updateCounter);
|
||||
} else {
|
||||
// Ensure final value is exact
|
||||
element.textContent = formatter(target);
|
||||
element.classList.remove('updating');
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(updateCounter);
|
||||
}
|
||||
|
||||
function renderSummary({ totalAmount, personCount, motorCount, carCount }) {
|
||||
const amountEl = document.getElementById('card-total-amount');
|
||||
const personEl = document.getElementById('card-person-count');
|
||||
const motorEl = document.getElementById('card-motor-count');
|
||||
const carEl = document.getElementById('card-car-count');
|
||||
|
||||
if (amountEl) amountEl.textContent = formatCurrency(totalAmount || 0);
|
||||
if (personEl) personEl.textContent = formatNumber(personCount || 0);
|
||||
if (motorEl) motorEl.textContent = formatNumber(motorCount || 0);
|
||||
if (carEl) carEl.textContent = formatNumber(carCount || 0);
|
||||
// Use counter animation for all values
|
||||
animateCounter(amountEl, totalAmount || 0, formatCurrency, 1500);
|
||||
animateCounter(personEl, personCount || 0, formatNumber, 1200);
|
||||
animateCounter(motorEl, motorCount || 0, formatNumber, 1200);
|
||||
animateCounter(carEl, carCount || 0, formatNumber, 1200);
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
@@ -1104,14 +1154,14 @@ function setupFilters() {
|
||||
dailyChartInstance.data.labels = [];
|
||||
dailyChartInstance.data.datasets[0].data = [];
|
||||
dailyChartInstance.data.datasets[1].data = [];
|
||||
dailyChartInstance.update('none');
|
||||
dailyChartInstance.update('active');
|
||||
}
|
||||
|
||||
const categoryChartInstance = getCategoryChart();
|
||||
if (categoryChartInstance) {
|
||||
categoryChartInstance.data.labels = [];
|
||||
categoryChartInstance.data.datasets[0].data = [];
|
||||
categoryChartInstance.update('none');
|
||||
categoryChartInstance.update('active');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user