Draw text inside pie chart section created using CoreGraphics

161 views Asked by At

I created a simple pie chart on Swift that looks like this: Current state

But now I need to add text inside each pie section like this: Expected result

and I'm not sure how to accomplish that.

This is the code I used to create the graph

 override func draw(_ rect: CGRect) {

        let ctx = UIGraphicsGetCurrentContext()
        let radius = min(frame.size.width, frame.size.height) * 0.5
        let viewCenter = CGPoint(x: bounds.size.width * 0.5, y: bounds.size.height * 0.5)
        let valueCount = segments.reduce(0, {$0 + $1.value})
        var startAngle = -CGFloat.pi * 0.5

        for segment in segments { 
            ctx?.setFillColor(segment.color.cgColor)
            let endAngle = startAngle + 2 * .pi * (segment.value / valueCount)
            ctx?.move(to: viewCenter)
            ctx?.addArc(center: viewCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
            ctx?.fillPath()
            startAngle = endAngle
        }
    }

I created labels for each section, but I'm not sure how to calculate the frame position or if there's a property in the context that could help me to place them inside each section like in the image above. I tried using the startAngle/endAngle as a starting point but they just stack one on top of another.

1

There are 1 answers

2
HangarRash On BEST ANSWER

The trick is to first calculate the center of where each segment's label should be positioned. A little trigonometry gives you:

let midAngle = (startAngle + endAngle) / 2
let textCenter = CGPoint(x: cos(midAngle) * radius * 0.75 + viewCenter.x, y: sin(midAngle) * radius * 0.75 + viewCenter.y)

where midAngle is the centerline of each segment (halfway between the start and end angles). The 0.75 is the percentage of the radius you want the text to be from the center of the circle. Adjust as desired.

Then you need to figure out the bounding box of the text to be drawn for a given segment and then adjust that based on the calculated center.

Finally you can draw the text.

Here is a version of your code that can be run in an iOS Swift Playground. I guessed at the Segment struct based on your code. What I show below is enough to make it run.

import UIKit
import PlaygroundSupport

struct Segment {
    let value: CGFloat
    let color: UIColor
}

class GraphView: UIView {
    var segments: [Segment] = [
        Segment(value: 40, color: .green),
        Segment(value: 20, color: .yellow),
        Segment(value: 50, color: .blue),
        Segment(value: 70, color: .orange),
        Segment(value: 120, color: .red),
    ]

    override func draw(_ rect: CGRect) {
        let ctx = UIGraphicsGetCurrentContext()
        let radius = min(frame.size.width, frame.size.height) * 0.5
        let viewCenter = CGPoint(x: bounds.size.width * 0.5, y: bounds.size.height * 0.5)
        let valueCount = segments.reduce(0, {$0 + $1.value})
        var startAngle = -CGFloat.pi * 0.5

        // Attributes for the labels. Adjust as desired
        let textAttrs: [NSAttributedString.Key : Any] = [
            .font: UIFont.preferredFont(forTextStyle: .headline),
            .foregroundColor: UIColor.black,
        ]

        for segment in segments {
            ctx?.setFillColor(segment.color.cgColor)
            let endAngle = startAngle + 2 * .pi * (segment.value / valueCount)
            ctx?.move(to: viewCenter)
            ctx?.addArc(center: viewCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false)
            ctx?.fillPath()

            // Calculate center location of label
            // Replace the 0.75 with a desired distance from the center
            let midAngle = (startAngle + endAngle) / 2
            let textCenter = CGPoint(x: cos(midAngle) * radius * 0.75 + viewCenter.x, y: sin(midAngle) * radius * 0.75 + viewCenter.y)

            // The text label (adjust as needed)
            let label = "\(segment.value)"

            // Calculate the bounding box and adjust for the center location
            var rect = label.boundingRect(with: CGSize(width: 1000, height: 1000), attributes: textAttrs, context: nil)
            rect.origin.x = textCenter.x - rect.size.width / 2
            rect.origin.y = textCenter.y - rect.size.height / 2

            label.draw(in: rect, withAttributes: textAttrs)

            startAngle = endAngle
        }
    }
}

let graph = GraphView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))

This gives you the following:

enter image description here

Some of the text methods (boundingRect and draw) come from NSString which are convenient methods for drawing into a CGContext. Easier than the text drawing methods found in CGContext.