오늘은 Pie Chart를 직접 그려보려 합니다.
File -> New File -> iOS -> Cocoa Touch Class 메뉴를 통해, 다음과 같이 PieChartView라는 Custom View를 생성합니다.
import UIKit
class PieChartView: UIView {
/*
// Only override draw() if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func draw(_ rect: CGRect) {
// Drawing code
}
*/
}
UIBezierPath를 가지고 다음과 같이 부채꼴을 그릴수가 있는데요, 이를 활용하여 Pie Chart를 만들수가 있습니다.
override func draw(_ rect: CGRect) {
UIColor.red.setStroke()
let radius = rect.width / 2 * 0.6
let cx = rect.midX
let cy = rect.midY
let arcPath = UIBezierPath()
arcPath.lineWidth = 2
arcPath.move(to: CGPoint(x: cx, y: cy))
arcPath.addArc(withCenter: CGPoint(x: cx, y: cy), // 호의 중심
radius: radius, // 원의 반지름
startAngle: 0, // 호의 시작 각도
endAngle: .pi / 2, // 호의 끝 각도
clockwise: true) // 시계 방향으로 호 그리기
arcPath.close()
arcPath.stroke()
}
이 코드는 다음과 같은 그림을 그립니다.
위 코드를 보시면 startAngle, endAngle이 있는데요, 여기에 전달되는 값은 degree단위가 아닌 radian단위입니다.
(degree는 원 한바퀴를 360도로 표현하지만, radian은 원 한바퀴를 2π로 표현하지요.)
UIBezierPath의 addArc에 전달되는 파라메터들은 다음 그림과 같은 의미를 같습니다.
기본 개념을 확인했으니, 다음과 같이 값을 전달하여 Pie를 그려보도록 하겠습니다.
(위에서 그려본 부채꼴 내부를 컬러값으로 채우기만 하면 Pie모양이 나오게 됩니다.)
barChartView.dataList = [10, 30, 15, 40, 5] // 모두 합쳐 100이 되도록 전달
private let colors = [UIColor.orange, UIColor.blue, UIColor.red, UIColor.magenta, UIColor.green]
override func draw(_ rect: CGRect) {
// 1. 각 value에 맞는 부채꼴 그리기
let radius = rect.width / 2 * 0.6 // Pie Chart가 View영역내에 그려지도록 반지름 크기 설정
let cx = rect.midX
let cy = rect.midY
var startAngle: CGFloat = 0
dataList?.enumerated().forEach({ index, value in
colors[index % colors.count].setFill() // 컬러값은 colors배열에 있는 값을 반복해서 이용
// value 100은 360도(2π)를 의미하므로, 그에 맞게 value값을 radian으로 변환
let endAngle = startAngle + .pi * 2 * CGFloat(value) / 100
let arcPath = UIBezierPath()
arcPath.move(to: CGPoint(x: cx, y: cy))
arcPath.addArc(withCenter: CGPoint(x: cx, y: cy),
radius: radius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
arcPath.close()
arcPath.fill()
startAngle = endAngle
})
}
자, 이렇게 간단한 코드로 Pie Chart를 그릴수가 있습니다.
여기에 각 영역을 좀 구분해주면 더 예쁠것 같습니다.
그래서, 다음과 같이 각 영역 경계부분에 배경색과 같은 색깔로 직선을 그려보겠습니다.
override func draw(_ rect: CGRect) {
let bgColor = UIColor.white
bgColor.setFill()
UIBezierPath(rect: rect).fill()
// 1. 각 value에 맞는 부채꼴 그리기
let radius = rect.width / 2 * 0.6
let cx = rect.midX
let cy = rect.midY
var startAngle: CGFloat = 0
dataList?.enumerated().forEach({ index, value in
colors[index % colors.count].setFill() // 컬러값은 colors배열에 있는 값을 반복해서 이용
// value 100은 360도(2π)를 의미하므로, 그에 맞게 value값을 radian으로 변환
let endAngle = startAngle + .pi * 2 * CGFloat(value) / 100
let arcPath = UIBezierPath()
arcPath.move(to: CGPoint(x: cx, y: cy))
arcPath.addArc(withCenter: CGPoint(x: cx, y: cy),
radius: radius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
arcPath.close()
arcPath.fill()
startAngle = endAngle
})
// 2. 각 부채꼴들이 살짝 떨어져 보이도록 구분선을 그려준다.
bgColor.setStroke()
startAngle = 0
var x = cx + radius
var y = cy
dataList?.forEach({ value in
let endAngle = startAngle + .pi * 2 * CGFloat(value) / 100
let angle = Double(startAngle)
// startAngle만큼 x, y를 회전시켜 부채꼴 가운데 좌표를 구한다.
let x2 = cos(angle) * Double(x - cx) - sin(angle) * Double(y - cy) + Double(cx)
let y2 = sin(angle) * Double(x - cx) + cos(angle) * Double(y - cy) + Double(cy)
let linePath = UIBezierPath()
linePath.lineWidth = 2
linePath.move(to: CGPoint(x: cx, y: cy))
linePath.addLine(to: CGPoint(x: x2, y: y2))
linePath.stroke()
startAngle = endAngle
})
}
이제, 다음과 같이 각 부채꼴이 배경색으로 잘 구분되어 보입니다.
이는 다음 그림에 설명한 "점의 회전 변환 공식"으로 startAngle에 위치에 접하는 원의 접점을 회전시켜가며, 원의 중심과 회전된 원의 접점을 잇는 직선을 배경색으로 그려준 것입니다.
자, 마지막으로 각 파이가 정확히 몇 퍼센트를 표시하는지 직접 문자열로 표시해주면 더 좋을 것 같습니다.
override func draw(_ rect: CGRect) {
......
// 3. 각 value값을 문자열로 표시해 준다.
startAngle = 0
x = cx + radius * 0.7
y = cy
let font = UIFont.systemFont(ofSize: 10)
let attrs = [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: UIColor.black]
dataList?.forEach({ value in
let endAngle = startAngle + .pi * 2 * CGFloat(value) / 100
// 각 value에 해당하는 부채꼴의 중심각도를 구한다.
let angle: Double = Double(startAngle + (endAngle - startAngle) / 2)
// 각 부채꼴의 중심각을 기준으로 x, y를 회전시켜 부채꼴 가운데 좌표를 구한다.
let x2 = cos(angle) * Double(x - cx) - sin(angle) * Double(y - cy) + Double(cx)
let y2 = sin(angle) * Double(x - cx) + cos(angle) * Double(y - cy) + Double(cy)
let strValue: String = "\(value)%"
let size = strValue.sizeWithFont(font) // 전달된 font로 문자열이 표시될수 있는 사이즈 확인.
// 문자열 영역의 중심이 (x2, y2)좌표가 되도록 그려준다.
strValue.draw(with: CGRect(x: x2 - size.width / 2, y: y2 - size.height / 2, width: size.width, height: size.height), options: .usesLineFragmentOrigin, attributes: attrs, context: nil)
startAngle = endAngle
})
}
아래와 같이 각 파이별 퍼센테이지가 문자열로 잘 표시됩니다.
이 문자열 역시 "점의 회전 변환 공식"을 이용하여 각 파이의 중심각을 찾아 원의 중심으로부터 적당히 떨어진 위치에 문자열을 그리도록 한 것입니다.
특정 font를 이용하여 문자열을 그렸을때 차지하는 영역의 크기를 확인하기 위해 다음과 같이 String extension을 추가하였습니다.
extension String {
func sizeWithFont(_ font: UIFont) -> CGSize {
let fontAttributes = [NSAttributedString.Key.font: font]
return (self as NSString).size(withAttributes: fontAttributes)
}
}
자, 최종 코드는 다음과 같습니다.
import UIKit
class PieChartView: UIView {
var dataList: [Int]? {
didSet {
setNeedsDisplay()
}
}
private let colors = [UIColor.orange, UIColor.blue, UIColor.red, UIColor.magenta, UIColor.green]
override func draw(_ rect: CGRect) {
let bgColor = UIColor.white
bgColor.setFill()
UIBezierPath(rect: rect).fill()
// 1. 각 value에 맞는 부채꼴 그리기
let radius = rect.width / 2 * 0.6
let cx = rect.midX
let cy = rect.midY
var startAngle: CGFloat = 0
dataList?.enumerated().forEach({ index, value in
colors[index % colors.count].setFill() // 컬러값은 colors배열에 있는 값을 반복해서 이용
// value 100은 360도(2π)를 의미하므로, 그에 맞게 value값을 radian으로 변환
let endAngle = startAngle + .pi * 2 * CGFloat(value) / 100
let arcPath = UIBezierPath()
arcPath.move(to: CGPoint(x: cx, y: cy))
arcPath.addArc(withCenter: CGPoint(x: cx, y: cy),
radius: radius,
startAngle: startAngle,
endAngle: endAngle,
clockwise: true)
arcPath.close()
arcPath.fill()
startAngle = endAngle
})
// 2. 각 부채꼴들이 살짝 떨어져 보이도록 구분선을 그려준다.
bgColor.setStroke()
startAngle = 0
var x = cx + radius
var y = cy
dataList?.forEach({ value in
let endAngle = startAngle + .pi * 2 * CGFloat(value) / 100
let angle = Double(startAngle)
// startAngle만큼 x, y를 회전시켜 부채꼴 가운데 좌표를 구한다.
let x2 = cos(angle) * Double(x - cx) - sin(angle) * Double(y - cy) + Double(cx)
let y2 = sin(angle) * Double(x - cx) + cos(angle) * Double(y - cy) + Double(cy)
let linePath = UIBezierPath()
linePath.lineWidth = 2
linePath.move(to: CGPoint(x: cx, y: cy))
linePath.addLine(to: CGPoint(x: x2, y: y2))
linePath.stroke()
startAngle = endAngle
})
// 3. 각 value값을 문자열로 표시해 준다.
startAngle = 0
x = cx + radius * 0.7
y = cy
let font = UIFont.systemFont(ofSize: 10)
let attrs = [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: UIColor.black]
dataList?.forEach({ value in
let endAngle = startAngle + .pi * 2 * CGFloat(value) / 100
// 각 value에 해당하는 부채꼴의 중심각도를 구한다.
let angle: Double = Double(startAngle + (endAngle - startAngle) / 2)
// 각 부채꼴의 중심각을 기준으로 x, y를 회전시켜 부채꼴 가운데 좌표를 구한다.
let x2 = cos(angle) * Double(x - cx) - sin(angle) * Double(y - cy) + Double(cx)
let y2 = sin(angle) * Double(x - cx) + cos(angle) * Double(y - cy) + Double(cy)
let strValue: String = "\(value)%"
let size = strValue.sizeWithFont(font)
// 문자열 영역의 중심이 (x2, y2)좌표가 되도록 그려준다.
strValue.draw(with: CGRect(x: x2 - size.width / 2, y: y2 - size.height / 2, width: size.width, height: size.height), options: .usesLineFragmentOrigin, attributes: attrs, context: nil)
startAngle = endAngle
})
}
}
extension String {
func sizeWithFont(_ font: UIFont) -> CGSize {
let fontAttributes = [NSAttributedString.Key.font: font]
return (self as NSString).size(withAttributes: fontAttributes)
}
}
'iOS > 차트그리기' 카테고리의 다른 글
5. iOS Radar Chart에 Animation 적용하기 (0) | 2022.07.05 |
---|---|
4. iOS Radar Chart 직접 그리기 (0) | 2022.06.17 |
2. iOS Bar Chart 직접 그리기 (0) | 2022.06.14 |
1. iOS Line chart 직접 그리기 (0) | 2022.06.14 |