스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Swift Charts에서 파이 그래프와 상호 교환성 탐색하기
Swift Charts가 기본으로 돌아갔습니다. 프레임워크의 최신 개선 기능으로 앱에서 파이 그래프와 도넛 그래프를 만들어 보세요. 그래프에서 스크롤 사용을 가능하게 만들 수 있는 방법을 알아보고, 데이터의 추가적인 세부 사항을 드러낼 수 있도록 그래프 선택 API를 탐색하며, 상호 교환성의 증가가 어떻게 여러분의 그래프를 더 멋지게 만들어주는지 확인해 보세요.
챕터
- 0:20 - Pie charts
- 4:22 - Selection
- 7:49 - Scrolling
리소스
관련 비디오
WWDC23
-
다운로드
♪ ♪
안녕하세요 저는 Richard입니다 오늘 여러분께 Swift Charts의 새 기능을 알려드릴 건데요 파이 그래프와 선택 스크롤링을 알려드릴 겁니다 우선 파이 그래프부터 시작해 보겠습니다 Swift Charts는 다양한 데이터 시각화를 위해 구성과 맞춤화가 가능한 빌딩 블록을 제공하는데요 오늘은 새롭게 추가된 파이 그래프를 알아볼 겁니다 파이 그래프는 총 값이 다양한 분류로 이루어져 있음을 간단하고 친숙한 모양으로 알려줍니다 이건 제 친구들의 푸드 트럭에서 팔린 팬케이크 판매량 데이터를 시각화한 그래프인데요 파이 그래프에는 축이 없고 정확한 숫자가 필요없는 약식 환경 표현에 아주 좋습니다 각 조각들이 어떻게 원이 되는지 이해해 보자면 이들은 일부 대 전체 관계를 시각화하기에 딱 좋죠 사람들이 파이 그래프를 좋아하는 이유 중 하나는 둥글고 접근하기 쉬운 모양 때문인데요 마크 기반 구성 문법을 사용해 만들 수 있습니다 이미 친숙하시겠지만요 새로운 마크 유형 SectorMark를 소개할게요 이건 파이 그래프의 슬라이스를 나타내죠 이건 극 공간에 배치됩니다 이 극이 아니라 '극좌표계'를 말하죠 부채꼴 크기는 그것이 나타내는 값에 비례합니다 반지름을 따라 부채꼴 모양을 맞춤화할 수 있죠 안쪽 반지름을 늘리면 도넛 그래프가 됩니다 SectorMark를 통해 쉽게 이들을 그릴 수 있는데요 예시를 보여드릴게요 제 친구들의 인터내셔널 팬케이크 푸드 트럭 사업입니다 작년 일일 판매량이 아주 크고 활발하게 성장했죠 판매 팬케이크는 6종이었고요 저는 이번 여름 이 판매 앱을 개선해달라는 요청을 받았습니다
그래서 최고 판매 메뉴를 시각화하는 그래프로 시작했죠 그래서 현재 앱에 단순한 중첩막대그래프가 있는 겁니다 BarMark를 사용해 판매량이 X 차원을 따라 중첩되죠 범주 데이터이기 때문에 범주는 각 중첩막대의 전경 스타일로 반영됩니다 이 그래프가 효과는 있지만 이제 파이 그래프로 넘어가서 스크린에서 이용 가능한 공간을 통해 데이터를 두드러지게 만들어 봅시다 여기서 BarMark와 x 인자를 SectorMark와 angle로 바꾸기만 하면 됩니다 아주 간단하죠!
SectorMark로 각도를 사용해 판매량을 나타냈습니다 파이 그래프에 제공하는 각도 값은 원에 맞게 자동으로 정규화되는데요 스타일 맞춤화도 적용할 수 있습니다 부채꼴 사이 간격을 만들려면 angularInset을 설정하면 되는데요 부채꼴 삽입 각도 너비를 1.5포인트로 설정하겠습니다 두 부채꼴 사이의 삽입 너비는 두 배가 되어 3포인트가 되죠 코너 반지름을 둥글고 예쁘게 설정할 수도 있는데요 코드 몇 줄 만으로도 이미 근사해 보이네요 이제 이걸 도넛 그래프로 만들어 보죠
안쪽 반지름을 총 반지름의 비율로 설정하겠습니다 제겐 황금 비율로 딱 맞아 보이는데 여러분의 도넛은 다른 모양일 수 있죠 다 팔린 팬케이크 중 카차파가 1등입니다 차트 위에 표시되어 있죠 하지만 도넛 그래프 중간이 비어 있으니 저 텍스트를 그래프 중앙으로 옮겨 보려고 합니다 chartBackground에 저 텍스트를 넣을게요 텍스트가 도넛 구멍의 중심에 올 수 있도록 위치 계산도 했습니다
이제 실용적인 도넛 그래프가 됐네요 여기까지 파이 그래프와 도넛 그래프였습니다 데이터를 강조에 아주 좋은 방법이죠 큰 스크린에서도 엄청 멋있고요 이제 그래프 상호 교환성 특징을 알아보겠습니다 '선택'부터 시작하죠 그래프에서 상호 교환성을 가능하게 함으로써 추가적인 세부 내용을 계속해서 드러낼 수 있습니다 상호 교환성을 통해 터치 등 다양한 인풋 형태로 보는 사람이 데이터를 자연스레 탐색하도록 할 수 있죠
선택은 그래프와 소통하는 직접적인 방식으로 심박동수 그래프처럼 Apple 디자인 그래프가 딱 완벽한 예시입니다 한 축을 따라 어느 한 점을 선택하면 그래프는 추가 정보를 드러낼 거고요 이 아이디어를 팬케이크 판매 앱에 가져와 보죠
앱에서 한 그래프가 두 도시에서 일주일간 평균 일일 판매량을 시각화한 겁니다
이 차트에서는 값 선정시 판매 수치를 드러낼 수 있죠 팝업창에서 선택한 날에 팔린 팬케이크 양을 보여줍니다
이렇게 그래프가 정의되는데요 각 도시 데이터 시리즈가 있죠 시리즈의 각 요소는 일주일 중 하루와 판매량입니다 선 스타일은 도시명에 따라 변하죠 chartOverlay 제어자는 이미 친숙하실 텐데요 SwiftUI를 겹치게 해 제스처를 포착하죠 하지만 iOS 17에서는 chartXSelection을 사용할 겁니다 이건 제 제스처에서 인식한 모든 것을 처리하고 선정된 값을 하나의 바인딩에 저장합니다
선택 제어자는 X축을 따라 원 데이터값을 주기 때문에 계산된 프로퍼티를 선 그래프의 데이터 포인트에 맞출 수 있죠 값 선택 시 팝업창 띄우기까지 한번 확장해 볼까요?
하나의 값이 선택됐을 때 수직자가 표시되도록 선택 지표로 추가했습니다 Z 지수를 기본값 0보다 낮게 설정해서 이 표시가 선 표시보다 뒤에 있도록 했죠 이제 선택 지표 위에 팝업창을 만들겠습니다 커스텀 SwiftUI 뷰를 사용해 주석으로 할 수 있는데요 주석은 대개 그래프 내에 배치됩니다 하지만 이 경우 팝업창이 그래프 경계를 벗어나죠 여기서 주석에 대한 오버플로우 해상도가 필요합니다
팝업창을 그래프의 X축에 맞추고 싶은데요 팝업창이 그래프의 수평 경계를 나가지 않도록 하기 위해서요 Y축에서는 오버플로우 해상도를 비활성화해서 주석이 그래프 위에 있도록 할 겁니다
선택 바인딩과 주석을 통한 자 표시를 통해 상호 교환적인 선 그래프를 만들었습니다 Swift Charts는 macOS에서도 선택을 지원합니다 여기서는 호버 제스처가 값 선택의 기본값이죠
여기서는 단일 값 선정 외에 그래프 선택 제어자의 변형이 범위 선택을 가능케 합니다 iOS에서 기본값은 두 손가락 탭 제스처인데요 macOS에서는 드래그 제스처입니다 Swift Charts에서도 선택에 대한 커스텀 제스처 제공이 가능하죠 ChartProxy는 제스처 위치 기반 값 선택을 도울 겁니다
그래프 값 선택은 X, Y 좌표를 넘어 파이, 도넛 그래프와도 매끄럽게 작동하고요 부채꼴을 태핑하면 강조되는 게 흥미롭죠
그래서 선택은 그래프에 대한 추가 정보를 드러내는 것과 관련이 깊습니다 상호 교환성의 또 다른 부분은 데이터 탐색인데요 '스크롤링'에 대해 알아봅시다
1년 동안의 일일 판매량을 시각화하고 싶은데요 스크린에 365일 전체를 나타내는 건 비현실적이니 스크롤 작동이 가능해야 하죠 스크롤링 활성화를 위해 chartScrollableAxes 제어자를 호출합니다 chartXVisibleDomain으로 30일을 시간 간격으로 하는 창을 눈에 보이게 설정할 수 있습니다 현재 가시 영역에서 총 판매량을 보여줄 수 있도록 chartScrollPosition을 사용해 현재 일자를 바인딩에 저장하죠 이제 손가락을 써서 스크롤할 수 있습니다
플롯 스크롤만 가능할 뿐 아니라 축 콘텐츠도 스크롤되어 아주 부드럽게 움직입니다 스크롤링은 다양한 방식으로 맞춤화가 가능한데요 일자 단위로 항상 빠르게 스크롤하고 싶다고 해봅시다 그래서 '스크롤 동작'이 필요한데요 ScrollTargetBehavior는 SwiftUI와 Swift Charts에서 스크롤 뷰 콘텐츠와 값을 통합할 수 있게 추가된 건데요
제가 원하는 스내핑 동작을 위해 하루 중 첫 시간으로 설정했습니다 majorAlignment는 스와이핑 동작을 정의해 한층 더 맞춤화하죠 이건 스와이핑으로 그래프 전체를 훑어볼 때 항상 매월 1일로 갈 수 있도록 한 달의 첫 번째 날로 설정했습니다
스크롤 가능한 차트는 SwiftUI의 스크롤 뷰 중 최신 개선 사항 위에 구축된 겁니다 더 많은 정보는 아래 세션에서 확인해 보세요 Swift Charts는 여러분에게 데이터 시각화의 무한한 가능성을 제공합니다 X, Y 좌표의 그래프 외에도 파이 그래프는 이제 Apple 디자인 그래프를 만드는 API 가족의 일부이죠 파이 그래프는 단순하지만 강력한 시각화 효과를 줍니다 부분 대 전체 데이터 관계를 나타낼 때 가장 좋죠 선택과 스크롤링 같은 상호 교환성 기능은 사용자가 데이터를 탐색할 때 전혀 다른 차원의 데이터 시각화를 보여줄 겁니다 파이와 도넛 차트를 즐겁게 이용하시길 바랍니다 ♪ ♪
-
-
2:06 - Stacked bar chart
Chart(data, id: \.name) { element in BarMark( x: .value("Sales", element.sales), stacking: .normalized ) .foregroundStyle(by: .value("Name", element.name)) } .chartXAxis(.hidden)
-
2:44 - Pie chart
Chart(data, id: \.name) { element in SectorMark( angle: .value("Sales", element.sales) ) .foregroundStyle(by: .value("Name", element.name)) }
-
3:05 - Pie chart with angular inset
Chart(data, id: \.name) { element in SectorMark( angle: .value("Sales", element.sales), angularInset: 1.5 ) .foregroundStyle(by: .value("Name", element.name)) }
-
3:06 - Pie chart with corner radius
Chart(data, id: \.name) { element in SectorMark( angle: .value("Sales", element.sales), angularInset: 1.5 ) .cornerRadius(5) .foregroundStyle(by: .value("Name", element.name)) }
-
3:33 - Donut chart
Chart(data, id: \.name) { element in SectorMark( angle: .value("Sales", element.sales), innerRadius: .ratio(0.618), angularInset: 1.5 ) .cornerRadius(5) .foregroundStyle(by: .value("Name", element.name)) }
-
4:02 - Donut chart with text in the center
Chart(data, id: \.name) { element in SectorMark( angle: .value("Sales", element.sales), innerRadius: .ratio(0.618), angularInset: 1.5 ) .cornerRadius(5) .foregroundStyle(by: .value("Name", element.name)) } .chartBackground { chartProxy in GeometryReader { geometry in let frame = geometry[chartProxy.plotAreaFrame] VStack { Text("Most Sold Style") .font(.callout) .foregroundStyle(.secondary) Text(mostSold) .font(.title2.bold()) .foregroundColor(.primary) } .position(x: frame.midX, y: frame.midY) } }
-
5:14 - Chart visualizing average sales by city
struct LocationDetailsChart: View { ... var body: some View { Chart { ForEach(data) { series in ForEach(series.sales, id: \.day) { element in LineMark( x: .value("Day", element.day, unit: .day), y: .value("Sales", element.sales) ) } .foregroundStyle(by: .value("City", series.city)) .symbol(by: .value("City", series.city)) .interpolationMethod(.catmullRom) } } ... } }
-
5:39 - Chart selection modifier
struct LocationDetailsChart: View { @Binding var rawSelectedDate: Date? var body: some View { Chart { ForEach(data) { series in ForEach(series.sales, id: \.day) { element in LineMark( x: .value("Day", element.day, unit: .day), y: .value("Sales", element.sales) ) } .foregroundStyle(by: .value("City", series.city)) .symbol(by: .value("City", series.city)) .interpolationMethod(.catmullRom) } } .chartXSelection(value: $rawSelectedDate) } }
-
5:47 - Processing raw selected date from chart selection binding
struct LocationDetailsChart: View { @Binding var rawSelectedDate: Date? var selectedDate: Date? { guard let rawSelectedDate else { return nil } return data.first?.sales.first(where: { let endOfDay = endOfDay(for: $0.day) return ($0.day ... endOfDay).contains(rawSelectedDate) })?.day } var body: some View { Chart { ForEach(data) { series in ForEach(series.sales, id: \.day) { element in LineMark( x: .value("Day", element.day, unit: .day), y: .value("Sales", element.sales) ) } .foregroundStyle(by: .value("City", series.city)) .symbol(by: .value("City", series.city)) .interpolationMethod(.catmullRom) } } .chartXSelection(value: $rawSelectedDate) } }
-
6:06 - Rule mark as selection indicator
Chart { ForEach(data) { series in ForEach(series.sales, id: \.day) { element in LineMark( x: .value("Day", element.day, unit: .day), y: .value("Sales", element.sales) ) } } if let selectedDate { RuleMark( x: .value("Selected", selectedDate, unit: .day) ) .foregroundStyle(Color.gray.opacity(0.3)) .offset(yStart: -10) .zIndex(-1) } } .chartXSelection(value: $rawSelectedDate)
-
6:20 - Selection popover
Chart { ForEach(data) { series in ForEach(series.sales, id: \.day) { element in LineMark( x: .value("Day", element.day, unit: .day), y: .value("Sales", element.sales) ) } } if let selectedDate { RuleMark( x: .value("Selected", selectedDate, unit: .day) ) .foregroundStyle(Color.gray.opacity(0.3)) .offset(yStart: -10) .zIndex(-1) .annotation( position: .top, spacing: 0, overflowResolution: .init( x: .fit(to: .chart), y: .disabled ) ) { valueSelectionPopover } } } .chartXSelection(value: $rawSelectedDate)
-
7:07 - Range selection
Chart(data) { series in ForEach(series.sales, id: \.day) { element in LineMark( x: .value("Day", element.day, unit: .day), y: .value("Sales", element.sales) ) } ... } .chartXSelection(value: $rawSelectedDate) .chartXSelection(range: $rawSelectedRange)
-
7:22 - Overriding default selection gesture
Chart(data) { series in ForEach(series.sales, id: \.day) { element in LineMark( x: .value("Day", element.day, unit: .day), y: .value("Sales", element.sales) ) } ... } .chartXSelection(value: $rawSelectedDate) .chartGesture { proxy in DragGesture(minimumDistance: 0) .onChanged { proxy.selectXValue(at: $0.location.x) } .onEnded { _ in selectedDate = nil } }
-
7:31 - Selection in pie charts and donut charts
Chart(data, id: \.name) { element in SectorMark( angle: .value("Sales", element.sales), innerRadius: .ratio(0.618), angularInset: 1.5 ) .cornerRadius(5) .foregroundStyle(by: .value("Name", element.name)) .opacity(element.name == selectedName ? 1.0 : 0.3) } .chartAngleSelection(value: $selectedAngle)
-
7:54 - Daily sales chart
Chart { ForEach(SalesData.last365Days, id: \.day) { BarMark( x: .value("Day", $0.day, unit: .day), y: .value("Sales", $0.sales) ) } .foregroundStyle(.blue) }
-
8:07 - Daily sales chart with a scrollable axis
Chart { ForEach(SalesData.last365Days, id: \.day) { BarMark( x: .value("Day", $0.day, unit: .day), y: .value("Sales", $0.sales) ) } .foregroundStyle(.blue) } .chartScrollableAxes(.horizontal)
-
8:11 - Setting the visible domain for a scrollable chart
Chart { ForEach(SalesData.last365Days, id: \.day) { BarMark( x: .value("Day", $0.day, unit: .day), y: .value("Sales", $0.sales) ) } .foregroundStyle(.blue) } .chartScrollableAxes(.horizontal) .chartXVisibleDomain(length: 3600 * 24 * 30)
-
8:18 - Chart scroll position
Chart { ForEach(SalesData.last365Days, id: \.day) { BarMark( x: .value("Day", $0.day, unit: .day), y: .value("Sales", $0.sales) ) } .foregroundStyle(.blue) } .chartScrollableAxes(.horizontal) .chartXVisibleDomain(length: 3600 * 24 * 30) .chartScrollPosition(x: $scrollPosition)
-
8:50 - Snapping in a scrolling chart
Chart { ForEach(SalesData.last365Days, id: \.day) { BarMark( x: .value("Day", $0.day, unit: .day), y: .value("Sales", $0.sales) ) } .foregroundStyle(.blue) } .chartScrollableAxes(.horizontal) .chartXVisibleDomain(length: 3600 * 24 * 30) .chartScrollPosition(x: $scrollPosition) .chartScrollTargetBehavior( .valueAligned( matching: DateComponents(hour: 0), majorAlignment: .matching(DateComponents(day: 1))))
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.