Range Radar Custom Animation
Uses range data ([low, high]) so the radar polygon has an internal baseline. Swap datasets with animationInterpolateFn enabled to compare the point animation with the baseline animation.
import type { AnimationInterpolateFn, PolarLayout, RadarPoint } from 'recharts';
import { interpolate, Legend, PolarAngleAxis, PolarGrid, PolarRadiusAxis, Radar, RadarChart, Tooltip } from 'recharts';
import type { Lever } from '../../../components/Shared/levers/Levers.tsx';
import { createSelectLever } from '../../../components/Shared/levers/Levers.tsx';
import { animationDurationLever } from '../../../components/Shared/levers/gallery/animationDurationLever.tsx';
import { replayAnimationLever } from '../../../components/Shared/levers/gallery/replayAnimationLever.tsx';
import { swapDataSetLever } from '../../../components/Shared/levers/gallery/swapDataLever.tsx';
const animationOptions = {
pointToPoint: { label: 'Point to point (default)' },
centerOut: { label: 'Always from center (custom)' },
} as const;
type AnimationStyle = keyof typeof animationOptions;
type ControlsType = {
animationStyle: AnimationStyle;
animationDuration: number;
replayKey: number;
dataSet: 'a' | 'b';
};
export const rangeRadarChartCustomAnimationDefaultState: ControlsType = {
animationStyle: 'centerOut',
animationDuration: 1600,
replayKey: 0,
dataSet: 'a',
};
const dataA = [
{ subject: 'Math', range: [30, 110] },
{ subject: 'Art', range: [55, 120] },
{ subject: 'Music', range: [40, 95] },
{ subject: 'Physics', range: [65, 135] },
{ subject: 'History', range: [50, 105] },
{ subject: 'Sports', range: [70, 125] },
];
const dataB = [
{ subject: 'Math', range: [20, 80] },
{ subject: 'Art', range: [70, 140] },
{ subject: 'Music', range: [35, 75] },
{ subject: 'Physics', range: [80, 145] },
{ subject: 'History', range: [45, 90] },
{ subject: 'Sports', range: [95, 150] },
];
const animateFromCenter: AnimationInterpolateFn<RadarPoint, PolarLayout> = (items, animationElapsedTime) => {
if (items == null) {
return [];
}
if (animationElapsedTime === 1) {
return items.flatMap(item => (item.status === 'removed' ? [] : [item.next]));
}
return items.flatMap(item => {
if (item.status === 'removed') {
return [];
}
return [
{
...item.next,
x: interpolate(item.next.cx, item.next.x, animationElapsedTime),
y: interpolate(item.next.cy, item.next.y, animationElapsedTime),
radius: interpolate(0, item.next.radius, animationElapsedTime),
},
];
});
};
export const rangeRadarChartCustomAnimationLevers = [
replayAnimationLever<ControlsType>(),
swapDataSetLever<ControlsType>(),
createSelectLever<ControlsType, AnimationStyle>({
key: 'animationInterpolateFn',
label: 'animationInterpolateFn',
options: Object.entries(animationOptions).map(([value, option]) => ({
value: value as AnimationStyle,
label: option.label,
})),
getValue: state => state.animationStyle,
onChange: (animationStyle, state) => ({ ...state, animationStyle }),
}),
animationDurationLever<ControlsType>(),
] satisfies ReadonlyArray<Lever<ControlsType>>;
export default function RangeRadarChartCustomAnimation(props: Partial<ControlsType>) {
const { animationStyle, animationDuration, replayKey, dataSet } = {
...rangeRadarChartCustomAnimationDefaultState,
...props,
};
const animationInterpolateFn = animationStyle === 'centerOut' ? animateFromCenter : undefined;
return (
<RadarChart
key={replayKey}
style={{ width: '100%', maxWidth: 560, aspectRatio: 1 }}
responsive
data={dataSet === 'a' ? dataA : dataB}
>
<PolarGrid />
<PolarAngleAxis dataKey="subject" />
<PolarRadiusAxis domain={[0, 150]} />
<Tooltip />
<Legend />
<Radar
name="Range"
dataKey="range"
fillOpacity={0.35}
isAnimationActive
animationDuration={animationDuration}
animationInterpolateFn={animationInterpolateFn}
/>
</RadarChart>
);
}