Swift Charts: Raise the bar

Written by Cihat Gündüz

Description: Dive deep into data visualizations: Learn how Swift Charts and SwiftUI can help your apps represent complex datasets through a wide variety of chart options. We’ll show you how to plot different kinds of data and compose marks to create more elaborate charts. We’ll also take you through Swift Charts’ extensive chart customization API to help you match the style of your charts to your app. To get the most out of this session, we recommend you begin with “Hello Swift Charts” from WWDC22.

  • Problem space of chart libraries
    • Data Visualization
    • Communicate Data
    • Accessible
    • Localization
    • Dark Mode
    • Layout
    • Dynamic Type
    • Device Screen Sizes
    • Multi-Platform
    • Animation
  • declarative syntax, like SwiftUI
  • create charts by composition

Marks and composition

  • a Mark is a graphical element that represents data
  • Chart is a SwiftUI view used as the wrapper for charts
  • Provide multiple BarMark views to show multiple bars
  • .foregroundStyle can be used to specify a color
  • .asseccisibilityLabel and .accessibilityValue can be used to customize the default VoiceOver value
  • Data driven, e.g.:
let dailySales: [(day: Date, sales: Int)] = [
  (day: /* May 8th */, sales: 168),
  (day: /* May 9th */, sales: 117),
  (day: /* May 10th */, sales: 106),
  ...
]

Chart {
  ForEach(dailySales, id: \.day) {
    BarMark(
      x: .value("Day", $0.day, unit: .day),
      y: .value("Sales", $0.sales)
    )
  }
}
  • switching chart types is easy, e.g. replace BarChart with LineChart (or PointChart)
  • x parameter specifies the x-axis, y param the y-axis
  • use .foregroundStyle(by: .value("City", series.city) for automatic coloring
  • Use .symbol(by: .value...) for different chapes
  • Supports .interpolationMethod(.catmull...) for interpolation
  • Supports .position for custom positioning
  • AreaMark with x:yStart:yEnd to show a range of data
  • Also works with BarMark (to show range of data)
  • RectangleMark shows distinct marks for mid instead of building a graph in BarMark
  • RuleMark(y:) can be used alongside with .annotation(position:) for guiding lines

Plotting data with mark properties

  • Data Types
    • Quantitative (Int, Double, Decimal)
    • Nominal (String, Continent, ProductType)
    • Temporal (day: Date, time: Date)
  • Quantitative Sales, Nominal Name
  • Orientation of the bar depends on where the nominal data is
  • Available data marks and properties:
  • scale is available for all data types, e.g. yScale
  • by default, the lib infers the scales automatically
  • Use .chartYScale(domain:) modifier for custom

Customizations

  • All elements (axes, legends, plot area) are customizable
  • .chartXAxis {} with AxisMarks inside can be used for custom axis
  • Use AxisGridLine, AxisTick and AxisValueLabel with marks
  • also supports if kind of conditionals for dynamic axis
  • AxisMarks accepts a position param, e.g. .leading
  • AxisMarks accepts preset param, e.g. .extended to align visually with rest of interface
  • hide axis via .chartXAxis(.hidden) for example
  • hide legend via .chartLegend(.hidden)
  • .chartPlotStyle {} to customize plot area
    • e.g., plotArea.frame(height: 60 * numberOfCategories)
    • or plotArea.background(.pink.opacity(0.2)), any modifiers for views available
  • ChartProxy can be used to access the position(forX:) or value(atX:)
    • allows to coordinate other views with chart, e.g. select an interval in the chart with a slider
  • .chartOverlay modifier provides a ChartProxy in the content
struct InteractiveBrushingChart: View {
  @State var range: (Date, Date)? = nil

  var body: some View {
    Chart { ... }
    .chartOverlay { proxy in
      GeometryReader { nthGeoItem in
        Rectangle().fill(.clear).contentShape(Rectangle())
          .gesture(DragGesture()
            .onChanged { value in
              // Find the x-coordinates in the chart’s plot area.
              let xStart = value.startLocation.x - nthGeoItem[proxy.plotAreaFrame].origin.x
              let xCurrent = value.location.x - nthGeoItem[proxy.plotAreaFrame].origin.x
              // Find the date values at the x-coordinates.
              if let dateStart: Date = proxy.value(atX: xStart),
                 let dateCurrent: Date = proxy.value(atX: xCurrent) {
                range = (dateStart, dateCurrent)
              }
            }
            .onEnded { _ in range = nil } // Clear the state on gesture end.
          )
      }
    }
  }

  // ...
}
  • the proxy allows to store data in state and drive the chart rendering with that data (e.g. for a hover and show data effect)

Missing anything? Corrections? Contributions are welcome 😃

Related

Written by

Cihat Gündüz

Cihat Gündüz

📱Indie iOS Developer, 🎬Content Creator for 👨🏻‍💻Developers. 👾Twitch-Streamer & ▶️YouTuber.