skip to Main Content

I am trying to make pie chart as below which is working fine but I have issue with end path (which is orange in below image).

enter image description here

What I want to do is make end of orange shape to below the green one so that I can achieve as below.

enter image description here

Any suggestion how this can be done?

Code can be found at below link.

https://drive.google.com/file/d/1ST0zNooLgRaI8s2pDK3NMjBQYjBSRoXB/view?usp=sharing

Below is what I have.

func drawBeizer(start_angle : CGFloat, end_angle : CGFloat, final_color : UIColor) {
    let path1 : UIBezierPath = UIBezierPath()
    
    path1.addArc(withCenter: CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2), radius: ((self.frame.size.width-main_view_width)/2), startAngle: start_angle, endAngle: end_angle, clockwise: true)
    path1.lineWidth = main_view_width
    path1.lineCapStyle = .round
    final_color.setStroke()
    path1.stroke()
    
}

This function I am passing start angle and end angle & color for the path.

2

Answers


  1. Chosen as BEST ANSWER

    I found a way to do this. Below is the full code.

    I am adding other addArc using end angles.

    ViewController.swift

    import UIKit
    
    class ViewController: UIViewController {
    
        @IBOutlet weak var myView: MyView!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            
            myView.setupData(inner_color: [.green, .purple, .blue, .orange], inner_view_width: self.view.frame.width/5.0, inner_angle: [0.0, 90.0, 180.0, 270.0])
            
            myView.backgroundColor = UIColor.white
    
        }
        
    }
    

    MyView.swift

    import UIKit
    
    class MyView: UIView {
        
        var main_radius : CGFloat = 0.0
        
        var main_color : [UIColor] = [UIColor.black, UIColor.green]
        
        var main_view_width : CGFloat = 1.0
        
        var main_angle : [CGFloat] = [-CGFloat.pi, 0.0]
        var actual_angle : [CGFloat] = []
        
        func setupData(inner_color : [UIColor], inner_view_width : CGFloat, inner_angle : [CGFloat]) {
            main_color = inner_color
            
            main_view_width = inner_view_width
            
            main_angle = inner_angle
            
            actual_angle = inner_angle
            
            var temp_main_angle : [CGFloat] = []
            for i in 0..<main_angle.count {
                temp_main_angle.append(main_angle[i]*Double.pi/180)
            }
            main_angle = temp_main_angle
            
            draw(CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.width))
        }
        
        override func draw(_ rect: CGRect) {
            
            if (main_color.count >= 2) {
                
                var inner_position : Int = 0
                
                // first draw with sharp corner
                for i in 0..<main_color.count {
                    
                    if (i == (main_color.count-1)) {
                        inner_position = 0
                    } else {
                        inner_position = i+1
                    }
                    
                    drawBeizer(start_angle: main_angle[i], end_angle: main_angle[inner_position], final_color: main_color[i])
                }
                
                
                // append with ending icons for circle
                for i in 0..<main_color.count {
                    
                    if (i == (main_color.count-1)) {
                        inner_position = 0
                    } else {
                        inner_position = i+1
                    }
                    
                    drawBeizer(start_angle: main_angle[inner_position], end_angle: main_angle[inner_position]+(1*Double.pi/180), final_color: main_color[i], withCircle: true) // make here final_color as .black to see what I am doing with this loop
                }
            }
        }
        
        func drawBeizer(start_angle : CGFloat, end_angle : CGFloat, final_color : UIColor, withCircle: Bool = false) {
            let path1 : UIBezierPath = UIBezierPath()
            
            path1.addArc(withCenter: CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2), radius: ((self.frame.size.width-main_view_width)/2), startAngle: start_angle, endAngle: end_angle, clockwise: true)
            path1.lineWidth = main_view_width
            if (withCircle) {
                path1.lineCapStyle = .round
            }
            final_color.setStroke()
            path1.stroke()
            
        }
    }
    

    Full code here

    enter image description here enter image description here


  2. This solution is for SwiftUI using Path.

    struct DonutElement: Shape{
        
        var width: CGFloat = 50
        var startAngle: Angle
        var endAngle: Angle
        
        func path(in rect: CGRect) -> Path {
            // define commonly used vars
            // the extend of the shape
            let outerRadius = min(rect.width, rect.height) / 2
            // the middle of the shape
            let midRadius = outerRadius - width / 2
            // the inner radius
            let innerRadius = outerRadius - width
            // centerpoint used to move coordinate system in to center
            let center = CGPoint(x: rect.width / 2, y: rect.height / 2)
            
            return Path{ path in
                // calculate the startpoint from the startAngle. startAngle is allready normalized
                let startPoint = startAngle.polarCoordinates(center: center, radius: outerRadius)
    
                // move the path without drawing to the starting position
                path.move(to: startPoint)
                
                // add the starting concav arc
                // the center point for this arc are the polar coordinates relative to midradius of the donut
                path.addArc(center: startAngle.polarCoordinates(center: center, radius: midRadius), radius: width / 2, startAngle: startAngle.normalizing(), endAngle: (.init(degrees: 180) - startAngle), clockwise: true)
                
                // add the arc that presents the inner donut line
                // center point is the center of the drawing rect with normalized angles
                path.addArc(center: center, radius: innerRadius, startAngle: startAngle.normalizing(), endAngle: endAngle.normalizing(), clockwise: true)
                
                // add the convex arc at the end of the donut element
                // switch clockwise to false and swap end and start angle
                // replace startAngle with endAngle
                path.addArc(center: endAngle.polarCoordinates(center: center, radius: midRadius), radius: width / 2, startAngle: (.init(degrees: 180) - endAngle), endAngle:  endAngle.normalizing(), clockwise: false)
                
                // add the outer stroke to close the shape
                path.addArc(center: center, radius: outerRadius, startAngle: endAngle.normalizing(), endAngle: startAngle.normalizing(), clockwise: false)
                
                // just in case
                path.closeSubpath()
            }
        }
    }
    
    extension Shape {
        func fillWithStroke<Fill: ShapeStyle, Stroke: ShapeStyle>(_ fillStyle: Fill, strokeBorder strokeStyle: Stroke, lineWidth: Double = 1) -> some View {
            self
                .stroke(strokeStyle, lineWidth: lineWidth)
                .background(self.fill(fillStyle))
        }
    }
    
    struct AngleContainer: Identifiable, Hashable{
        let id = UUID()
        var startAngle: Angle
        var endAngle: Angle
        var color: Color
    }
    
    
    
    struct Donut: View{
        // each element ends here and starts at the previous or 0. Values in percent
        var elementStops: [(CGFloat , Color)] = [(0.4 , .blue), (45 , .red), (55 , .green), (78 , .gray), (100 , .white)]
        var width: CGFloat = 50.0
        private var angles: [AngleContainer] {
            var angles = [AngleContainer]()
            
            for (index, stop) in elementStops.enumerated(){
                if index == 0{
                    let startAngle = Angle(degrees: 0)
                    let endAngle = Angle(degrees: stop.0/100 * 360)
                    
                    angles.append(AngleContainer(startAngle: startAngle,endAngle: endAngle,color: stop.1))
                } else{
                    let startAngle = Angle(degrees: elementStops[index - 1].0 / 100 * 360)
                    let endAngle = Angle(degrees: stop.0/100 * 360)
                    
                    angles.append(AngleContainer(startAngle: startAngle,endAngle: endAngle,color: stop.1))
                }
            }
            
            return angles
        }
        var body: some View{
            ZStack{
                ForEach(angles){ angleContainer in
                    DonutElement(width: width, startAngle: angleContainer.startAngle, endAngle: angleContainer.endAngle)
                        .fillWithStroke(angleContainer.color, strokeBorder: .black.opacity(0.2))
                }
            }
            .background(.white)
        }
    }
    
    // used for transfering coordinate system
    extension CGPoint{
        mutating func transfered(to center: CGPoint){
            x = center.x + x
            y = center.y - y
        }
        
        func transfering(to center: CGPoint) -> CGPoint{
            .init(x:  center.x + x, y: center.y - y)
        }
    
    }
    

    I am normalizing angles to behave more like in math with 90 degrees at the top and not at the bottom.

    // convenience so angle feels more natural
    extension Angle{
        func normalizing() -> Angle {
            Angle(degrees: 0) - self
        }
        
        func polarCoordinates(center: CGPoint, radius: CGFloat) -> CGPoint{
            CGPoint(x: cos(self.radians) * radius, y: sin(self.radians) * radius )
                .transfering(to: center)
        }
    }
    

    Result:

    enter image description here

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search