DEV Community

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

Posted on

How to Create a 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

Today we are going to create a chart that will take into account three values, the monetary amount spent on groceries, the amount spent on expenses and the regular expenses.

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

Then we will declare three amounts, the groceries, the bills and the regular. Now let's declare the total which is the sum of the previous three.

// @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 groceries = 241;
  const bills = 372;
  const regular = 188;
  const total = groceries + bills + regular;

  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 calculate the percentage of each one according to the total.

// @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 groceries = 241;
  const bills = 372;
  const regular = 188;
  const total = groceries + bills + regular;

  const groceriesPercentage = (groceries / total) * 100;
  const billsPercentage = (bills / total) * 100;
  const regularPercentage = (regular / total) * 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

What remains to be done is to calculate the displacement of each one's stroke.

// @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 groceries = 241;
  const bills = 372;
  const regular = 188;
  const total = groceries + bills + regular;

  const groceriesPercentage = (groceries / total) * 100;
  const billsPercentage = (bills / total) * 100;
  const regularPercentage = (regular / total) * 100;

  const groceriesStrokeDashoffset =
    circleCircumference - (circleCircumference * groceriesPercentage) / 100;
  const billsStrokeDashoffset =
    circleCircumference - (circleCircumference * billsPercentage) / 100;
  const regularStrokeDashoffset =
    circleCircumference - (circleCircumference * regularPercentage) / 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 the least, we will calculate the angle of each one.

// @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 groceries = 241;
  const bills = 372;
  const regular = 188;
  const total = groceries + bills + regular;

  const groceriesPercentage = (groceries / total) * 100;
  const billsPercentage = (bills / total) * 100;
  const regularPercentage = (regular / total) * 100;

  const groceriesStrokeDashoffset =
    circleCircumference - (circleCircumference * groceriesPercentage) / 100;
  const billsStrokeDashoffset =
    circleCircumference - (circleCircumference * billsPercentage) / 100;
  const regularStrokeDashoffset =
    circleCircumference - (circleCircumference * regularPercentage) / 100;

  const groceriesAngle = (groceries / total) * 360;
  const billsAngle = (bills / total) * 360;
  const regularAngle = groceriesAngle + billsAngle;

  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 jsx. 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 groceries = 241;
  const bills = 372;
  const regular = 188;
  const total = groceries + bills + regular;

  const groceriesPercentage = (groceries / total) * 100;
  const billsPercentage = (bills / total) * 100;
  const regularPercentage = (regular / total) * 100;

  const groceriesStrokeDashoffset =
    circleCircumference - (circleCircumference * groceriesPercentage) / 100;
  const billsStrokeDashoffset =
    circleCircumference - (circleCircumference * billsPercentage) / 100;
  const regularStrokeDashoffset =
    circleCircumference - (circleCircumference * regularPercentage) / 100;

  const groceriesAngle = (groceries / total) * 360;
  const billsAngle = (bills / total) * 360;
  const regularAngle = groceriesAngle + billsAngle;

  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 groceries = 241;
  const bills = 372;
  const regular = 188;
  const total = groceries + bills + regular;

  const groceriesPercentage = (groceries / total) * 100;
  const billsPercentage = (bills / total) * 100;
  const regularPercentage = (regular / total) * 100;

  const groceriesStrokeDashoffset =
    circleCircumference - (circleCircumference * groceriesPercentage) / 100;
  const billsStrokeDashoffset =
    circleCircumference - (circleCircumference * billsPercentage) / 100;
  const regularStrokeDashoffset =
    circleCircumference - (circleCircumference * regularPercentage) / 100;

  const groceriesAngle = (groceries / total) * 360;
  const billsAngle = (bills / total) * 360;
  const regularAngle = groceriesAngle + billsAngle;

  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 the total can have zero value, because we start the month without any expenses, they will gradually accumulate. 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 groceries = 241;
  const bills = 372;
  const regular = 188;
  const total = groceries + bills + regular;

  const groceriesPercentage = (groceries / total) * 100;
  const billsPercentage = (bills / total) * 100;
  const regularPercentage = (regular / total) * 100;

  const groceriesStrokeDashoffset =
    circleCircumference - (circleCircumference * groceriesPercentage) / 100;
  const billsStrokeDashoffset =
    circleCircumference - (circleCircumference * billsPercentage) / 100;
  const regularStrokeDashoffset =
    circleCircumference - (circleCircumference * regularPercentage) / 100;

  const groceriesAngle = (groceries / total) * 360;
  const billsAngle = (bills / total) * 360;
  const regularAngle = groceriesAngle + billsAngle;

  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 create the first circle that will correspond to the expenses we had on buying groceries. So let's pass the value of the radius, the circumference, the stroke dash offset as a prop, and let's give it a rotation of 0 degrees.

