Animated Bar series
Start streaming to move the window forward and compare the matching and interpolation strategies.
import {
AnimationInterpolateFn,
AnimationItem,
AnimationMatchBy,
Bar,
BarChart,
BarRectangleItem,
CartesianGrid,
CartesianLayout,
interpolate,
matchByDataKey,
matchByIndex,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
import { generateMockData, RechartsDevtools } from '@recharts/devtools';
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 { animationMatchByLever } from '../../../components/Shared/levers/gallery/animationMatchByLever.tsx';
import { streamWindowLever } from '../../../components/Shared/levers/gallery/streamWindowLever.tsx';
import { cartesianLayoutLever } from '../../../components/Shared/levers/gallery/cartesianLayoutLever.tsx';
const WINDOW = 6;
const DATA_LENGTH = 30;
const allData = generateMockData(DATA_LENGTH, 90).map((o, i) => ({ ...o, i }));
function getCircularWindowData(windowStart: number) {
return Array.from({ length: WINDOW }, (_, index) => allData[(windowStart + index) % DATA_LENGTH]!);
}
type AnimationVariant = 'default' | 'custom';
type ControlsType = {
animationMatchBy: 'index' | 'dataKey';
windowStart: number;
layout: CartesianLayout;
animationDuration: number;
animationVariant: AnimationVariant;
};
const swipeLeftAnimateItems: AnimationInterpolateFn<BarRectangleItem, CartesianLayout> = (
items: ReadonlyArray<AnimationItem<BarRectangleItem>> | null,
animationElapsedTime: number,
) => {
if (items == null) return [];
return items.flatMap((item): ReadonlyArray<BarRectangleItem> => {
if (!item) return [];
if (item.status === 'removed') {
return [
{
...item.prev,
x: interpolate(item.prev.x, item.prev.x - item.prev.width * 2, animationElapsedTime),
},
];
}
if (item.status === 'matched') {
return [
{
...item.next,
x: interpolate(item.prev.x, item.next.x, animationElapsedTime),
y: interpolate(item.prev.y, item.next.y, animationElapsedTime),
width: interpolate(item.prev.width, item.next.width, animationElapsedTime),
height: interpolate(item.prev.height, item.next.height, animationElapsedTime),
},
];
}
const { next } = item;
return [{ ...next, x: interpolate(item.next.x + item.next.width * 2, item.next.x, animationElapsedTime) }];
});
};
const swipeBottomAnimateItems: AnimationInterpolateFn<BarRectangleItem, CartesianLayout> = (
items: ReadonlyArray<AnimationItem<BarRectangleItem>> | null,
animationElapsedTime: number,
) => {
if (items == null) return [];
return items.flatMap((item): ReadonlyArray<BarRectangleItem> => {
if (!item) return [];
if (item.status === 'removed') {
return [
{
...item.prev,
y: interpolate(item.prev.y, item.prev.y - item.prev.height * 2, animationElapsedTime),
},
];
}
if (item.status === 'matched') {
return [
{
...item.next,
y: interpolate(item.prev.y, item.next.y, animationElapsedTime),
},
];
}
const { next } = item;
return [{ ...next, y: interpolate(next.y + next.height * 2, next.y, animationElapsedTime) }];
});
};
const animationInterpolateFn: AnimationInterpolateFn<BarRectangleItem, CartesianLayout> = (
items,
animationElapsedTime,
layout,
) => {
if (layout === 'horizontal') {
return swipeLeftAnimateItems(items, animationElapsedTime, layout);
}
return swipeBottomAnimateItems(items, animationElapsedTime, layout);
};
export const animatedBarTimeSeriesDefaultState: ControlsType = {
animationMatchBy: 'dataKey',
windowStart: 0,
layout: 'horizontal',
animationDuration: 1200,
animationVariant: 'default',
};
export const animatedBarTimeSeriesLevers: ReadonlyArray<Lever<ControlsType>> = [
streamWindowLever<ControlsType>({
wrapAt: DATA_LENGTH,
}),
cartesianLayoutLever(),
animationMatchByLever<ControlsType>({
options: [
{ value: 'index', label: 'matchByIndex (default)' },
{ value: 'dataKey', label: "matchByDataKey('label')" },
],
}),
createSelectLever<ControlsType, AnimationVariant>({
key: 'animationVariant',
label: 'animationInterpolateFn',
options: [
{ value: 'default', label: 'default' },
{ value: 'custom', label: 'custom (swipe)' },
],
getValue: state => state.animationVariant,
onChange: (animationVariant, state) => ({ ...state, animationVariant }),
}),
animationDurationLever<ControlsType>(),
];
export default function AnimatedBarTimeSeriesExample(props: Partial<ControlsType>) {
const { animationMatchBy, windowStart, layout, animationDuration, animationVariant } = {
...animatedBarTimeSeriesDefaultState,
...props,
};
const data = getCircularWindowData(windowStart);
const matchProp: typeof matchByIndex | AnimationMatchBy<{ payload?: unknown }> =
animationMatchBy === 'dataKey' ? matchByDataKey('label') : matchByIndex;
return (
<BarChart
style={{ width: '100%', aspectRatio: 1.618, maxWidth: 600 }}
responsive
data={data}
margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
layout={layout}
>
<CartesianGrid />
<XAxis allowDataOverflow type="auto" />
<YAxis type="auto" />
<Tooltip />
<Bar
dataKey="y"
animationDuration={animationDuration}
animationMatchBy={matchProp}
animationInterpolateFn={animationVariant === 'custom' ? animationInterpolateFn : undefined}
/>
<RechartsDevtools />
</BarChart>
);
}