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.
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:
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
.