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:
BTekno Dev
2026-01-02 00:12:03 +07:00
parent e7e2042e86
commit 768a1e146c
3 changed files with 121 additions and 11 deletions

View File

@@ -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');
}
}