Photo by Steve Allison on Unsplash
Animated bar chart on load - React Native
Animating a simple bar chart on load using React Native and Reanimated
See the finished example
Aim
Animate a bar chart on the initial load, and when the data changes. The bars should grow in height.
Attempt 1
Reanimated provides layout animations out of the box, such as SlideInUp
and FadeIn
. These are easy to use and require very little code, and if you don't want to use the provided animations, you can create a custom layout animation. This was the approach I tried first, creating a custom entering animation to animate the height of my bars. However, I found that the animation only took the height of the first item, meaning all the bars had the same height regardless of the property passed to them. I'm not too sure why this is, but it could perhaps be because of the way the 'worklet' is created and runs independently on the UI thread, and so perhaps my use of the map function does not play nicely here.
Solution
Instead of using Reanimated's layout animations, I ended up writing a simple animation and triggering it in a UseEffect on load.
I store the height in a Animated.SharedValue
initiated to 0 and pass it to an animated style using the UseAnimatedStyle
hook. The shared value is then set to the desired height in a UseEffect
where the dependency is the height value - meaning it would fire again should the data change.
Code
// BarChart.tsx
import {View, useWindowDimensions, Text} from 'react-native';
import React, {FC} from 'react';
import tw from 'twrnc';
import Bar from './Bar';
import {BarData} from './types';
interface BarChartProps {
data: BarData[];
yMaxValue: number;
}
const BarChart: FC<BarChartProps> = ({data, yMaxValue}) => {
const chartHeight = 200;
const {width} = useWindowDimensions();
const margin = 4;
const calculateBarHeight = (barValue: BarData) => {
return (barValue.value / yMaxValue) * chartHeight;
};
const calculateBarWidth = () => {
return width / data.length - margin * 2;
};
return (
<>
<View style={tw`flex-row`}>
{data.map((bar, index) => {
const barHeight = calculateBarHeight(bar);
const barWidth = calculateBarWidth();
return (
<View key={index.toString()}>
<Bar
barMargin={margin}
barHeight={barHeight}
barWidth={barWidth}
totalHeight={chartHeight}
/>
<Text style={tw`self-center text-base text-white pt-3`}>
{bar.label}
</Text>
</View>
);
})}
</View>
<View style={tw`flex-row`}></View>
</>
);
};
export default BarChart;
//Bar.tsx
import {View} from 'react-native';
import React, {FC, useEffect} from 'react';
import tw from 'twrnc';
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
interface BarProps {
totalHeight: number;
barHeight: number;
barWidth: number;
barMargin: number;
}
const Bar: FC<BarProps> = ({totalHeight, barHeight, barWidth, barMargin}) => {
const animatedHeight = useSharedValue<number>(0);
useEffect(() => {
animatedHeight.value = withTiming(barHeight);
}, [barHeight, animatedHeight]);
const animatedStyle = useAnimatedStyle(() => {
return {
height: animatedHeight.value,
};
});
return (
<View
style={[
tw`bg-gray-800 flex-row rounded-2xl mx-[${barMargin}]`,
{height: totalHeight, width: barWidth},
]}
>
<Animated.View
style={[
tw`bg-green-500 rounded-2xl self-end`,
{width: barWidth},
animatedStyle,
]}
/>
</View>
);
};
export default Bar;