React Native Reanimated - Seeking Alternative Approach for Achieving "Counting Effect" in Bar Chart Component

Please provide the following:

  1. SDK Version: 49
  2. Platforms: mobile

I’m working on a bar chart component and aiming to implement a neat “counting effect” by having the numbers associated with each bar increment or decrement by 0.1. The idea is to create a visually appealing transition as the data changes.

However, I’ve run into a snag. When I set the increment value at 0.1 or even up to 0.3, the component starts to lag. It’s only at 0.4-0.5 where it doesn’t lag. Here’s the code snippet where this is happening:

if (displayValue < targetValues[index]) { return Math.min(displayValue + 0.1, targetValues[index]); } else if (displayValue > targetValues[index]) { return Math.max(displayValue - 0.1, targetValues[index]); }

I’ve been stuck on this for a while now and am wondering if there’s a smarter or more efficient way to achieve this counting effect without the lag. This is my first post on any forum since i like to solve problems on my own, but now I’m desperate for new ideas. Any suggestions or alternative approaches would be hugely appreciated!

Thanks a ton in advance for any help or insights!

import React, { useState, useEffect, useMemo } from "react";
import {
  View,
  Text,
  Button,
  Dimensions,
  StyleSheet,
  TouchableOpacity,
} from "react-native";
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  withTiming,
} from "react-native-reanimated";
import { AntDesign } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import { AppRegistry } from "react-native";

const initialBlackDisplayValues = [0, 0, 0, 0, 0, 0];
const initialDisplayValues = [0, 0, 0, 0, 0, 0];

