DEV Community

Cover image for How to Create a Dynamic Donut Pie Chart using React Native SVG
Francisco Mendes
Francisco Mendes

Posted on

How to Create a Dynamic Donut Pie Chart using React Native SVG

Overview

One of the things we all end up needing to do at some point in our career is creating custom charts. One of the approaches we can take is to create charts with SVG's.

And today I'm going to use React Native together with react-native-svg, obviously there are other bookstores that do the same but I'm only going to use this one because it's the one I feel more confident working with.

Today's example

In today's example, we will receive two values, one of which will be the number of items that have lost their warranty and the other will be the number of items that have not yet expired.

But this time we are not going to represent the graph in percentage as we did in other examples, today each of these items that has/had a warranty will be represented by a single stroke.

Let's code

Let's install the following dependencies:

npm install react-native-svg
Enter fullscreen mode Exit fullscreen mode

Now we can start working on our component.

// @src/App.js
import React from 'react';

const App = () => {
  return (
    // ...
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Then we will import the View component and StyleSheet from React Native to create our container.

// @src/App.js
import React from 'react';
import { View, StyleSheet } from "react-native";

const App = () => {
  return (
    <View style={styles.container}>
      // ...
    </View>
  );
};

export default App;


const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
});
Enter fullscreen mode Exit fullscreen mode

Next we will create our chart wrapper which will contain the chart and the amount that has already been spent.

// @src/App.js
import React from 'react';
import { View, StyleSheet } from "react-native";

const App = () => {
  return (
    <View style={styles.container}>
      <View style={styles.graphWrapper}>
        // ...
      </View>
    </View>
  );
};

export default App;


const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  graphWrapper: {
    alignItems: "center",
    justifyContent: "center",
  },
});
Enter fullscreen mode Exit fullscreen mode

Now let's import Svg, G (Group) and Circle from react-native-svg, so we can start working on our chart.

But first we have to start working on our data, so let's define the radius of the circle and the circumference of the circle.

// @src/App.js
import React from 'react';
import { View, StyleSheet } from "react-native";
import Svg, { G, Circle } from "react-native-svg";

const App = () => {
  const radius = 70;
  const circleCircumference = 2 * Math.PI * radius;

  return (
    <View style={styles.container}>
      <View style={styles.graphWrapper}>
        // ...
      </View>
    </View>
  );
};

export default App;


const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  graphWrapper: {
    alignItems: "center",
    justifyContent: "center",
  },
});
Enter fullscreen mode Exit fullscreen mode

Now let's define each of the values as well as the total that will be the sum of the two.

// @src/App.js
import React from 'react';
import { View, StyleSheet } from "react-native";
import Svg, { G, Circle } from "react-native-svg";

const App = () => {
  const radius = 70;
  const circleCircumference = 2 * Math.PI * radius;

  const expired = 2;
  const nonExpired = 4;
  const total = expired + nonExpired;

  return (
    <View style={styles.container}>
      <View style={styles.graphWrapper}>
        // ...
      </View>
    </View>
  );
};

export default App;


const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  graphWrapper: {
    alignItems: "center",
    justifyContent: "center",
  },
});
Enter fullscreen mode Exit fullscreen mode

After that we will create a variable named data which will be an empty array. In this empty array, each element (circles) that will be present in the chart will be inserted. However we have two different values, one is for warranties that have expired and the other is for warranties that have not expired.

Each of the values will have a different color and so we will do a for loop each so that they are inserted into the array.

// @src/App.js
import React from 'react';
import { View, StyleSheet } from "react-native";
import Svg, { G, Circle } from "react-native-svg";

const App = () => {
  const radius = 70;
  const circleCircumference = 2 * Math.PI * radius;

  const expired = 2;
  const nonExpired = 4;
  const total = expired + nonExpired;

  const data = [];

  for (let i = 1; i <= expired; i++) {
    data.push({
      color: "#F0A500",
    });
  }

  for (let i = 1; i <= nonExpired; i++) {
    data.push({ color: "#334756" });
  }

  return (
    <View style={styles.container}>
      <View style={styles.graphWrapper}>
        // ...
      </View>
    </View>
  );
};

export default App;


const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  graphWrapper: {
    alignItems: "center",
    justifyContent: "center",
  },
});
Enter fullscreen mode Exit fullscreen mode

After that we will need to know the percentage each one occupies according to the total, as well as the value of the stroke dash offset.

// @src/App.js
import React from 'react';
import { View, StyleSheet } from "react-native";
import Svg, { G, Circle } from "react-native-svg";

const App = () => {
  const radius = 70;
  const circleCircumference = 2 * Math.PI * radius;

  const expired = 2;
  const nonExpired = 4;
  const total = expired + nonExpired;

  const data = [];

  for (let i = 1; i <= expired; i++) {
    data.push({
      color: "#F0A500",
    });
  }

  for (let i = 1; i <= nonExpired; i++) {
    data.push({ color: "#334756" });
  }

  const percentage = (1 / total) * 100;
  const strokeDashoffset =
    circleCircumference - (circleCircumference * percentage) / 100;

  return (
    <View style={styles.container}>
      <View style={styles.graphWrapper}>
        // ...
      </View>
    </View>
  );
};

