Dunfey · Hotel WWDC as data, est. 1983
Front desk everything
Years
Topics

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 ↗

Transcript all transcripts

Chapters

Code shown on screen · 19 snippets

Stacked bar chart swift · at 2:06 ↗
Chart(data, id: \.name) { element in
  BarMark(
    x: .value("Sales", element.sales),
    stacking: .normalized
  )
  .foregroundStyle(by: .value("Name", element.name))
}
.chartXAxis(.hidden)
Pie chart swift · at 2:44 ↗
Chart(data, id: \.name) { element in
  SectorMark(
    angle: .value("Sales", element.sales)
  )
  .foregroundStyle(by: .value("Name", element.name))
}
Pie chart with angular inset swift · at 3:05 ↗
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 swift · at 3:06 ↗
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 swift · at 3:33 ↗
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 swift · at 4:02 ↗
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 swift · at 5:14 ↗
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 swift · at 5:39 ↗
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)
  }
}
Processing raw selected date from chart selection binding swift · at 5:47 ↗
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)
  }
}
Rule mark as selection indicator swift · at 6:06 ↗
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 swift · at 6:20 ↗
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 swift · at 7:07 ↗
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 swift · at 7:22 ↗
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 swift · at 7:31 ↗
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 swift · at 7:54 ↗
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 swift · at 8:07 ↗
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 swift · at 8:11 ↗
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 swift · at 8:18 ↗
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 swift · at 8:50 ↗
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