How to create a pie chart in SwiftUI

Pie charts are widely used in the business world and the mass media. There is some criticism of the use of pie charts as it can be difficult to compare sector sizes, however they can be useful when the number of sectors is small. In this article we walk through how to create a pie chart in SwiftUI using path.addArc method. It also shows how to add labels to each sector of the pie chart.

A Pie chart is a circular chart divided into slices to illustrate numerical proportion. The arc length of each slice (and its area) is proportional to the quantity it represents.



Display a piece of pie

SwiftUI framework provides addArc to create a sector of a pie chart. The following code creates a sector with an angle of 120 degrees. The default location and direction of the sector may seem incorrect. This is because the default orientation in SwiftUI is a flipped xy coordinate system with the top left corner starting at (0, 0). The x coordinate increases moving from left to right and y increases moving down the screen. The sector shape is centered at (200,200) and the startAngle set to zero and the endAngle set to 120 degrees. The clockwise parameter is set to false, yet the sector starts at 3 o'clock and ends at 7 o'clock, which seems in a clockwise direction. This is because of the flipped y-axis orientation.

 1struct ArcView: View {
 2    var body: some View {
 3        Path { path in
 4            path.move(to: CGPoint(x: 200, y: 200))
 5            path.addArc(center: .init(x: 200, y: 200),
 6                        radius: 150,
 7                        startAngle: Angle(degrees: 0.0),
 8                        endAngle: Angle(degrees: 120.0),
 9                        clockwise: false
10            )
11        }
12        .fill(Color.purple)
13    }
14}

Using addArc to create a piece of pie
Using addArc to create a piece of pie



Display multiple pieces of pie

Multiple sectors can be combined in a ZStack to create a pie chart. The angles are adjusted so that the pie chart starts at 12 o'clock.

 1    var body: some View {
 2        let angleOffset = 90.0
 3        ZStack {
 4            Path { path in
 5                path.move(to: CGPoint(x: 200, y: 200))
 6                path.addArc(center: .init(x: 200, y: 200),
 7                            radius: 150,
 8                            startAngle: Angle(degrees: 0.0 - angleOffset),
 9                            endAngle: Angle(degrees: 160.0 - angleOffset),
10                            clockwise: false
11                )
12            }
13            .fill(Color(red: 126/255, green: 40/255, blue: 80/255))
14
15            Path { path in
16                path.move(to: CGPoint(x: 200, y: 200))
17                path.addArc(center: .init(x: 200, y: 200),
18                            radius: 150,
19                            startAngle: Angle(degrees: 160.0 - angleOffset),
20                            endAngle: Angle(degrees: 280.0 - angleOffset),
21                            clockwise: false
22                )
23            }
24            .fill(Color(red: 37/255, green: 166/255, blue: 188/255))
25
26            Path { path in
27                path.move(to: CGPoint(x: 200, y: 200))
28                path.addArc(center: .init(x: 200, y: 200),
29                            radius: 150,
30                            startAngle: Angle(degrees: 280.0 - angleOffset),
31                            endAngle: Angle(degrees: 360.0 - angleOffset),
32                            clockwise: false
33                )
34            }
35            .fill(Color(red: 49/255, green: 63/255, blue: 95/255))
36        }
37    }

Display multiple sectors to form a pie chart using a ZStack
Display multiple sectors to form a pie chart using a ZStack



Create sector shape

Let's start by separating the code to create a Sector shape. This takes two parameters for the start angle and the end angle of the sector. It creates a sector shape of a circle that would fit in the containing frame.

 1struct Sector: Shape {
 2    var startAngle: Angle
 3    var endAngle: Angle
 4
 5    func path(in rect: CGRect) -> Path {
 6        // centre of the containing rect
 7        let c = CGPoint(x: rect.width/2.0, y: rect.height/2.0)
 8        // radius of a circle that will fit in the rect
 9        let r = Double(min(rect.width,rect.height)) * 0.9 / 2.0
10        var path = Path()
11        path.move(to: c)
12        path.addArc(center: c,
13                    radius: CGFloat(r),
14                    startAngle: startAngle,
15                    endAngle: endAngle,
16                    clockwise: false
17        )
18        path.closeSubpath()
19        return path
20    }
21}

