iOS 17 กับการมาของ Charts Framwork ที่ไม่ต้องใช้ผ่าน Cocoapods อีกต่อไป
อย่างที่ทุกคนรู้กันว่า Charts นั้นสำคัญสำหรับงาน Data visualization ไม่ว่าจะเป็น Bar chart, Pie chart หรือ Donut chart
ซึ่งชาว iOS Developer จะสร้าง Charts แต่จะทีก็ต้องคอยมา Custom Charts เขียนกันเองอย่างการใช้ Path เข้ามาช่วย หรือการใช้ Third Party อย่าง Cocoapods — Charts นี่แหละ แต่ภายในงาน WWDC 2023 ได้มีการปล่อย Pie Chart ออกมาอย่างเป็นทางการให้ชาว iOS เรานำไปเล่นกันอย่างง่าย ๆ
ในบทความนี้ เราจะมาลองเล่น Pie Chart และ Donut Chart กัน โดยใช้ SwiftUI และมาเรียนรู้เกี่ยวกับ Interactivity ใน Swift Charts ด้วยเช่นกัน
Prepare data
ก่อนอื่นเรามาเตรียม Data อย่างง่าย ๆ สำหรับ Charts framework กันก่อน โดยการ Create new project ขึ้นมาจากนั้นเพิ่มโค้ดส่วนนี้ลงไป
struct Donut {
let id = UUID().uuidString
let name: String
let count: Int
}
struct ContentView: View {
private var donuts: [Donut] = [
Donut(name: "HONEY DIPPED", count: 12),
Donut(name: "PEANUT", count: 30),
Donut(name: "TRIO", count: 29),
Donut(name: "BLUEBERRY SHELL", count: 7),
Donut(name: "PON DE RING", count: 82),
Donut(name: "TWIST", count: 54)
]
var body: some View { ... }
}
Pie Charts
ในการสร้าง Pie Chart ต้องใช้คำสั่ง SectorMark to represent Pie Chart UI
var body: some View {
NavigationStack {
VStack {
Chart {
ForEach(donuts, id: \.name) { donut in
SectorMark(
angle: .value("Donut", donut.count)
)
.foregroundStyle(by: .value("Type", donut.name))
}
}
.frame(height: 500)
}
.padding()
.navigationTitle("DONUTS")
}
}
ตัว angle
parameter ของ SectorMark
จะถูกคำนวณและนำมาวาดเป็น pie chartโดยใช้ค่าจาก donut.count
Customizing the Pie Chart
เส้นกั้นระหว่าง Sector
SectorMark
มีจำนวน parameters ที่สามารถ custom ของแต่ละ sector ได้ โดยการเพิ่มช่องว่างระหว่าง sectors โดยการใช้ angularInset
SectorMark(
angle: .value("Donut", donut.count),
angularInset: 4
)
กำหนดขนาด Sector ที่ต้องการ
เราสามารถเพิ่มขนาดของแต่ละ sectors โดยใช้ outerRadius
parameter.
ตัวอย่างเช่น ถ้าเราต้องไฮไลท์ Trio Donut Sector ให้มีขนาดที่ใหญ่ขึ้น สามารถทำได้โดยการใส่
outerRadius
parameter.
SectorMark(
angle: .value("Donut", donut.count),
outerRadius: donut.name == "TRIO" ? 180 : 150,
angularInset: 4
)
ใส่ Label แต่ให้แต่ละ Sector
การเพิ่ม Label ให้แต่ละ sector เราสามารถใช้ annotation
modifier ให้ SectorMark
โดยการ set position
เป็น .overlay
SectorMark(
angle: .value("Donut", donut.count),
outerRadius: donut.name == "TRIO" ? 180 : 150,
angularInset: 4
)
.foregroundStyle(by: .value("Name", donut.name))
.annotation(position: .overlay) {
Text("\(donut.count)")
.font(.title)
.foregroundStyle(.white)
}
Donut Chart
จากด้านบนเราทำการสร้าง Pie Chart กันเสร็จเรียบร้อยแล้ว เราสามารถเปลี่ยนจาก Pie Chart เป็น Donut Chart ได้อย่างง่าย ๆ โดยการใช้ innerRadius
parameter ของ SectorMark
เพียง 1 บรรทัดก็จะเปลี่ยนจาก Pie Chart เป็น Donut Chart ได้ทันที
SectorMark(
angle: .value("Donut", donut.count),
innerRadius: .ratio(0.6),
outerRadius: donut.name == "TRIO" ? 180 : 150,
angularInset: 4
)
.foregroundStyle(by: .value("Name", donut.name))
.annotation(position: .overlay) {
Text("\(donut.count)")
.font(.title)
.foregroundStyle(.white)
}
หรือเราสามารถใช้ cornerRadius
modifier ให้ Sector เพื่อ round the corners ได้
SectorMark(
angle: .value("Donut", donut.count),
innerRadius: .ratio(0.6),
outerRadius: donut.name == "TRIO" ? 180 : 150,
angularInset: 4
)
.foregroundStyle(by: .value("Name", donut.name))
.cornerRadius(10)
.annotation(position: .overlay) {
Text("\(donut.count)")
.font(.title)
.foregroundStyle(.white)
}
สุดท้ายเราสามารถเพิ่ม Chart Background ได้โดยการใช้ chartBackground
modifier ให้ Chart
Chart {
...
}
.frame(height: 500)
.chartBackground(content: { _ in
Image(systemName: "moon.dust")
.resizable()
.frame(width: 100, height: 100)
})
Interacting with Charts
การเพิ่ม Interaction ให้ทั้ง Pie Chart และ Donut Chart ใน new swiftUI version โดยการใช้ chartAngleSelection
modifier ให้ Chart เป็น binding เมื่อ User กดไปที่ Sector และใช้ onChange
modifier เพิ่มจับค่าของ chart angle value
@State private var selectedCount: Int?
...
Chart {
...
}
.frame(height: 500)
.chartAngleSelection(value: $selectedCount)
.onChange(of: selectedCount) { (oldValue, newValue) in
if let newValue {
print(newValue)
}
}
สังเกต Donut Chart นี้ใช้ค่าของ donut.count มา Plot เป็น Chart ดังนั้นเมื่อกดที่ Sector จะทำการ print newValue ออกมาซึ่งค่าของจุดแรกเริ่ม หรือค่าที่ value = 0 จะอยู่ที่ 00:00 นาฬิกา แล้ววนตามเข็มนาฬิกาไปจนถึง 11:59 ก็คือสุดสิ้นสุด หรือค่าที่ value = ค่าของ donut.count ทั้งหมดมาบวกกัน (value = 214)
ในการจะหาค่า chart angle value ว่าเป็นของ Sector ไหนนั้นเราจำเป็นต้องเขียน function เพื่อคำนวณหาค่าเอง
private func findSectorSelected(by value: Int) -> String? {
var sum = 0
let donut = donuts.first() { donut in
sum += donut.count
return value <= sum
}
return donut?.name
}
เมื่อได้ function ในการหา Sector name แล้วต่อไปก็ประกาศตัวแปรเพื่อรับค่าที่เราคลิก Sector นั้น ๆ
@State private var selectedSector: String?
เมื่อ Sector ถูก hold ตัว selectedSector ก็จะถูก Highlight ตามที่เราเลือก
.onChange(of: selectedCount) { (_, newValue) in
if let newValue {
selectedSector = findSectorSelected(by: newValue)
} else {
selectedSector = nil
}
}
ส่วน Sector ที่เราไม่ได้เลือกก็จะถูก fade ให้สีจางลงนั่นเอง
SectorMark(
...
)
.opacity(selectedSector == nil ? 1.0 : (selectedSector == donut.name ? 1.0 : 0.3))
เย้! จบแล้วสำหรับบทความ Chart Framwork หวังว่าทุกท่านจะสนุกกับการเรียนรู้เรื่อง Chart กันนะครับ
ถ้าหากผิดพลาดประการใดหรือผมเขียนผิดยังไงต้องอภัยล่วงหน้าด้วยนะครับ ถ้าผู้อ่านหากมีคำแนะนำอย่างใดสามารถ Comment ไว้ได้เลย
“ แล้วพบกันใหม่ในบทความต่อไปเด้อครับโผ้ม “