function App() {
  const { height, width } = Dimensions.get("window");

  const colors2 = [
    ["#ff0000ff", "#ff9900ff"],
    ["#ff8c00ff", "#fff200ff"],
    ["#04ff00ff", "#17b988ff"],
    ["#00ffeaff", "#059ce2ff"],
    ["#007bffff", "#8800ffff"],
    ["#9900ffff", "#ff00aeff"],
  ];

  const colors3 = [
    ["#ff00009d", "#ff99009d"],
    ["#ff8c009d", "#fff2009d"],
    ["#04ff009d", "#17b9889d"],
    ["#00ffea9d", "#059ce29d"],
    ["#007bff9d", "#8800ff9d"],
    ["#9900ff9d", "#ff00ae9d"],
  ];

  const [displayValues, setDisplayValues] = useState(initialDisplayValues);
  const [targetValues, setTargetValues] = useState(initialDisplayValues);
  const animations = displayValues.map(() => useSharedValue(0));

  const [blackDisplayValues, setBlackDisplayValues] = useState(
    initialBlackDisplayValues
  );
  const [blackTargetValues, setBlackTargetValues] = useState(
    initialBlackDisplayValues
  );
  const blackAnimations = blackDisplayValues.map(() => useSharedValue(0));

  const opacity = useSharedValue(0);
  const blackTextOpacity = useSharedValue(0);
  const [blackBarsVisible, setBlackBarsVisible] = useState(false);
  const [buttonDisabled, setButtonDisabled] = useState(false);
  const [averageManOn, setAvrageManOn] = useState(false);

  const shouldUpdateDisplayValues = useMemo(() => {
    return !targetValues.every(
      (value, index) => value === displayValues[index]
    );
  }, [targetValues, displayValues]);

  const shouldUpdateBlackDisplayValues = useMemo(() => {
    return !blackTargetValues.every(
      (value, index) => value === blackDisplayValues[index]
    );
  }, [blackTargetValues, blackDisplayValues]);

  useEffect(() => {
    if (shouldUpdateDisplayValues) {
      const interval = setInterval(() => {
        const newDisplayValues = displayValues.map((displayValue, index) => {
          if (displayValue < targetValues[index]) {
            return Math.min(displayValue + 0.1, targetValues[index]);
          } else if (displayValue > targetValues[index]) {
            return Math.max(displayValue - 0.1, targetValues[index]);
          } else {
            return displayValue;
          }
        });

        setDisplayValues(newDisplayValues);
      }, 30);

      return () => clearInterval(interval);
    }
  }, [targetValues, displayValues, shouldUpdateDisplayValues]);

  useEffect(() => {
    if (shouldUpdateBlackDisplayValues) {
      const interval = setInterval(() => {
        const newBlackDisplayValues = blackDisplayValues.map(
          (displayValue, index) => {
            if (displayValue < blackTargetValues[index]) {
              return Math.min(displayValue + 0.1, blackTargetValues[index]);
            } else if (displayValue > blackTargetValues[index]) {
              return Math.max(displayValue - 0.1, blackTargetValues[index]);
            } else {
              return displayValue;
            }
          }
        );

        setBlackDisplayValues(newBlackDisplayValues);
      }, 30);

      return () => clearInterval(interval);
    }
  }, [blackTargetValues, blackDisplayValues, shouldUpdateBlackDisplayValues]);

  useEffect(() => {
    displayValues.forEach((displayValue, index) => {
      animations[index].value = withSpring(displayValue / 10, {
        damping: 100,
        stiffness: 2000,
        mass: 2,
      });
    });
  }, [displayValues]);

  const animatedStyles = animations.map((animation, index) =>
    useAnimatedStyle(() => {
      return {
        width: animation.value * (width * 0.77),

        // Change this to interpolate the color based on the opacity value
      };
    })
  );

  const toggleBlackValues = () => {
    if (buttonDisabled) {
      return;
    }

    setButtonDisabled(true);
    setTimeout(() => setButtonDisabled(false), 1500);

    if (blackBarsVisible) {
      blackTextOpacity.value = withTiming(0, {
        duration: 1000,
      });
      setTimeout(() => {
        opacity.value = withTiming(0, {
          duration: 1000,
        });
      }, 600);
      setBlackTargetValues([0, 0, 0, 0, 0, 0]);
    }
    if (!blackBarsVisible) {
      opacity.value = withTiming(1, {
        duration: 700,
      });
      setTimeout(() => {
        setBlackTargetValues([1.9, 9.2, 1.2, 1.5, 2.3, 1.8]);
        blackTextOpacity.value = 1;
      }, 750);
    }
    setBlackBarsVisible(!blackBarsVisible);

    setTimeout(() => {
      setAvrageManOn(!averageManOn);
    }, 500);
  };

  useEffect(() => {
    blackDisplayValues.forEach((displayValue, index) => {
      blackAnimations[index].value = withSpring(displayValue / 10, {
        damping: 100,
        stiffness: 2000,
        mass: 2,
      });
    });
  }, [blackDisplayValues]);

  const blackAnimatedStyles = blackAnimations.map((animation, index) =>
    useAnimatedStyle(() => {
      return {
        width: animation.value * (width * 0.77),

        position: "absolute",
        display: animation.value === 0 ? "none" : "flex",
      };
    })
  );

  const blackTextAnimatedStyles = blackAnimations.map((animation, index) =>
    useAnimatedStyle(() => {
      return {
        position: "absolute",
        left: animation.value * (width * 0.77) + width * 0.01,
        opacity: blackTextOpacity.value,
      };
    })
  );

  const GradientAnimatedStyles = blackAnimations.map((animation, index) =>
    useAnimatedStyle(() => {
      return {
        opacity: opacity.value,
      };
    })
  );

  return (
    <View style={styles.container0}>
      <Animated.View style={styles.container}>
        <View style={styles.conatiner3}>
          <Animated.View>
            <TouchableOpacity
              onPress={toggleBlackValues}
              style={styles.customButton}
              activeOpacity={1}
            >
              <AntDesign
                name="question"
                size={height * 0.03}
                color={blackBarsVisible ? "gray" : "white"}
              />

              <Text
                style={[
                  styles.customButtonText,
                  { color: blackBarsVisible ? "gray" : "white" },
                ]}
              >
                Press here for comparison
              </Text>
            </TouchableOpacity>
          </Animated.View>
        </View>

        <Animated.View style={styles.container2}>
          {animatedStyles.map((animatedStyle, index) => (
            <View key={index}>
              <View style={styles.statContainer}></View>
              <View style={styles.row}>
                <View style={styles.border}>
                  <View style={styles.textConatiner}>
                    <Text style={styles.text}>
                      {displayValues[index].toFixed(1)}
                    </Text>
                  </View>
                  <View style={styles.innerView}>
                    <Animated.View style={[styles.animatedView, animatedStyle]}>
                      <Animated.View
                        style={[
                          GradientAnimatedStyles,
                          {
                            backgroundColor: "#7c7c7cff",
                            width: "100%",
                            height: "100%",
                            position: "absolute",
                            zIndex: 2,
                          },
                        ]}
                      ></Animated.View>
                      <LinearGradient
                        // Array of colors for gradient
                        colors={colors2[index]}
                        // Where the gradient starts
                        start={{ x: 0, y: 0 }}
                        // Where the gradient ends
                        end={{ x: 1, y: 1 }}
                        style={styles.background}
                      />
                    </Animated.View>
                    <Animated.View
                      style={[
                        styles.blackAnimatedView,
                        blackAnimatedStyles[index],
                      ]}
                    >
                      <LinearGradient
                        // Array of colors for gradient
                        colors={colors3[index]}
                        // Where the gradient starts
                        start={{ x: 0, y: 0 }}
                        // Where the gradient ends
                        end={{ x: 1, y: 1 }}
                        style={styles.background}
                      />
                    </Animated.View>
                    <Animated.View style={blackTextAnimatedStyles[index]}>
                      <Text style={styles.blackText}>
                        {blackDisplayValues[index].toFixed(1)}
                      </Text>
                    </Animated.View>
                  </View>
                </View>
              </View>
            </View>
          ))}
        </Animated.View>

        <View>
          <Button
            title="Button 1"
            onPress={() => setTargetValues([9, 8, 1, 3, 2, 6.5])}
          ></Button>
          <Button
            title="Button 2"
            onPress={() => setTargetValues([2, 4, 7, 6, 9, 5])}
          ></Button>
        </View>
      </Animated.View>
    </View>
  );
}

