Animated bar chart on load - React Native

Animating a simple bar chart on load using React Native and Reanimated

ยท

3 min read

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;

Resources

ย