feat: fondu entre les frames de l'animation heatmap
ci/woodpecker/push/woodpecker Pipeline was successful

Fade out 250ms → mise à jour des données → fade in 250ms sur le canvas
Leaflet.heat. Aucun état React supplémentaire — manipulation directe
du canvas interne via _canvas.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
syoul
2026-03-23 21:47:10 +01:00
parent 40c09e2e4b
commit 30057a07fb
+28 -13
View File
@@ -64,25 +64,40 @@ export function HeatMap({ transactions }: HeatMapProps) {
};
}, []);
// Update heatmap data when transactions change
// Update heatmap data with fade transition when transactions change
useEffect(() => {
if (!heatRef.current || !mapRef.current) return;
// Normalize amounts for intensity (log scale feels better visually)
const maxAmount = Math.max(...transactions.map((t) => t.amount), 1);
const canvas = (heatRef.current as unknown as { _canvas?: HTMLCanvasElement })._canvas;
const points: L.HeatLatLngTuple[] = transactions.map((tx) => [
tx.lat,
tx.lng,
Math.min(Math.log1p(tx.amount) / Math.log1p(maxAmount), 1),
]);
const update = () => {
const maxAmount = Math.max(...transactions.map((t) => t.amount), 1);
const points: L.HeatLatLngTuple[] = transactions.map((tx) => [
tx.lat,
tx.lng,
Math.min(Math.log1p(tx.amount) / Math.log1p(maxAmount), 1),
]);
try {
heatRef.current?.setLatLngs(points);
} catch {
// map was torn down (React StrictMode double-invoke), ignore
}
};
// Guard: only update if the heat layer is still attached to the map
try {
heatRef.current.setLatLngs(points);
} catch {
// map was torn down (React StrictMode double-invoke), ignore
if (!canvas) {
update();
return;
}
canvas.style.transition = 'opacity 0.25s ease-in-out';
canvas.style.opacity = '0';
const t = setTimeout(() => {
update();
canvas.style.opacity = '1';
}, 250);
return () => clearTimeout(t);
}, [transactions]);
return (