Show two sample sectors in a ZStack.

 1struct ShowSectorView: View {
 2    var body: some View {
 3        VStack {
 4            ZStack {
 5                Sector(startAngle: Angle(degrees: -65),
 6                       endAngle: Angle(degrees: -15))
 7                    .fill(Color.red)
 8                Sector(startAngle: Angle(degrees: 115),
 9                       endAngle: Angle(degrees: 205))
10                    .fill(Color.green)
11            }
12            .frame(width: 300, height: 300)
13
14            Spacer()
15        }
16    }
17}

Custom Sector shape created between two angles
Custom Sector shape created between two angles



Create Pie Chart view using Sector shapes

The Sector shape can be used in the following PieChartView to create a pie chart. This takes an array of numbers for the values of each of the items in the pie chart. The sizes passed in are sorted in decreasing order and each size is plotted on the pie chart relative to the sum of the sizes. The pie chart is set to start at 12 o'clock and move in a clockwise direction from largest to smallest size.

 1struct PieChartView: View {
 2    var sizes:[Double]
 3
 4    let angleOffset = 90.0
 5    let colors = [
 6        Color(red: 126/255, green: 40/255, blue: 80/255),
 7        Color(red: 49/255, green: 63/255, blue: 95/255),
 8        Color(red: 37/255, green: 166/255, blue: 188/255),
 9        Color(red: 130/255, green: 122/255, blue: 162/255),
10        Color(red: 241/255, green: 165/255, blue: 138/255)]
11
12    var body: some View {
13        let total = sizes.reduce(0, +)
14        let angles = sizes.sorted(by: >).map { $0 * 360.0 / total }
15        var sum = 0.0
16        let runningAngles = angles.map { (sum += $0, sum).1 }
17
18        ZStack {
19            ForEach(0..<runningAngles.count) { i in
20                let startAngle = i==0 ? 0.0 : runningAngles[i-1]
21                Sector(
22                    startAngle: Angle(degrees: startAngle - angleOffset),
23                    endAngle: Angle(degrees: runningAngles[i] - angleOffset)
24                )
25                .fill(colors[i % colors.count])
26            }
27        }
28    }
29}

The following code demonstrates a couple of sample pie charts and shows that the pie charts scale to fit into the containing frame.

 1struct ShowPieChartView: View {
 2    var body: some View {
 3        VStack {
 4            PieChartView(sizes: [30.6, 25.5, 13.4])
 5                .frame(width: 300, height: 300)
 6                .padding(30)
 7
 8            HStack {
 9                PieChartView(sizes: [20.0, 5, 15.0, 12, 3])
10                    .frame(width: 100, height: 100)
11                PieChartView(sizes: [1,2,3,4])
12                    .frame(width: 100, height: 100)
13                PieChartView(sizes: [300, 200])
14                    .frame(width: 100, height: 100)
15            }
16
17            Spacer()
18        }
19    }
20}

Custom View to create a Piechart
Custom View to create a Piechart



How many sectors in a pie chart

The number of colors defined for the sectors in the pie chart is limited to five as there is a limit to how useful pie charts are for a large number of items. Pie charts do get a bad rap every now and then. I think they can be useful if the items being compared is limited in number to two or three and there are significant differences to show. The following image shows that it is not possible to tell when three sectors are slightly different. Pie charts can be useful to show when one item is dominant relative to other items. I've included a pie chart with one sector as more of a test on the PieChartView, but this is a really useless pie chart. To make PieChartView more robust, guard statements could be added to set constraints on the number of sectors allowed.

 1struct PieChartsView: View {
 2    var body: some View {
 3        VStack {
 4            HStack {
 5                VStack {
 6                    PieChartView(sizes: [30])
 7                        .frame(width: 280, height: 280)
 8                    Text("One Sector")
 9                }
10                VStack {
11                    PieChartView(sizes: [30, 28])
12                        .frame(width: 280, height: 280)
13                    Text("Two Sectors")
14                }
15                VStack {
16                    PieChartView(sizes: [30, 28, 26])
17                        .frame(width: 280, height: 280)
18                    Text("Three Sectors")
19                }
20            }
21            HStack {
22                VStack {
23                    PieChartView(sizes: [30, 28, 26, 24])
24                        .frame(width: 280, height: 280)
25                    Text("Four Sectors")
26                }
27                VStack {
28                    PieChartView(sizes: [30, 28, 26, 24, 22])
29                        .frame(width: 280, height: 280)
30                    Text("Five Sectors")
31                }
32                VStack {
33                    PieChartView(sizes: [130, 28, 100, 24, 22])
34                        .frame(width: 280, height: 280)
35                    Text("Five Sectors")
36                }
37            }
38            Spacer()
39        }
40        .font(.title)
41    }
42}