// @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 groceries = 241;
  const bills = 372;
  const regular = 188;
  const total = groceries + bills + regular;

  const groceriesPercentage = (groceries / total) * 100;
  const billsPercentage = (bills / total) * 100;
  const regularPercentage = (regular / total) * 100;

  const groceriesStrokeDashoffset =
    circleCircumference - (circleCircumference * groceriesPercentage) / 100;
  const billsStrokeDashoffset =
    circleCircumference - (circleCircumference * billsPercentage) / 100;
  const regularStrokeDashoffset =
    circleCircumference - (circleCircumference * regularPercentage) / 100;

  const groceriesAngle = (groceries / total) * 360;
  const billsAngle = (bills / total) * 360;
  const regularAngle = groceriesAngle + billsAngle;

  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"
              />
             ) : (
               <>
                 <Circle
                  cx="50%"
                  cy="50%"
                  r={radius}
                  stroke="#F05454"
                  fill="transparent"
                  strokeWidth="40"
                  strokeDasharray={circleCircumference}
                  strokeDashoffset={groceriesStrokeDashoffset}
                  rotation={0}
                  originX="90"
                  originY="90"
                  strokeLinecap="round"
                 />
                 // ...
               </>
             )
            }
          </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 create the second circle that will correspond to the expenses that we had to pay bills. So we're going to pass the value of the radius, the circumference, the stroke dash offset as a prop, and the rotation will be the value of the angle of the groceries (because we want it to start after the stroke of the groceries).

// @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 groceries = 241;
  const bills = 372;
  const regular = 188;
  const total = groceries + bills + regular;

  const groceriesPercentage = (groceries / total) * 100;
  const billsPercentage = (bills / total) * 100;
  const regularPercentage = (regular / total) * 100;

  const groceriesStrokeDashoffset =
    circleCircumference - (circleCircumference * groceriesPercentage) / 100;
  const billsStrokeDashoffset =
    circleCircumference - (circleCircumference * billsPercentage) / 100;
  const regularStrokeDashoffset =
    circleCircumference - (circleCircumference * regularPercentage) / 100;

  const groceriesAngle = (groceries / total) * 360;
  const billsAngle = (bills / total) * 360;
  const regularAngle = groceriesAngle + billsAngle;

  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"
              />
             ) : (
               <>
                 <Circle
                  cx="50%"
                  cy="50%"
                  r={radius}
                  stroke="#F05454"
                  fill="transparent"
                  strokeWidth="40"
                  strokeDasharray={circleCircumference}
                  strokeDashoffset={groceriesStrokeDashoffset}
                  rotation={0}
                  originX="90"
                  originY="90"
                  strokeLinecap="round"
                 />
                 <Circle
                  cx="50%"
                  cy="50%"
                  r={radius}
                  stroke="#30475E"
                  fill="transparent"
                  strokeWidth="40"
                  strokeDasharray={circleCircumference}
                  strokeDashoffset={billsStrokeDashoffset}
                  rotation={groceriesAngle}
                  originX="90"
                  originY="90"
                  strokeLinecap="round"
                 />
                 // ...
               </>
             )
            }
          </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 create the third and last circle that will correspond to the expenses we had with regular expenses. So we will pass as prop the value of the radius, the circumference, the stroke dash offset, and the rotation will be the value of the regular expenses angle.