AppRegistry.registerComponent("main", () => App);

export default App;

const { height, width } = Dimensions.get("window");

const styles = StyleSheet.create({
  container0: {
    alignSelf: "center",
    backgroundColor: "black",
    flex: 1,
    borderColor: "red",
    borderWidth: 1,
    width: "100%",
    height: "100%",
    maxWidth: 800, // Sätter en maxbredd på 800 pixlar
    minHeight: 700,
  },
  container: {
    flex: 1,
    justifyContent: "flex-end",

    paddingLeft: "2%",
    paddingRight: "2%",
    backgroundColor: "black",
    borderColor: "green",
    borderWidth: 1,
  },
  container2: {
    height: "29%",
    backgroundColor: "black",
    padding: "1%",
    justifyContent: "space-around",
    borderColor: "#555a6c",
    borderWidth: 0,
    borderTopWidth: 2,
    borderBottomLeftRadius: 10,
    borderBottomRightRadius: 10,
    opacity: 1,
  },
  row: {
    flexDirection: "row",
    alignItems: "center",
    borderColor: "blue",
    borderWidth: 0,
    justifyContent: "flex-start",
  },
  innerView: {
    height: "100%",
    width: width * 0.77,
    backgroundColor: "#e0e0e0",
    alignItems: "flex-start",
    justifyContent: "center",
    borderLeftWidth: height * 0.002,
    borderColor: "#555a6c",
  },
  animatedView: {
    height: "100%",
    zIndex: 0,
    position: "absolute",
  },
  blackAnimatedView: {
    height: "100%",
    zIndex: 2,
  },
  blackText: {
    color: "black",
    fontSize: height * 0.013,
    fontWeight: "600",
    marginTop: -width * 0.0015,
  },
  text: {
    fontSize: height * 0.013,
    color: "white",
    marginTop: -width * 0.0015,
  },
  textConatiner: {
    position: "absolute",
    left: "1.8%",
    borderColor: "pink",
    borderWidth: 0,
    height: 20,
    width: "5.5%",

    justifyContent: "center",
    alignItems: "flex-start",
  },

  customButton: {
    paddingTop: height * 0.01,

    borderRadius: 5,
    alignItems: "center",
    justifyContent: "center", // Lägg till denna rad
    textAlign: "center",
  },

  customButtonText: {
    color: "white",
    fontSize: height * 0.01,
  },
  background: {
    width: "100%",
    height: "100%",
  },
  statContainer: {
    alignItems: "center",
    borderColor: "red",
    borderWidth: 0,
    paddingTop: 0,
    justifyContent: "flex-end",
  },
  statText: {
    color: "white",
    fontSize: height * 0.012,
  },
  border: {
    flexDirection: "row",
    height: height * 0.02,
    width: width * 0.85,
    borderWidth: height * 0.002,
    borderColor: "#555a6c",
    alignItems: "center",
    justifyContent: "flex-end",
  },
});
{
  "name": "lifepath",
  "version": "1.0.0",
  "main": "App.js",
  "scripts": {
    "start": "expo start",
    "android": "expo start --android",
    "ios": "expo start --ios",
    "web": "expo start --web",
    "build": "npx expo export:web"
  },
  "dependencies": {
    "@expo/vector-icons": "^13.0.0",
    "@react-native-community/masked-view": "^0.1.11",
    "expo": "^49.0.8",
    "expo-linear-gradient": "~12.3.0",
    "expo-navigation-bar": "~2.3.0",
    "expo-updates": "~0.18.13",
    "install": "^0.13.0",
    "npx": "^3.0.0",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-native": "0.72.4",
    "react-native-reanimated": "~3.3.0",
    "react-native-screens": "~3.22.0"
  },
  "devDependencies": {
    "@babel/core": "^7.20.0"
  },
  "private": true
}

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.