Streaming is available in most browsers,
and in the Developer app.
-
Explore pie charts and interactivity in Swift Charts
Swift Charts has come full circle: Get ready to bake up pie and donut charts in your app with the latest improvements to the framework. Learn how to make your charts scrollable, explore the chart selection API for revealing additional details in your data, and find out how enabling additional interactivity can make your charts even more delightful.
Chapters
- 0:20 - Pie charts
- 4:22 - Selection
- 7:49 - Scrolling
Resources
Related Videos
WWDC23
-
Download
♪ ♪ Richard: Hello, I'm Richard. Today, I'm thrilled to talk to you about some exciting new features in Swift Charts: Pie charts, selection, and scrolling. Let's start with pie charts. Swift Charts provides composable and customizable building blocks for you to create all kinds of data visualizations. Today, new to the family of Swift Charts are the delicious, beautiful pie charts. Pie charts show how a total value is made up by various categories, via simple, familiar shapes. For example, here's a chart visualizing the pancake sales data for my friends' food truck. Pie charts do not have axes, and they are great for casual settings where precision is not critical. With broad understanding of how wedges make up a full circle, they are intuitive for visualizing part-to-whole relationships. One important reason people like pie charts is because of their round, approachable shapes. You can create a pie chart using the mark-based composition syntax that you are already familiar with. Introducing a new mark type, SectorMark. A SectorMark represents a slice in the pie. It's positioned in polar space. Not this polar, but the polar coordinate system. The size of a sector is proportional to the value that it represents. Along the radius, you can customize the look of a sector. If I increase the inner radius, the pie chart becomes a donut chart. With SectorMark, you can easily build all kinds of pie charts and donut charts. Let me show you an example. Our friends' international pancake food truck business saw a big, revitalizing growth in their daily sales throughout the last year, selling six styles of pancakes. This summer, I took on another challenge to help them improve their sales app.
I start with a chart visualizing the best-selling styles of pancakes. For that, the app currently has a simple stacked bar chart. It's got a BarMark, with sales being stacked along the X dimension. As it's categorical data, the category is reflected as the foreground style of each stacked bar. This chart gets the job done, but let's turn it into a pie chart to take advantage of the available space on the screen and make the data stand out. All I need to do is to switch out BarMark and argument x for SectorMark and angle. It is that simple! With SectorMark, I use angle to represent the sales quantity. The angle values you provide for a pie chart are automatically normalized to a full circle. I can also apply some style customizations. angularInset can be set on a sector to create a gap between sectors. Here, I set the angular inset for sectors to 1.5 points, so the gap between two sectors is double the inset, 3 points wide. I also set a corner radius which gives me a nicely rounded pie crust. That already looks stunning with just a few lines of code. To try a different look, let's turn this chart into a donut chart.
I set the inner radius to be a ratio of the full radius of the pie. To me, the golden ratio looks just about right, but you might like a different thickness for your donut. Of all the sold pancakes, we have a clear winner, cachapa, that's currently displayed above the chart. But because donut charts are hollow in the center, I want to move the text to the center of the chart. I put the text in a chartBackground. I use some position calculations to make sure the text is centered in the donut hole.
Now that's a nifty-looking donut chart. So that's pie charts and donut charts. They are a great way to make an impression with your data, and they are absolutely stunning on a big screen. Next, I'd like to dive deep into some chart interactivity features, starting with selection. By enabling interactivity in your charts, you are progressively disclosing additional details. Interactivity encourages people to explore data naturally using various forms of input, such as touch.
Selection is a direct way for you to communicate with a chart, and Apple-designed charts, like the heart rate chart, are perfect examples of that. By selecting a point along an axis, the chart will reveal additional information. Let's bring this idea over to the pancake sales app.
One chart in the app visualizes the average sales of each day of the week in two cities.
I'll enable value selection on this chart to reveal the detailed sales numbers via a popover that shows the number of pancakes sold on the selected day.
This is how the chart is defined. Each city has a data series. Each element in the series has a day of the week and a sales count. The lines are then styled by city name. You may be already familiar with the chartOverlay modifier, which allows you to overlay a SwiftUI view to capture gestures. But in iOS 17, I can use the new chartXSelection modifier. It handles all the gesture recognition for me and stores the selected value to a binding.
The selection modifier gives me the raw date value along the X axis, so I define a computed property to match it to a data point in my line chart. Let's extend the chart to display a popover when a value is selected.
When a value is selected, I add a vertical rule mark as a selection indicator. I set its Z index to be lower than the default 0 value to make sure the rule mark stays behind the line marks. Now let's make a popover on top of the selection indicator. I can do this as an annotation using a custom SwiftUI view. Annotations are usually positioned within the chart, but in this case, the popover extends beyond the bounds of the chart. This is where I need an overflow resolution for annotations.
On the X axis, I make it fit to the chart such that the popover never moves past the horizontal bounds of the chart. On the Y axis, I disable overflow resolution so that the annotation can be just above the chart.
With a selection binding and a rule mark with annotation, I now have an interactive line chart. Swift Charts supports selection on macOS too, where hover gesture is the default for value selection.
Besides single value selection, a variant of this chart selection modifier allows you to select a range. On iOS, the default is a two-finger tap gesture, and on macOS, it's a drag gesture. Swift Charts also allows you to provide a custom gesture for selection, with ChartProxy helping you select a value based on gesture locations.
Beyond charts in X and Y coordinates, chart value selection works seamlessly with pie charts and donut charts. Tapping and highlighting sectors is a lot of fun.
So selection was all about revealing additional information in your chart. Another important part of interactivity is about navigating the data. Let's talk about scrolling.
I want to create a chart that visualizes daily pancake sales for a whole year. Fitting all 365 days on a screen would be unrealistic, so it has to be scrollable. To enable scrolling, I simply call the chartScrollableAxes modifier. With chartXVisibleDomain, I set a visible window of 30 days as a time interval. To be able to display the total pancake sales in the current visible domain, I use chartScrollPosition to store the current date to a binding. Now I can just take my finger and scroll.
Not only can I scroll the plot, the axis content scrolls with it, and it's buttery smooth. Scrolling can be customized in a few different ways. For example, I'd like scrolling to always snap to a date unit. This brings us to scroll behaviors. ScrollTargetBehavior is a new addition to SwiftUI and Swift Charts that allows you to align the scroll view content with values.
For the snapping behavior that I wanted, I set it to match the first hour of a day. majorAlignment customizes it one step further by defining the swiping behavior. Here, I set it to the first day of a month, so that when I page through a chart by swiping, I'll always land on the first of each month.
Scrollable charts are built on top of some of the latest enhancements to scroll views in SwiftUI. For more information, be sure to check out Beyond Scroll Views. Swift Charts provides you with endless possibilities to visualize data. Beyond charts in and X and Y coordinates, pie charts are now part of the family of APIs to create Apple-designed charts. Pie charts are simple yet powerful visualizations. They work the best when representing part-to-whole data relationships. Interactivity features like selection and scrolling enable a whole new dimension in your data visualizations, putting your user at the driver's seat as they explore data. Enjoy baking pie and donut charts. ♪ ♪
-
-
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))))
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.