2023 SwiftUI & UI Frameworks
WWDC23 · 10 min · SwiftUI & UI Frameworks
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.
Watch at developer.apple.com ↗Chapters
Code shown on screen · 19 snippets
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) Pie chart
Chart(data, id: \.name) { element in
SectorMark(
angle: .value("Sales", element.sales)
)
.foregroundStyle(by: .value("Name", element.name))
} 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))
} 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))
} 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))
} 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)
}
} 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)
}
}
...
}
} Chart selection modifier
struct LocationDetailsChart: View {
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)
}
} Processing raw selected date from chart selection binding
struct LocationDetailsChart: View {
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)
}
} 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) 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) 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) 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 }
} 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) Daily sales chart
Chart {
ForEach(SalesData.last365Days, id: \.day) {
BarMark(
x: .value("Day", $0.day, unit: .day),
y: .value("Sales", $0.sales)
)
}
.foregroundStyle(.blue)
} 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) 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) 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) 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)))) Resources
Related sessions
-
34 min -
15 min