0

My UI designer asked me to build this donut pie chart and I have been banging my head against the wall for days! It seems impossible. Is there a way to do this in SwiftUI or should I ask the designer for a design that's actually implementable?

enter image description here

I'm able to make the below, but then I'm stuck. I can't get the separation between each segment and I can't get the rounded corners. I thought about a border but that doesn't work. Any thoughts?: enter image description here

import SwiftUI

struct ContentView: View {
    @ObservedObject var charDataObj = ChartDataContainer()
    @State var indexOfTappedSlice = -1
    
    var body: some View {
        VStack {
            Spacer()
            
            ZStack {
                Circle()
                    .stroke(Color(hex: 0x2A3950), style: StrokeStyle(lineWidth: 100, lineCap: .round, lineJoin: .round))
                    .frame(width: 220, height: 270)
                
                ForEach(0..<charDataObj.chartData.count) { index in
                    Circle()
                        .trim(from: index == 0 ? 0.0 : charDataObj.chartData[index-1].value / 100,
                              to: (charDataObj.chartData[index].value / 100))
                        .stroke(charDataObj.chartData[index].color, style: StrokeStyle(lineWidth: 50.0, lineCap: .butt, lineJoin: .round))
                        .rotationEffect(Angle(degrees: 270.0))
                        .onTapGesture {
                            indexOfTappedSlice = indexOfTappedSlice == index ? -1 : index
                        }
                        .scaleEffect(index == indexOfTappedSlice ? 1.1 : 1.0)
                        .animation(.spring())
                }
            }.frame(width: 150, height: 150)
            
            Spacer()
        }
        .frame(maxWidth: .infinity)
        .background(Color(hex: 0x42506B))
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

//MARK:- Chart Data
struct ChartData {
    var id = UUID()
    var color : Color
    var percent : CGFloat
    var value : CGFloat
}

@available(iOS 15.0, *)
class ChartDataContainer : ObservableObject {
    @Published var chartData =
    [ChartData(color: .green, percent: 26, value: 0),
     ChartData(color: .blue, percent: 28, value: 0),
     ChartData(color: .white, percent: 23, value: 0),
     ChartData(color: .yellow, percent: 12, value: 0),
     ChartData(color: .red, percent: 11, value: 0),]
    
    init() {
        calc()
    }
    func calc(){
        var value : CGFloat = 0
        
        for i in 0..<chartData.count {
            value += chartData[i].percent
            chartData[i].value = value
        }
    }
}

extension Color {
    init(hex: Int, opacity: Double = 1.0) {
        let red = Double((hex & 0xff0000) >> 16) / 255.0
        let green = Double((hex & 0xff00) >> 8) / 255.0
        let blue = Double((hex & 0xff) >> 0) / 255.0
        self.init(.sRGB, red: red, green: green, blue: blue, opacity: opacity)
    }
}

I can get some separation with an adjustment to the trim, but it looks awful:

.trim(from: index == 0 ? 0.003: (charDataObj.chartData[index-1].value / 100) + 0.003,
                              to: (charDataObj.chartData[index].value / 100) - 0.003)

enter image description here

5
  • Hmm, this would definitely be possible with a custom shape a bunch of trigonometry... this might help: onmyway133.com/posts/how-to-draw-arc-corner-using-bezier-path Commented Mar 14, 2023 at 4:34
  • I think it would be a custom shape, take the percentage as a parameter and draw a new shape without using Circle() see developer.apple.com/documentation/swiftui/shape Commented Mar 14, 2023 at 5:16
  • 1
    @aheze thanks. I've not worked with custom shapes before, will take a look. Commented Mar 14, 2023 at 14:38
  • Were you able to come up with a solution for iOS 16? Commented Sep 27, 2023 at 0:12
  • @EricJubber no, only iOS 17: stackoverflow.com/a/76475855/3890041 Commented Sep 28, 2023 at 1:07

1 Answer 1

3

This is now possible in iOS 17, and it's very easy:

Chart(dailySales, id:\.day) { element in
    SectorMark(
        angle: .value("Sales", element.sales),
        innerRadius: .ratio(0.55),
        angularInset: 2
    )
    .cornerRadius(10)
    .foregroundStyle(element.color)
}
.rotationEffect(Angle(degrees: -35))
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.