Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | import { useEffect, useRef, useState } from "react";
interface DataItem {
label: string;
code?: string;
value1: number;
value2: number;
difference: number;
date: string;
}
interface AnimationState {
entering: Set<string>;
exiting: Set<string>;
updating: Set<string>;
}
export const useGapChartAnimation = (
processedDataSet: DataItem[],
enableTransitions: boolean = true
) => {
const previousDataRef = useRef<DataItem[]>([]);
const [animationState, setAnimationState] = useState<AnimationState>({
entering: new Set(),
exiting: new Set(),
updating: new Set(),
});
useEffect(() => {
if (!enableTransitions) {
previousDataRef.current = processedDataSet;
return;
}
const previousData = previousDataRef.current;
const currentLabels = new Set(processedDataSet.map(d => d.label));
const previousLabels = new Set(previousData.map(d => d.label));
// Identify entering items (new items not in previous data)
const entering = new Set<string>();
processedDataSet.forEach(d => {
if (!previousLabels.has(d.label)) {
entering.add(d.label);
}
});
// Identify exiting items (items in previous data but not current)
const exiting = new Set<string>();
previousData.forEach(d => {
if (!currentLabels.has(d.label)) {
exiting.add(d.label);
}
});
// Identify updating items (items that exist in both but may have changed position/values)
const updating = new Set<string>();
processedDataSet.forEach((d, index) => {
const prevIndex = previousData.findIndex(pd => pd.label === d.label);
if (
prevIndex !== -1 &&
(prevIndex !== index ||
d.value1 !== previousData[prevIndex].value1 ||
d.value2 !== previousData[prevIndex].value2)
) {
updating.add(d.label);
}
});
setAnimationState({ entering, exiting, updating });
// Trigger enter animations
if (entering.size > 0) {
// Force reflow to ensure initial state is applied
requestAnimationFrame(() => {
setAnimationState(prev => ({
...prev,
entering: new Set(),
}));
});
}
// Update previousDataRef immediately for exiting items
previousDataRef.current = processedDataSet;
}, [processedDataSet, enableTransitions]);
// Get opacity for an item based on animation state
const getItemOpacity = (label: string, defaultOpacity: number = 1) => {
if (animationState.entering.has(label)) {
return 0;
}
if (animationState.exiting.has(label)) {
return 0;
}
return defaultOpacity;
};
// Get transform for an item based on animation state
const getItemTransform = () => {
// No transform animations, only fade
return "";
};
// Don't include exiting items - they should disappear immediately
const renderDataSet = [...processedDataSet];
// Helper to check if an item should have transitions
const shouldTransition = (label: string) => {
// Only apply transitions to updating items, not entering or exiting
return animationState.updating.has(label);
};
return {
animationState,
getItemOpacity,
getItemTransform,
renderDataSet,
shouldTransition,
};
};
|