Pie charts are more useful when the number of sectors is two or three

Pie charts are more useful when the number of sectors is two or three



Sector with Label

Create a SectorView to contain a sector and a label. This will allow the creation of a pie chart with labels on each section, which will help clarify when sectors are similar sizes. A number of new parameters are required by the view to specify the sector color as well value and offset for the label.

 1struct SectorView: View {
 2    var startAngle: Angle
 3    var endAngle: Angle
 4    var value: Double
 5    var color: Color
 6    var labelOffset: Double
 7    var labelSize: Double = 20.0
 8
 9    var labelPoint: CGPoint {
10        let midAngleRad = startAngle.radians + (endAngle.radians - startAngle.radians)/2.0
11        return CGPoint(x: labelOffset * cos(midAngleRad),
12                       y: labelOffset * sin(midAngleRad))
13    }
14
15    var body: some View {
16        VStack {
17            Sector(
18                startAngle: startAngle,
19                endAngle: endAngle
20            )
21            .fill(color)
22            .overlay(
23                Text("\(value, specifier: "%.1f")")
24                    .font(.system(size: CGFloat(labelSize),
25                                  weight:.bold,
26                                  design: .rounded))
27                    .padding(4)
28                    .foregroundColor(.white)
29                    .background(Color(.black).opacity(0.3))
30                    .cornerRadius(5)
31                    .offset(x: labelPoint.x, y: labelPoint.y)
32            )
33        }
34    }
35}

Custom view for a sector with a label
Custom view for a sector with a label



Pie Chart with labels

A new Pie chart view with label is created that is similar to the original PieChartView. In the ZStack for each of the sizes a SectorView is created rather than a Sector shape previously.

 1struct PieChartWithLabelView: View {
 2    var sizes: [Double]
 3    var labelOffset : Double
 4    var labelSize: Double = 20.0
 5
 6    let angleOffset = 90.0
 7    let colors = [
 8        Color(red: 126/255, green: 40/255, blue: 80/255),
 9        Color(red: 49/255, green: 63/255, blue: 95/255),
10        Color(red: 37/255, green: 166/255, blue: 188/255),
11        Color(red: 130/255, green: 122/255, blue: 162/255),
12        Color(red: 241/255, green: 165/255, blue: 138/255)]
13
14    var body: some View {
15        let total = sizes.reduce(0, +)
16        let sortedSizes = sizes.sorted(by: >)
17        let angles = sizes.sorted(by: >).map { $0 * 360.0 / total }
18        var sum = 0.0
19        let runningAngles = angles.map { (sum += $0, sum).1 }
20
21        ZStack {
22            ForEach(0..<runningAngles.count) { i in
23                let startAngle = i==0 ? 0.0 : runningAngles[i-1]
24                SectorView(
25                    startAngle: Angle(degrees: startAngle - angleOffset),
26                    endAngle: Angle(degrees: runningAngles[i] - angleOffset),
27                    value: sortedSizes[i],
28                    color: colors[i % colors.count],
29                    labelOffset: labelOffset,
30                    labelSize: labelSize)
31            }
32        }
33    }
34}

Tis code shows examples of the Pie Charts with labels as well as showing how the pie chart size responds to fit the frame size.

 1    var body: some View {
 2        VStack(spacing:30) {
 3            PieChartWithLabelView(sizes: [30, 15, 9],
 4                                  labelOffset: 70)
 5                .frame(width: 280, height: 280)
 6
 7            PieChartWithLabelView(sizes: [3, 2, 6],
 8                                  labelOffset: 30,
 9                                  labelSize: 8)
10                .frame(width: 100, height: 100)
11
12            Spacer()
13        }
14    }

Pie chart with labels
Pie chart with labels




Conclusion

This article showed how to create a pie chart from sector shapes in SwiftUI. This is not meant to be a charting package, but it does demonstrate how to encapsulate functionality to create a Sector shape inside a Sector View and how to combine these views in a Pie chart view.