export default App;


const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  graphWrapper: {
    alignItems: "center",
    justifyContent: "center",
  },
});
Enter fullscreen mode Exit fullscreen mode

Last but not least, we need to know what angle each one occupies on the circumference, as we will establish a spacing between each of the elements.

If we only have one element we will not have any spacing, otherwise we will have a small spacing.

// @src/App.js
import React from 'react';
import { View, StyleSheet } from "react-native";
import Svg, { G, Circle } from "react-native-svg";

const App = () => {
  const radius = 70;
  const circleCircumference = 2 * Math.PI * radius;

  const expired = 2;
  const nonExpired = 4;
  const total = expired + nonExpired;

  const data = [];

  for (let i = 1; i <= expired; i++) {
    data.push({
      color: "#F0A500",
    });
  }

  for (let i = 1; i <= nonExpired; i++) {
    data.push({ color: "#334756" });
  }

  const percentage = (1 / total) * 100;
  const strokeDashoffset =
    circleCircumference - (circleCircumference * percentage) / 100;

  const angle = (1 / total) * 360;
  const sliceSpacing = total === 1 ? 0 : 4;

  return (
    <View style={styles.container}>
      <View style={styles.graphWrapper}>
        // ...
      </View>
    </View>
  );
};

export default App;


const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  graphWrapper: {
    alignItems: "center",
    justifyContent: "center",
  },
});
Enter fullscreen mode Exit fullscreen mode

Now we can start working on our chart. First let's establish the screen space that will be used, using the Svg tag.

// @src/App.js
import React from 'react';
import { View, StyleSheet } from "react-native";
import Svg, { G, Circle } from "react-native-svg";

const App = () => {
  const radius = 70;
  const circleCircumference = 2 * Math.PI * radius;

  const expired = 2;
  const nonExpired = 4;
  const total = expired + nonExpired;

  const data = [];

  for (let i = 1; i <= expired; i++) {
    data.push({
      color: "#F0A500",
    });
  }

  for (let i = 1; i <= nonExpired; i++) {
    data.push({ color: "#334756" });
  }

  const percentage = (1 / total) * 100;
  const strokeDashoffset =
    circleCircumference - (circleCircumference * percentage) / 100;

  const angle = (1 / total) * 360;
  const sliceSpacing = total === 1 ? 0 : 4;

  return (
    <View style={styles.container}>
      <View style={styles.graphWrapper}>
        <Svg height="160" width="160" viewBox="0 0 180 180">
          // ...
        </Svg>
      </View>
    </View>
  );
};

export default App;


const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  graphWrapper: {
    alignItems: "center",
    justifyContent: "center",
  },
});
Enter fullscreen mode Exit fullscreen mode

Now we're going to use the G tag and we're going to define an axis and then give it a little rotation. The use of this tag is due to the fact that we end up having more than one geometric shape, which in this case is the circle.

// @src/App.js
import React from 'react';
import { View, StyleSheet } from "react-native";
import Svg, { G, Circle } from "react-native-svg";

const App = () => {
  const radius = 70;
  const circleCircumference = 2 * Math.PI * radius;

  const expired = 2;
  const nonExpired = 4;
  const total = expired + nonExpired;

  const data = [];

  for (let i = 1; i <= expired; i++) {
    data.push({
      color: "#F0A500",
    });
  }

  for (let i = 1; i <= nonExpired; i++) {
    data.push({ color: "#334756" });
  }

  const percentage = (1 / total) * 100;
  const strokeDashoffset =
    circleCircumference - (circleCircumference * percentage) / 100;

  const angle = (1 / total) * 360;
  const sliceSpacing = total === 1 ? 0 : 4;

  return (
    <View style={styles.container}>
      <View style={styles.graphWrapper}>
        <Svg height="160" width="160" viewBox="0 0 180 180">
          <G rotation={-90} originX="90" originY="90">
            // ...
          </G>
        </Svg>
      </View>
    </View>
  );
};

export default App;


const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  graphWrapper: {
    alignItems: "center",
    justifyContent: "center",
  },
});
Enter fullscreen mode Exit fullscreen mode

Now let's do conditional rendering, this is because we may not have any warranties. So if the total value is zero, we will show a circle with a white stroke and the radius we defined earlier.

// @src/App.js
import React from 'react';
import { View, StyleSheet } from "react-native";
import Svg, { G, Circle } from "react-native-svg";

const App = () => {
  const radius = 70;
  const circleCircumference = 2 * Math.PI * radius;

  const expired = 2;
  const nonExpired = 4;
  const total = expired + nonExpired;

  const data = [];

  for (let i = 1; i <= expired; i++) {
    data.push({
      color: "#F0A500",
    });
  }

  for (let i = 1; i <= nonExpired; i++) {
    data.push({ color: "#334756" });
  }

  const percentage = (1 / total) * 100;
  const strokeDashoffset =
    circleCircumference - (circleCircumference * percentage) / 100;

  const angle = (1 / total) * 360;
  const sliceSpacing = total === 1 ? 0 : 4;

  return (
    <View style={styles.container}>
      <View style={styles.graphWrapper}>
        <Svg height="160" width="160" viewBox="0 0 180 180">
          <G rotation={-90} originX="90" originY="90">
            { total === 0 ? (
              <Circle
                cx="50%"
                cy="50%"
                r={radius}
                stroke="#F1F6F9"
                fill="transparent"
                strokeWidth="40"
              />
             ) : (
               // ...
             )}
          </G>
        </Svg>
      </View>
    </View>
  );
};

