Moinuddin Girach
Moinuddin Girach

Reputation: 223

doughnut chart with rounded

I want to create doughnut chart with overlapping one side with rounded corner. like

enter image description here

requirement is starting end should be bellow previous line and ending end should above next line. I tired to do it but last line layer show both on top. I tried with bellow code

private func drawCircle(){
        var startAngle: CGFloat = .pi / -2
        var endAngle: CGFloat = 0.0
        
        drawableItems.forEach { (item) in
            print(item.title)
            print(startAngle)
            let center = calculateCenter()
            endAngle = startAngle + calculateAngles(item: item)
            if endAngle > 2 * CGFloat.pi {
                endAngle -= 2 * .pi
            }
            
            // Radius of the view
            let radius: CGFloat = calculateRadius()
            let arcWidth: CGFloat = self.arcWidth
            let circlePath = UIBezierPath(arcCenter: center,
                                          radius: radius/2 - arcWidth/2,
                                          startAngle: startAngle,
                                          endAngle: endAngle,
                                          clockwise: true)
            
            circlePath.lineCapStyle = .round
            
            let shapeLayer: CAShapeLayer = CAShapeLayer()
            shapeLayer.path = circlePath.cgPath
            shapeLayer.strokeColor = item.color.cgColor
            shapeLayer.lineWidth = arcWidth
            shapeLayer.fillColor = UIColor.clear.cgColor
            shapeLayer.lineCap = .round
            layer.addSublayer(shapeLayer)
            startAngle = endAngle
        }
    }
    

output gives as bellow. in which last layer with green colour show both end top instead of one bellow previous and other on top of next

enter image description here

please help me to do so.

Upvotes: 0

Views: 484

Answers (1)

HangarRash
HangarRash

Reputation: 15056

The issue is that your are drawing a simple arc with a thick line and rounded ends. The two rounded ends of the last arc is always going to cover the previous and first arcs.

The solution is to draw arcs that have a concave end that doesn't cover the convex end of the previous arc.

The following UIBezierPath extension will draw such an arc:

extension UIBezierPath {
    convenience init(
        chartArcCenter center: CGPoint,
        radius: CGFloat,
        lineWidth: CGFloat,
        startAngle: CGFloat,
        endAngle: CGFloat,
        clockwise: Bool
    ) {
        self.init()
        // Draw inner radius arc
        addArc(withCenter: center, radius: radius - lineWidth/2, startAngle: startAngle, endAngle: endAngle, clockwise: clockwise)
        // Draw convex endcap
        var x = cos(endAngle) * radius + center.x
        var y = sin(endAngle) * radius + center.y
        addArc(withCenter: CGPoint(x: x, y: y), radius: lineWidth / 2, startAngle: endAngle - .pi, endAngle: endAngle, clockwise: !clockwise)
        // Draw outer radius arc
        addArc(withCenter: center, radius: radius + lineWidth/2, startAngle: endAngle, endAngle: startAngle, clockwise: !clockwise)
        // Draw concave startcap
        x = cos(startAngle) * radius + center.x
        y = sin(startAngle) * radius + center.y
        addArc(withCenter: CGPoint(x: x, y: y), radius: lineWidth / 2, startAngle: startAngle, endAngle: startAngle + .pi, clockwise: clockwise)
        close()
    }
}

Use this in your code instead of the current UIBezierPath call. You can also make changes to the drawing settings of the shape layer.

With all changes in place your method becomes:

private func drawCircle(){
    var startAngle: CGFloat = .pi / -2
    var endAngle: CGFloat = 0.0
        
    // Radius of the view
    let radius: CGFloat = calculateRadius()
    let arcWidth: CGFloat = self.arcWidth
    let center = calculateCenter()

    drawableItems.forEach { (item) in
        endAngle = startAngle + calculateAngles(item: item)
            
        let circlePath = UIBezierPath(chartArcCenter: center,
                                      radius: radius/2 - arcWidth/2,
                                      lineWidth: arcWidth,
                                      startAngle: startAngle,
                                      endAngle: endAngle,
                                      clockwise: true)
           
        let shapeLayer: CAShapeLayer = CAShapeLayer()
        shapeLayer.path = circlePath.cgPath
        shapeLayer.fillColor = item.color.cgColor
        layer.addSublayer(shapeLayer)
        startAngle = endAngle
    }
}

Also note that there's no reason for the if endAngle > 2 * CGFloat.pi { block. It's OK if the angle "wraps". And the calculations of radius, width and center only need to be done once so no need for those inside the loop.

Upvotes: 1

Related Questions