음... 오늘은 Radar Chart(방사형 차트)를 그려볼까 합니다.
게임에서 캐릭터들의 능력치를 표시할때 흔히 보던 차트입니다.
File -> New File -> iOS -> Cocoa Touch Class 메뉴를 통해, 다음과 같이 RadarChartView라는 Custom View를 생성합니다.
import UIKit
class RadarChartView: 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를 가지고 오각형을 그려보겠습니다.
override func draw(_ rect: CGRect) {
let cx = rect.midX
let cy = rect.midY
let x = rect.midX
let y = CGFloat(50)
UIColor.black.setStroke()
let path = UIBezierPath()
path.lineWidth = 1
var radian = Double(0)
for _ in 0 ..< 5 {
// 중심좌표를 기준으로 점(x,y)를 radian만큼 회전시킨 점(x2, y2)을 계산한다.
let x2 = cos(radian) * Double(x - cx) - sin(radian) * Double(y - cy) + Double(cx)
let y2 = sin(radian) * Double(x - cx) + cos(radian) * Double(y - cy) + Double(cy)
let point = CGPoint(x: x2, y: y2)
if path.isEmpty {
path.move(to: point)
} else {
path.addLine(to: point)
}
radian += .pi * 2 / 5 // 360도를 5분할한 각만큼 증가
}
path.close()
path.stroke()
}
이렇게 위의 그림과 같이 오각형이 잘 그려졌는데요, 이 오각형을 구성하는 개념은 다음과 같습니다.
1. 적당한 곳에 위치한 점 P1의 좌표를 얻습니다.
2. 점 P1을 (360/5)°만큼씩 점 C를 중심으로 하여 시계방향으로 회전시켜가며 P2 ~ P5의 좌표를 얻어 해당 점들을 라인으로 이어줍니다.
3. 이렇게 얻은 path를 그려줍니다.
- path.close()를 호출한 이유는 P5와 P1사이를 잇기 위함입니다.
이런식으로 다각형을 반복해서 그려주면 맨위에 보신 거미줄 모양의 방사형 차트를 그릴수가 있습니다.
자 이제 위에서 본 코드를 활용하여 거미줄 모양을 그려보도록 하겠습니다.
enum CharacteristicType: String {
case agility = "민첩성"
case endurance = "지구력"
case strength = "근력"
case lexibility = "유연성"
case intellect = "지력"
}
class CustomUIBezierPath: UIBezierPath {
var movePoint: CGPoint?
override func move(to point: CGPoint) {
super.move(to: point)
movePoint = point
}
}
// 5개의 특성을 갖도록 한다
private let chartTypes = [CharacteristicType.agility, CharacteristicType.endurance, CharacteristicType.strength, CharacteristicType.lexibility, CharacteristicType.intellect]
override func draw(_ rect: CGRect) {
var radian: Double = 0
let step = 5 // 데이터 가이드 라인은 5단계로 표시한다
var stepLinePaths = [CustomUIBezierPath]() // 각 단계별 가이드 라인
for _ in 0 ..< step {
stepLinePaths.append(CustomUIBezierPath())
}
let heightMaxValue = rect.height / 2 * 0.7 // RadarChartView영역내에 모든 그림이 그려지도록 max value가 그려질 높이
let heightStep = heightMaxValue / CGFloat(step) // 1단계에 해당하는 높이
let cx = rect.midX
let cy = rect.midY
let x = rect.midX
let y = rect.midY - heightMaxValue
chartTypes.forEach { type in
// 차트 중심으로부터 각 특성 다각형 꼭지점까지 잇는 직선 그리기
UIColor.lightGray.setStroke()
let path = UIBezierPath()
path.lineWidth = 1
path.move(to: CGPoint(x: cx, y: cy))
path.addLine(to: transformRotate(radian: radian, x: x, y: y, cx: cx, cy: cy))
path.stroke()
// 단계별 가이드 라인 path 설정
stepLinePaths.enumerated().forEach { index, path in
let point = transformRotate(radian: radian, x: x, y: y + heightStep * CGFloat(index), cx: cx, cy: cy)
if path.isEmpty {
path.move(to: point)
} else {
path.addLine(to: point)
}
}
radian += .pi * 2 / Double(chartTypes.count)
}
stepLinePaths.enumerated().forEach { index, path in
// 단계별 가이드 라인 그리기
UIColor.lightGray.setStroke()
path.close()
path.stroke()
}
}
// 점(x, y)를 특정 좌표(cx, cy)를 중심으로 radian만큼 회전시킨 점의 좌표를 반환
private func transformRotate(radian: Double, x: CGFloat, y: CGFloat, cx: CGFloat, cy: CGFloat) -> CGPoint {
let x2 = cos(radian) * Double(x - cx) - sin(radian) * Double(y - cy) + Double(cx)
let y2 = sin(radian) * Double(x - cx) + cos(radian) * Double(y - cy) + Double(cy)
return CGPoint(x: x2, y: y2)
}
거미줄 모양이 예쁘게 잘 표시되고 있네요.
특정 캐릭터의 능력치가 0 ~ 100 까지의 값을 갖는다고 할때, 이를 5분할하여 가이드라인을 표시하도록 하였습니다.
P1이 위치하는 5각형은 100점을 의미하며, 가장 가운데 작은 5각형은 20점을 의미하게 됩니다.
그래서 P1의 y값을 heightStep씩 더해가며 각 5각형의 최초 꼭지점을 구한후, 이 꼭지점들을 시계방향으로 회전시켜가며 각 꼭지점들의 좌표를 구해 이은 것입니다.
이어서, 각 단계별 값이 어떻게 되는지와 각 꼭지점이 어떤 특성을 나타내는지 알수 있도록 문자열 정보를 추가로 그려봅니다.
override func draw(_ rect: CGRect) {
let font = UIFont.systemFont(ofSize: 8)
let attrs = [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: UIColor.black]
......
chartTypes.forEach { type in
......
// 각 꼭지점 부근에 각 특성 문자열 표시
let point = transformRotate(radian: radian, x: x, y: y * 0.7, cx: cx, cy: cy)
let strValue = type.rawValue
let size = strValue.sizeWithFont(font)
strValue.draw(with: CGRect(x: point.x - size.width / 2, y: point.y - size.height / 2, width: size.width, height: size.height),
options: .usesLineFragmentOrigin,
attributes: attrs,
context: nil)
......
radian += .pi * 2 / Double(chartTypes.count)
}
stepLinePaths.enumerated().forEach { index, path in
......
// 각 단계별 기준값 표시
if let movePoint = path.movePoint {
let strValue = "\(100 - 20 * index)"
let size = strValue.sizeWithFont(font)
strValue.draw(with: CGRect(x: movePoint.x + 3, y: movePoint.y - size.height / 2, width: size.width, height: size.height),
options: .usesLineFragmentOrigin,
attributes: attrs,
context: nil)
}
}
}
이렇게, 각 단계별 점수가 어떻게 되는지와 꼭지점이 어떤 특성을 의미하는지 쉽게 알수 있게 되었습니다.
stepLinePaths변수타입이 CustomUIBezierPath클래스 배열로 정의되어 있는것을 확인할수 있는데요, 이는 UIBezierPath.move(to:)를 통해 각 단계별 5각형의 최초 꼭지점 좌표를 저장하기 위함이었습니다.
그래서 이렇게 저장했던 좌표들의 우측에 각 단계별 점수를 표시한 것입니다.
그리고, 꼭지점 부근에 특성 문자열을 그리는 원리도 간단합니다.
1. 위에서 본 P1보다 약간 위에 위치하는 점의 좌표(x, y * 0.7)를 구한후, 이 점을 시계방향으로 회전시켜가며 각 꼭지점 부근에 위치한 점의 좌표들을 구합니다.
2. 각 특성 문자열이 그려질때 차지하게 될 size를 구합니다.
3. 이후 1번에서 얻은 각 점의 좌표를 중심으로 하는 사각형 영역에 문자열을 그려줍니다.
마지막으로, 아래와 같이 실제 캐릭터의 각 능력치를 전달하여 차트에 표시해 보는 일만 남았습니다.
radarChartView.dataList = [RadarChartData(type: .agility, value: 60),
RadarChartData(type: .endurance, value: 70),
RadarChartData(type: .strength, value: 50),
RadarChartData(type: .lexibility, value: 90),
RadarChartData(type: .intellect, value: 85)
struct RadarChartData {
let type: CharacteristicType
let value: Int
}
override func draw(_ rect: CGRect) {
......
let valuePath = UIBezierPath() // 각 특성값을 이어 구성할 다각형 path
chartTypes.forEach { type in
......
// 각 특성별 값에 해당하는 좌표를 구해 다각형 path 구성
if let value = dataList?.first(where: { $0.type == type })?.value {
let convValue = heightMaxValue * (CGFloat(value) / 100) // 전달된 값을 차트크기에 맞게 변환
let point = transformRotate(radian: radian, x: x, y: rect.midY - convValue, cx: cx, cy: cy)
if valuePath.isEmpty {
valuePath.move(to: point)
} else {
valuePath.addLine(to: point)
}
}
radian += .pi * 2 / Double(chartTypes.count)
}
......
// 각 특성별 값을 다각형으로 표시
UIColor(red: 1, green: 0, blue: 0, alpha: 0.5).setFill()
valuePath.close()
valuePath.fill()
}
능력치에 해당하는 다각형을 그리는 개념은 다음과 같습니다.
1. C·P1을 지나는 직선위에 있도록 각 특성별 능력치의 y좌표를 먼저 계산합니다.
- convValue가 heightMaxValue를 기준으로 환산된 능력치 값을 나타내는 값입니다.
- 이를 좌표계로 바꾸려면 점 C의 y좌표값(rect.midY)에서 convValue를 빼주면 되죠.
2. 이렇게 계산된 각 특성별 능력치 좌표를 각 특성에 맞는 각도로 회전시켜 다각형을 구성합니다.
3. 이제 2번에서 구성된 path를 그려줍니다.
다음은 이를 그리기 위한 전체 코드입니다.
import UIKit
enum CharacteristicType: String {
case agility = "민첩성"
case endurance = "지구력"
case strength = "근력"
case lexibility = "유연성"
case intellect = "지력"
}
struct RadarChartData {
let type: CharacteristicType
let value: Int
}
class CustomUIBezierPath: UIBezierPath {
var movePoint: CGPoint?
override func move(to point: CGPoint) {
super.move(to: point)
movePoint = point
}
}
class RadarChartView: UIView {
var dataList: [RadarChartData]? {
didSet {
setNeedsDisplay()
}
}
private let chartTypes = [CharacteristicType.agility, CharacteristicType.endurance, CharacteristicType.strength, CharacteristicType.lexibility, CharacteristicType.intellect]
override func draw(_ rect: CGRect) {
let font = UIFont.systemFont(ofSize: 8)
let attrs = [NSAttributedString.Key.font: font, NSAttributedString.Key.foregroundColor: UIColor.black]
var radian: Double = 0
let step = 5 // 데이터 가이드 라인은 5단계로 표시한다
var stepLinePaths = [CustomUIBezierPath]() // 각 단계별 가이드 라인
for _ in 0 ..< step {
stepLinePaths.append(CustomUIBezierPath())
}
let heightMaxValue = rect.height / 2 * 0.7 // RadarChartView영역내에 모든 그림이 그려지도록 max value가 그려질 높이
let heightStep = heightMaxValue / CGFloat(step) // 1단계에 해당하는 높이
let cx = rect.midX
let cy = rect.midY
let x = rect.midX
let y = rect.midY - heightMaxValue
let valuePath = UIBezierPath() // 각 특성값을 이어 구성할 다각형 path
chartTypes.forEach { type in
// 1. 차트 중심으로부터 각 특성 다각형 꼭지점까지 잇는 직선 그리기
UIColor.lightGray.setStroke()
let path = UIBezierPath()
path.lineWidth = 1
path.move(to: CGPoint(x: cx, y: cy))
path.addLine(to: transformRotate(radian: radian, x: x, y: y, cx: cx, cy: cy))
path.stroke()
// 2. 각 꼭지점 부근에 각 특성 문자열 표시
let point = transformRotate(radian: radian, x: x, y: y * 0.7, cx: cx, cy: cy)
let strValue = type.rawValue
let size = strValue.sizeWithFont(font)
strValue.draw(with: CGRect(x: point.x - size.width / 2, y: point.y - size.height / 2, width: size.width, height: size.height),
options: .usesLineFragmentOrigin,
attributes: attrs,
context: nil)
// 3. 단계별 가이드 라인 path 설정
stepLinePaths.enumerated().forEach { index, path in
let point = transformRotate(radian: radian, x: x, y: y + heightStep * CGFloat(index), cx: cx, cy: cy)
if path.isEmpty {
path.move(to: point)
} else {
path.addLine(to: point)
}
}
// 4. 각 특성별 값에 해당하는 좌표를 구해 다각형 path 구성
if let value = dataList?.first(where: { $0.type == type })?.value {
let convValue = heightMaxValue * (CGFloat(value) / 100) // 전달된 값을 차트크기에 맞게 변환
let point = transformRotate(radian: radian, x: x, y: rect.midY - convValue, cx: cx, cy: cy)
if valuePath.isEmpty {
valuePath.move(to: point)
} else {
valuePath.addLine(to: point)
}
}
radian += .pi * 2 / Double(chartTypes.count)
}
stepLinePaths.enumerated().forEach { index, path in
// 5. 단계별 가이드 라인 그리기
UIColor.lightGray.setStroke()
path.close()
path.stroke()
// 6. 각 단계별 기준값 표시
if let movePoint = path.movePoint {
let strValue = "\(100 - 20 * index)"
let size = strValue.sizeWithFont(font)
strValue.draw(with: CGRect(x: movePoint.x + 3, y: movePoint.y - size.height / 2, width: size.width, height: size.height),
options: .usesLineFragmentOrigin,
attributes: attrs,
context: nil)
}
}
// 7. 각 특성별 값을 다각형으로 표시
UIColor(red: 1, green: 0, blue: 0, alpha: 0.5).setFill()
valuePath.close()
valuePath.fill()
}
// 점(x, y)를 특정 좌표(cx, cy)를 중심으로 radian만큼 회전시킨 점의 좌표를 반환
private func transformRotate(radian: Double, x: CGFloat, y: CGFloat, cx: CGFloat, cy: CGFloat) -> CGPoint {
let x2 = cos(radian) * Double(x - cx) - sin(radian) * Double(y - cy) + Double(cx)
let y2 = sin(radian) * Double(x - cx) + cos(radian) * Double(y - cy) + Double(cy)
return CGPoint(x: x2, y: y2)
}
}
'iOS > 차트그리기' 카테고리의 다른 글
5. iOS Radar Chart에 Animation 적용하기 (0) | 2022.07.05 |
---|---|
3. iOS Pie Chart 직접 그리기 (0) | 2022.06.15 |
2. iOS Bar Chart 직접 그리기 (0) | 2022.06.14 |
1. iOS Line chart 직접 그리기 (0) | 2022.06.14 |