Enhance dashboard chart animations with improved element selection, faster update speed, and robust error handling

This commit is contained in:
Matt Batchelder
2026-02-21 01:55:20 -05:00
parent f8321568ce
commit 38d585e071
2 changed files with 176 additions and 98 deletions

View File

@@ -14,16 +14,30 @@ class DashboardAnimator {
// Chart dimensions
this.barChartHeight = 120;
this.chartUpdateSpeed = 2000; // Base cycle speed in ms (controls smoothness)
this.chartUpdateSpeed = 1500; // Faster: 1.5s cycle for constant movement
// Cache DOM elements
this.barsGroup1 = svg.querySelectorAll('#bars-group-1 .bar');
this.barsGroup2 = svg.querySelectorAll('#bars-group-2 .bar');
this.valuesGroup1 = svg.querySelectorAll('#values-group-1 .chart-value');
this.valuesGroup2 = svg.querySelectorAll('#values-group-2 .chart-value');
this.linePath = svg.getElementById('line-path');
this.lineFill = svg.getElementById('line-fill');
this.pieSegments = svg.querySelectorAll('.pie-segment');
// Cache DOM elements - try multiple selector methods for robustness
this.barsGroup1 = this.svg.querySelectorAll('#bars-group-1 rect.bar');
this.barsGroup2 = this.svg.querySelectorAll('#bars-group-2 rect.bar');
this.valuesGroup1 = this.svg.querySelectorAll('#values-group-1 text.chart-value');
this.valuesGroup2 = this.svg.querySelectorAll('#values-group-2 text.chart-value');
this.linePath = this.svg.querySelector('#line-path');
this.lineFill = this.svg.querySelector('#line-fill');
this.pieSegments = this.svg.querySelectorAll('g.pie-segment');
// Debug: Check if elements were found
const hasElements = this.barsGroup1.length > 0 || this.linePath || this.pieSegments.length > 0;
if (!hasElements) {
console.warn('[DashboardAnimator] No chart elements found. Debugging info:');
console.warn(' SVG element:', this.svg);
console.warn(' bars-group-1:', this.barsGroup1.length);
console.warn(' line-path:', !!this.linePath);
console.warn(' pie-segment:', this.pieSegments.length);
// Try direct getElementById as fallback
console.warn(' Testing getElementById on document:', document.getElementById('bars-group-1'));
} else {
console.log('[DashboardAnimator] ✓ Found elements - Bars:', this.barsGroup1.length, 'Line:', !!this.linePath, 'Pie:', this.pieSegments.length);
}
// Noise generators for smooth, realistic data
this.noisePhase1 = Math.random() * Math.PI * 2;
@@ -36,9 +50,20 @@ class DashboardAnimator {
}
init() {
if (!this.isReducedMotion) {
if (!this.isReducedMotion && (this.barsGroup1.length || this.linePath || this.pieSegments.length)) {
console.log('[DashboardAnimator] ✓ Starting animation loop');
// Visual indicator that animator started
this.svg.style.cssText = 'border: 2px solid green; border-radius: 4px;';
setTimeout(() => {
this.svg.style.border = '';
}, 500);
this.animate();
this.attachHoverListeners();
} else if (this.isReducedMotion) {
console.log('[DashboardAnimator] Motion preference: reduced');
} else {
console.log('[DashboardAnimator] ✗ No animatable elements found');
this.svg.style.cssText = 'border: 2px solid red; border-radius: 4px;';
}
}
@@ -72,14 +97,20 @@ class DashboardAnimator {
* Update bar chart with smooth animation
*/
updateBars(barsElements, valuesElements, maxValue = 100, isPercent = true) {
if (!barsElements || barsElements.length === 0) return;
barsElements.forEach((bar, index) => {
const value = this.generateValue(this.noisePhase1, index, maxValue);
const heightPercent = (value / maxValue) * 100;
const height = (heightPercent / 100) * this.barChartHeight;
bar.setAttribute('height', height);
// For SVG bars to grow upward, adjust y position inversely
const y = this.barChartHeight - height;
if (valuesElements[index]) {
bar.setAttribute('height', height);
bar.setAttribute('y', y);
if (valuesElements && valuesElements[index]) {
valuesElements[index].textContent = isPercent ?
Math.round(value) + '%' :
Math.round(value);
@@ -92,6 +123,8 @@ class DashboardAnimator {
* Update line graph with smooth curve
*/
updateLineGraph() {
if (!this.linePath || !this.lineFill) return;
const time = (Date.now() - this.startTime) / this.chartUpdateSpeed;
const points = [];
@@ -119,6 +152,8 @@ class DashboardAnimator {
* Update pie chart with smooth segment rotation
*/
updatePieChart() {
if (!this.pieSegments || this.pieSegments.length === 0) return;
const time = (Date.now() - this.startTime) / (this.chartUpdateSpeed * 0.5);
const baseRotation = (time * 15) % 360; // Slow rotation
@@ -134,10 +169,14 @@ class DashboardAnimator {
*/
animate = () => {
if (!this.isPaused) {
this.updateBars(this.barsGroup1, this.valuesGroup1, 100, true);
this.updateBars(this.barsGroup2, this.valuesGroup2, 5000, false);
this.updateLineGraph();
this.updatePieChart();
try {
this.updateBars(this.barsGroup1, this.valuesGroup1, 100, true);
this.updateBars(this.barsGroup2, this.valuesGroup2, 5000, false);
this.updateLineGraph();
this.updatePieChart();
} catch (err) {
console.error('[DashboardAnimator] Animation error:', err);
}
}
this.animationId = requestAnimationFrame(this.animate);
@@ -189,36 +228,84 @@ class DashboardAnimator {
*/
function initDashboardAnimation() {
const dashboardCharts = document.querySelectorAll('.dashboard-chart');
console.log('[initDashboardAnimation] Found', dashboardCharts.length, 'dashboard charts');
if (!dashboardCharts.length) return;
const observerOptions = {
root: null,
rootMargin: '50px',
threshold: 0.1
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
if (!entry.target._dashboardAnimator) {
entry.target._dashboardAnimator = new DashboardAnimator(entry.target);
}
} else {
// Pause animation when out of view for performance
if (entry.target._dashboardAnimator) {
entry.target._dashboardAnimator.pause();
dashboardCharts.forEach((chart, idx) => {
// Check if element is likely in viewport
const rect = chart.getBoundingClientRect();
const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
console.log('[initDashboardAnimation] Chart', idx, 'visible:', isVisible, 'rect:', rect);
if (isVisible || !chart._dashboardAnimator) {
// Start animation immediately if visible or if not set up yet
if (!chart._dashboardAnimator) {
console.log('[initDashboardAnimation] Creating DashboardAnimator for chart', idx);
try {
chart._dashboardAnimator = new DashboardAnimator(chart);
console.log('[initDashboardAnimation] ✓ DashboardAnimator created successfully');
} catch (err) {
console.error('[initDashboardAnimation] ✗ Failed to create DashboardAnimator:', err);
}
}
});
}, observerOptions);
}
});
dashboardCharts.forEach(chart => observer.observe(chart));
// Also set up IntersectionObserver for when elements come into view
if ('IntersectionObserver' in window) {
const observerOptions = {
root: null,
rootMargin: '100px',
threshold: 0.05
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
console.log('[IntersectionObserver]', entry.isIntersecting ? 'Intersecting' : 'Not intersecting');
if (entry.isIntersecting) {
if (!entry.target._dashboardAnimator) {
console.log('[IntersectionObserver] Creating new DashboardAnimator');
try {
entry.target._dashboardAnimator = new DashboardAnimator(entry.target);
} catch (err) {
console.error('[IntersectionObserver] Failed:', err);
}
} else {
entry.target._dashboardAnimator.resume();
}
} else {
if (entry.target._dashboardAnimator) {
entry.target._dashboardAnimator.pause();
}
}
});
}, observerOptions);
dashboardCharts.forEach(chart => observer.observe(chart));
}
}
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initDashboardAnimation);
} else {
initDashboardAnimation();
console.log('[Dashboard Animator] Script loaded, readyState:', document.readyState);
function tryInitialize() {
console.log('[Dashboard Animator] tryInitialize called');
const charts = document.querySelectorAll('.dashboard-chart');
console.log('[Dashboard Animator] Found', charts.length, 'dashboard charts on page');
if (charts.length > 0) {
console.log('[Dashboard Animator] Found charts, initializing now...');
initDashboardAnimation();
} else {
console.log('[Dashboard Animator] No charts found yet, retrying in 100ms...');
setTimeout(tryInitialize, 100);
}
}
if (document.readyState === 'loading') {
console.log('[Dashboard Animator] DOM still loading, waiting for DOMContentLoaded');
document.addEventListener('DOMContentLoaded', tryInitialize);
} else {
console.log('[Dashboard Animator] DOM already loaded, initializing immediately');
tryInitialize();
}