diff --git a/src/components/HeatMap.tsx b/src/components/HeatMap.tsx index bff36d4..98db3e7 100644 --- a/src/components/HeatMap.tsx +++ b/src/components/HeatMap.tsx @@ -33,6 +33,7 @@ export function HeatMap({ transactions }: HeatMapProps) { const containerRef = useRef(null); const mapRef = useRef(null); const heatRef = useRef(null); + const overlayRef = useRef(null); // Initialize map once useEffect(() => { @@ -64,11 +65,12 @@ export function HeatMap({ transactions }: HeatMapProps) { }; }, []); - // Update heatmap data with fade transition when transactions change + // Crossfade: capture current heatmap → update underneath → fade out overlay useEffect(() => { if (!heatRef.current || !mapRef.current) return; const canvas = (heatRef.current as unknown as { _canvas?: HTMLCanvasElement })._canvas; + const overlay = overlayRef.current; const update = () => { const maxAmount = Math.max(...transactions.map((t) => t.amount), 1); @@ -84,28 +86,45 @@ export function HeatMap({ transactions }: HeatMapProps) { } }; - if (!canvas) { + if (!canvas || !overlay) { update(); return; } - canvas.style.transition = 'opacity 0.15s ease-out'; - canvas.style.opacity = '0.15'; - - const t = setTimeout(() => { + // Freeze current frame in the overlay + try { + overlay.src = canvas.toDataURL(); + } catch { + // canvas tainted (shouldn't happen with heatmap-only canvas) update(); - canvas.style.transition = 'opacity 0.2s ease-in'; - canvas.style.opacity = '1'; - }, 150); + return; + } + overlay.style.transition = 'none'; + overlay.style.opacity = '1'; - return () => clearTimeout(t); + // Update heatmap underneath immediately + update(); + + // Then dissolve the overlay away + const raf = requestAnimationFrame(() => { + requestAnimationFrame(() => { + overlay.style.transition = 'opacity 0.5s ease-in-out'; + overlay.style.opacity = '0'; + }); + }); + + return () => cancelAnimationFrame(raf); }, [transactions]); return ( -
+
+
+ +
); }