export default App;


const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  graphWrapper: {
    alignItems: "center",
    justifyContent: "center",
  },
});
Enter fullscreen mode Exit fullscreen mode

Now let's make a map of the circles we have in the named data array. The unique key that will be used to map the array elements will be the index of the element, then we will pass the radius value, the color of the element, the value of the circle circumference, the value of the stroke dash offset with the spacing and we'll rotate it according to the element's index value.

// @src/App.js
import React from 'react';
import { View, StyleSheet } from "react-native";
import Svg, { G, Circle } from "react-native-svg";

const App = () => {
  const radius = 70;
  const circleCircumference = 2 * Math.PI * radius;

  const expired = 2;
  const nonExpired = 4;
  const total = expired + nonExpired;

  const data = [];

  for (let i = 1; i <= expired; i++) {
    data.push({
      color: "#F0A500",
    });
  }

  for (let i = 1; i <= nonExpired; i++) {
    data.push({ color: "#334756" });
  }

  const percentage = (1 / total) * 100;
  const strokeDashoffset =
    circleCircumference - (circleCircumference * percentage) / 100;

  const angle = (1 / total) * 360;
  const sliceSpacing = total === 1 ? 0 : 4;

  return (
    <View style={styles.container}>
      <View style={styles.graphWrapper}>
        <Svg height="160" width="160" viewBox="0 0 180 180">
          <G rotation={-90} originX="90" originY="90">
            { total === 0 ? (
              <Circle
                cx="50%"
                cy="50%"
                r={radius}
                stroke="#F1F6F9"
                fill="transparent"
                strokeWidth="40"
              />
             ) : (
               data.map((element, index) => (
                <Circle
                  key={index}
                  cx="50%"
                  cy="50%"
                  r={radius}
                  stroke={element.color}
                  fill="transparent"
                  strokeWidth="40"
                  strokeDasharray={circleCircumference}
                  strokeDashoffset={strokeDashoffset + sliceSpacing}
                  rotation={angle * index}
                  originX="90"
                  originY="90"
                />
              ))
             )}
          </G>
        </Svg>
      </View>
    </View>
  );
};

export default App;


const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  graphWrapper: {
    alignItems: "center",
    justifyContent: "center",
  },
});
Enter fullscreen mode Exit fullscreen mode

All that remains is for us to show the total value of guarantees that the user holds in the graph, for that we will import the Text component from React Native and create styles for it.

// @src/App.js
import React from 'react';
import { View, StyleSheet, Text } from "react-native";
import Svg, { G, Circle } from "react-native-svg";

const App = () => {
  const radius = 70;
  const circleCircumference = 2 * Math.PI * radius;

  const expired = 2;
  const nonExpired = 4;
  const total = expired + nonExpired;

  const data = [];

  for (let i = 1; i <= expired; i++) {
    data.push({
      color: "#F0A500",
    });
  }

  for (let i = 1; i <= nonExpired; i++) {
    data.push({ color: "#334756" });
  }

  const percentage = (1 / total) * 100;
  const strokeDashoffset =
    circleCircumference - (circleCircumference * percentage) / 100;

  const angle = (1 / total) * 360;
  const sliceSpacing = total === 1 ? 0 : 4;

  return (
    <View style={styles.container}>
      <View style={styles.graphWrapper}>
        <Svg height="160" width="160" viewBox="0 0 180 180">
          <G rotation={-90} originX="90" originY="90">
            { total === 0 ? (
              <Circle
                cx="50%"
                cy="50%"
                r={radius}
                stroke="#F1F6F9"
                fill="transparent"
                strokeWidth="40"
              />
             ) : (
               data.map((element, index) => (
                <Circle
                  key={index}
                  cx="50%"
                  cy="50%"
                  r={radius}
                  stroke={element.color}
                  fill="transparent"
                  strokeWidth="40"
                  strokeDasharray={circleCircumference}
                  strokeDashoffset={strokeDashoffset + sliceSpacing}
                  rotation={angle * index}
                  originX="90"
                  originY="90"
                />
              ))
             )}
          </G>
        </Svg>
        <Text style={styles.label}>{total}</Text>
      </View>
    </View>
  );
};

export default App;


const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
  },
  graphWrapper: {
    alignItems: "center",
    justifyContent: "center",
  },
  label: {
    position: "absolute",
    textAlign: "center",
    fontWeight: "700",
    fontSize: 24,
    color: "#082032",
  },
});
Enter fullscreen mode Exit fullscreen mode

You should get a result similar to this:

chart final result

Conclusion

As always, I hope you found it interesting. If you noticed any errors in this article, please mention them in the comments. 🧑🏻‍💻

Hope you have a great day! 🙌

Discussion (0)