(WWDC22) Hello Swift Charts
(WWDC22)Swift Charts: Raise the bar
Swift Charts Framework
Apple이 디자인한 Chart를 만들기 위한 Framework.
SwiftUI와 동일한 선언적 문법.
Mark
차트에서 각 항목에 대한 요소를 Mark라고 함.
위 그림에서 Mark는 Bar Mark.
import Charts
import SwiftUI
struct StylesDetailsChart: View {
var body: some View {
Chart {
BarMark(
x: .value("Name", "Cachapa"),
y: .value("Sales", 900)
)
}
}
}
.value로 값을 넣으면 막대의 높이, 위치를 직접 설정하지 않고 자동으로 설정.
Swift Charts 프레임워크는 막대 뿐만 아니라 막대의 길이가 무엇을 의미하는지 보여주는 x, y축의 막대 레이블도 자동으로 생성.
데이터가 여러개일 때 Identifiable한 데이터들을 forEach를 통해 그릴 수 있음.
struct Pancakes: Identifiable {
let name: String
let sales: Int
var id: String { name }
}
let sales: [Pancakes] = [
.init(name: "Cachapa", sales: 900),
.init(name: "Injera", sales: 850),
.init(name: "Crape", sales: 802)
]
struct StylesDetailsChart: View {
var body: some View {
Chart {
ForEach(sales) { element in
BarMark(
x: .value("Name", element.name),
y: .value("Sales", element.sales)
)
}
}
}
}
forEach가 차트의 유일한 콘텐츠일 땐 Chart 이니셜라이저에 주입 가능.
Chart는 forEach처럼 동작.
struct Pancakes: Identifiable {
let name: String
let sales: Int
var id: String { name }
}
let sales: [Pancakes] = [
.init(name: "Cachapa", sales: 900),
.init(name: "Injera", sales: 850),
.init(name: "Crape", sales: 802)
]
struct StylesDetailsChart: View {
var body: some View {
Chart(sales) { element in // Chart는 ForEach처럼 동작
BarMark(
x: .value("Name", element.name),
y: .value("Sales", element.sales)
)
}
}
}
x, y데이터를 바꿔주면 그에 맞춰서 적절한 축 스타일을 선택해서 그려줌.
struct Pancakes: Identifiable {
let name: String
let sales: Int
var id: String { name }
}
let sales: [Pancakes] = [
.init(name: "Cachapa", sales: 900),
.init(name: "Injera", sales: 850),
.init(name: "Crape", sales: 802),
.init(name: "Jian Bing", sales: 753),
.init(name: "Dosa", sales: 654),
.init(name: "American", sales: 618)
]
struct StylesDetailsChart: View {
var body: some View {
Chart(sales) { element in
BarMark(
x: .value("Sales", element.sales),
y: .value("Name", element.name)
)
}
}
}
다크모드, 디스플레이 크기, 방향 지원
접근성 지원
struct StylesDetailsChart: View {
var body: some View {
Chart(sales) { element in
BarMark(
x: .value("Sales", element.sales),
y: .value("Name", element.name)
)
.accessibilityLabel(element.name)
.accessibilityValue("\(element.sales) sold")
}
}
}
2개 이상의 데이터 표시
2개 이상의 데이터를 한번에 표시하기.
범례는 foregroundStyle(by:)로 표시해줌.
struct SalesSummary: Identifiable {
let weekday: Date
let sales: Int
var id: Date { weekday }
}
struct Series: Identifiable {
let city: String
let sales: [SalesSummary]
var id: String { city }
}
let seriesData: [Series] = [
.init(city: "Cupertino", sales: cupertinoData),
.init(city: "San Francisco", sales: sanfranciscoData)
]
struct LocationsDetailsChart: View {
var body: some View {
Chart(seriesData) { series in
ForEach(series.sales) { element in
BarMark(
x: .value("Day", element.weekday, unit: .day),
y: .value("Sales", element.sales))
}
.foregroundStyle(by: .value("City", series.city))
}
}
}
위처럼 그리면 누적되어 보여줌.
각각을 나누어 그룹화하려면 .position(by:) 수정자를 사용.
struct LocationsDetailsChart: View {
var body: some View {
Chart(seriesData) { series in
ForEach(series.sales) { element in
BarMark(
x: .value("Day", element.weekday, unit: .day),
y: .value("Sales", element.sales))
}
.foregroundStyle(by: .value("City", series.city))
.symbol(by: .value("City", series.city))
.position(by: .value("City", series.city)) // 각각을 그룹화
}
}
}
PointMark, LineMark
Mark 유형을 PointMark로 변경하거나
LineMark로 변경만 하면 그에 맞게 Mark를 그려줌.
두 Mark 유형을 동시에 그려줄 수도 있음.
struct LocationsDetailsChart: View {
var body: some View {
Chart(seriesData) { series in
ForEach(series.sales) { element in
PointMark(
x: .value("Day", element.weekday, unit: .day),
y: .value("Sales", element.sales))
}
.foregroundStyle(by: .value("City", series.city))
ForEach(series.sales) { element in
LineMark(
x: .value("Day", element.weekday, unit: .day),
y: .value("Sales", element.sales))
}
.foregroundStyle(by: .value("City", series.city))
}
}
}
symbol
.symbol(by:)를 통해 PointMark의 각 데이터들을 다른 기호로 표시시켜줌.
기호가 있으면 색맹인 사람들이 더 차트를 잘 읽을 수 있음.
struct LocationsDetailsChart: View {
var body: some View {
Chart(seriesData) { series in
ForEach(series.sales) { element in
PointMark(
x: .value("Day", element.weekday, unit: .day),
y: .value("Sales", element.sales))
}
.foregroundStyle(by: .value("City", series.city))
.symbol(by: .value("City", series.city)) // 기호로 구분
ForEach(series.sales) { element in
LineMark(
x: .value("Day", element.weekday, unit: .day),
y: .value("Sales", element.sales))
}
.foregroundStyle(by: .value("City", series.city))
}
}
}
선에 점을 표시하는 것이 일반적이기 때문에, LineMark에 symbol(by:) 수정자를 적용하면 Point가 함께 그려짐.
struct LocationsDetailsChart: View {
var body: some View {
Chart(seriesData) { series in
ForEach(series.sales) { element in
LineMark(
x: .value("Day", element.weekday, unit: .day),
y: .value("Sales", element.sales))
}
.foregroundStyle(by: .value("City", series.city))
.symbol(by: .value("City", series.city))
}
}
}
보간법(interpolation)
LineMark의 경우 interpolation으로 더 매끄러운 선을 그려줄 수 있음.
struct LocationsDetailsChart: View {
var body: some View {
Chart(seriesData) { series in
ForEach(series.sales) { element in
LineMark(
x: .value("Day", element.weekday, unit: .day),
y: .value("Sales", element.sales))
}
.foregroundStyle(by: .value("City", series.city))
.symbol(by: .value("City", series.city))
.interpolationMethod(.catmullRom) // 보간법 적용
}
}
}
최소값과 최대값의 시각화
yStart, yEnd를 통해 최소값과 최대값을 시각화할 수 있음.
struct MonthlySalesChart: View {
var body: some View {
Chart(SalesData.last12Months, id: \.month) {
AreaMark(
x: .value("Month", $0.month, unit: .month),
yStart: .value("Daily Min", $0.dailyMin), // 최소값
yEnd: .value("Daily Max", $0.dailyMax) // 최대값
)
.opacity(0.3)
LineMark(x: .value("Month", $0.month, unit: .month),
y: .value("Daily Average", $0.dailyAverage))
}
}
}
위에서 사용했던 Barmark, LineMark, PointMark와
x, y, foregroundStyle, symbol 속성 외에도
area, rule, rectangle Mark와
symbolSize, lineStyle 속성들도 있음.
RectangleMark, RuleMark
위 차트에서 AreaMark를 BarMark로 LineMark를 RectangleMark로 변경하고,
RuleMark를 추가.
struct MonthlySalesChart: View {
let averageValue = 137
var body: some View {
Chart {
ForEach(SalesData.last12Months, id: \.month) {
BarMark(
x: .value("Month", $0.month, unit: .month),
yStart: .value("Daily Min", $0.dailyMin), // 최소값
yEnd: .value("Daily Max", $0.dailyMax) // 최대값
)
RectangleMark(
x: .value("Month", $0.month, unit: .month),
y: .value("Daily Average", $0.dailyAverage),
height: 2
)
}
.opacity(0.3)
RuleMark(
y: .value("Average", averageValue)
)
.lineStyle(StrokeStyle(lineWidth: 3))
}
}
}
축 값의 변경
아래와 같이 작성하면 x축의 월 표시가 모두 표시가 되지 않음.
struct MonthlySalesChart: View {
let averageValue = 137
var body: some View {
Chart {
ForEach(SalesData.last12Months, id: \.month) {
BarMark(
x: .value("Month", $0.month, unit: .month),
y: .value("Sales", $0.sales)
)
}
}
}
}
.chartXAxis 수정자로 AxisMark를 일정간격(stride)으로 표시.
struct MonthlySalesChart: View {
let averageValue = 137
var body: some View {
Chart {
ForEach(SalesData.last12Months, id: \.month) {
BarMark(
x: .value("Month", $0.month, unit: .month),
y: .value("Sales", $0.sales)
)
}
}
.chartXAxis {
AxisMarks(values: .stride(by: .month))
}
}
}
위에서는 각 항목이 너무 길어서 잘림. AxisValueLabel을 통해 format을 좁은 형식을 쓰도록 설정해줄 수 있음.
struct MonthlySalesChart: View {
let averageValue = 137
var body: some View {
Chart {
ForEach(SalesData.last12Months, id: \.month) {
BarMark(
x: .value("Month", $0.month, unit: .month),
y: .value("Sales", $0.sales)
)
}
}
.chartXAxis {
AxisMarks(values: .stride(by: .month)) { value in
AxisGridLine()
AxisTick()
AxisValueLabel(format: .dateTime.month(.narrow))
}
}
}
}
Y축의 위치를 앞쪽(.leading)으로 적용.
struct MonthlySalesChart: View {
let averageValue = 137
var body: some View {
Chart {
ForEach(SalesData.last12Months, id: \.month) {
BarMark(
x: .value("Month", $0.month, unit: .month),
y: .value("Sales", $0.sales)
)
}
}
.chartXAxis {
AxisMarks(values: .stride(by: .month)) { value in
AxisGridLine()
AxisTick()
AxisValueLabel(format: .dateTime.month(.narrow))
}
}
.chartYAxis {
AxisMarks(position: .leading)
}
}
}
경우에 따라서는 그래프의 모양, 개요만 보여주고, X, Y축의 정보는 보여주고 싶지 않을 수도 있음.
이럴 때는 hidden 처리가 가능.
struct MonthlySalesChart: View {
let averageValue = 137
var body: some View {
Chart {
ForEach(SalesData.last12Months, id: \.month) {
BarMark(
x: .value("Month", $0.month, unit: .month),
y: .value("Sales", $0.sales)
)
}
}
.chartXAxis {
AxisMarks(values: .stride(by: .month)) { value in
AxisGridLine()
AxisTick()
AxisValueLabel(format: .dateTime.month(.narrow))
}
}
.chartYAxis(.hidden) // Y축 히든 처리
.chartXAxis(.hidden) // X축 히든 처리
}
}
chartPlotStyle
플롯 영역에 정확한 크기나 종횡비를 설정하고 싶을 때는 .chartPlotStyle 사용.
.background로 색상을 주거나, .border로 테두리 설정을 할 수도 있음.
struct StylesOverviewChart: View {
let numberOfCategories = TopStyleData.last30Days.count
var body: some View {
Chart(TopStyleData.last30Days, id: \.name) { element in
BarMark(x: .value("sales", element.sales),
y: .value("name", element.name))
}
.foregroundColor(.pink)
.chartPlotStyle { plotArea in
plotArea
.frame(height: 60.0 * CGFloat(numberOfCategories))
.background(.pink.opacity(0.2))
.border(.pink, width: 1)
}
}
}
ChartProxy
차트의 x, y 스케일에 접근할 수 있는 ChartProxy를 제공.
ChartProxy의 position(for:) 메서드로 해당 데이터값의 위치를 가져오거나
value(at:) 메서드로 주어진 위치의 데이터 값을 가져올 수도 있음.
let proxy: ChartProxy
proxy.position(forX: 123.0) // 값 123.0의 X위치를 가져옴.
proxy.value(atX: 100) // X위치 100의 데이터를 가져옴.