// @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 groceries = 241;
  const bills = 372;
  const regular = 188;
  const total = groceries + bills + regular;

  const groceriesPercentage = (groceries / total) * 100;
  const billsPercentage = (bills / total) * 100;
  const regularPercentage = (regular / total) * 100;

  const groceriesStrokeDashoffset =
    circleCircumference - (circleCircumference * groceriesPercentage) / 100;
  const billsStrokeDashoffset =
    circleCircumference - (circleCircumference * billsPercentage) / 100;
  const regularStrokeDashoffset =
    circleCircumference - (circleCircumference * regularPercentage) / 100;

  const groceriesAngle = (groceries / total) * 360;
  const billsAngle = (bills / total) * 360;
  const regularAngle = groceriesAngle + billsAngle;

  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"
              />
             ) : (
               <>
                 <Circle
                  cx="50%"
                  cy="50%"
                  r={radius}
                  stroke="#F05454"
                  fill="transparent"
                  strokeWidth="40"
                  strokeDasharray={circleCircumference}
                  strokeDashoffset={groceriesStrokeDashoffset}
                  rotation={0}
                  originX="90"
                  originY="90"
                  strokeLinecap="round"
                 />
                 <Circle
                  cx="50%"
                  cy="50%"
                  r={radius}
                  stroke="#30475E"
                  fill="transparent"
                  strokeWidth="40"
                  strokeDasharray={circleCircumference}
                  strokeDashoffset={billsStrokeDashoffset}
                  rotation={groceriesAngle}
                  originX="90"
                  originY="90"
                  strokeLinecap="round"
                 />
                 <Circle
                  cx="50%"
                  cy="50%"
                  r={radius}
                  stroke="#222831"
                  fill="transparent"
                  strokeWidth="40"
                  strokeDasharray={circleCircumference}
                  strokeDashoffset={regularStrokeDashoffset}
                  rotation={regularAngle}
                  originX="90"
                  originY="90"
                  strokeLinecap="round"
                />
               </>
             )
            }
          </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

Last but not least, just show the amount that was spent, for that we will import the React Native Text component 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 groceries = 241;
  const bills = 372;
  const regular = 188;
  const total = groceries + bills + regular;

  const groceriesPercentage = (groceries / total) * 100;
  const billsPercentage = (bills / total) * 100;
  const regularPercentage = (regular / total) * 100;

  const groceriesStrokeDashoffset =
    circleCircumference - (circleCircumference * groceriesPercentage) / 100;
  const billsStrokeDashoffset =
    circleCircumference - (circleCircumference * billsPercentage) / 100;
  const regularStrokeDashoffset =
    circleCircumference - (circleCircumference * regularPercentage) / 100;

  const groceriesAngle = (groceries / total) * 360;
  const billsAngle = (bills / total) * 360;
  const regularAngle = groceriesAngle + billsAngle;

  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"
              />
             ) : (
               <>
                 <Circle
                  cx="50%"
                  cy="50%"
                  r={radius}
                  stroke="#F05454"
                  fill="transparent"
                  strokeWidth="40"
                  strokeDasharray={circleCircumference}
                  strokeDashoffset={groceriesStrokeDashoffset}
                  rotation={0}
                  originX="90"
                  originY="90"
                  strokeLinecap="round"
                 />
                 <Circle
                  cx="50%"
                  cy="50%"
                  r={radius}
                  stroke="#30475E"
                  fill="transparent"
                  strokeWidth="40"
                  strokeDasharray={circleCircumference}
                  strokeDashoffset={billsStrokeDashoffset}
                  rotation={groceriesAngle}
                  originX="90"
                  originY="90"
                  strokeLinecap="round"
                 />
                 <Circle
                  cx="50%"
                  cy="50%"
                  r={radius}
                  stroke="#222831"
                  fill="transparent"
                  strokeWidth="40"
                  strokeDasharray={circleCircumference}
                  strokeDashoffset={regularStrokeDashoffset}
                  rotation={regularAngle}
                  originX="90"
                  originY="90"
                  strokeLinecap="round"
                />
               </>
             )
            }
          </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,
  },
});
Enter fullscreen mode Exit fullscreen mode

You should get a result similar to this:

chart final result with data

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! 🤙

Top comments (1)

Collapse
 
carlosdaniiel07 profile image
Carlos Daniel Martins de Almeida

Awesome!
Thank you very much!