Interactive Charts

I’ve spent a couple hours trying to figure out how to make SwiftUI charts interactive (not static, can tap, drag, there’s a tooltip and a cursor on the selected data). I found the word “interactive” mentioned even in Apple docs, but not a single handler or any example showing any form of interaction. Every article I found tells me to use a 3rd party package.

Is it possible that SwiftUI charts are just static (why would you even have something like that in 2022)? And if so, how did they create the charts in the Health App?

Answered by Carnifex in 731492022

I figured it out.

It doesn't come out of the box, some coding is needed.

(The algorithm is simple, get the X value for the tap/drag, convert it to chart X and Y, use Point and Rule markers. The example is for (Date, Double) chart values.)

  @State var position: ChartPosition?
   
  var body: some View {
    Chart {
      ForEach(session!.heightData) { height in
        AreaMark(
          x: .value("Time", height.time.start),
          y: .value("Height", height.height.doubleValue(for: diveManager.heightUnit))
        )
      }
      if let position = position {
        RuleMark(x: .value("Time", position.x))
          .foregroundStyle(.orange.opacity(0.5))
        RuleMark(y: .value("Height", position.y))
          .foregroundStyle(.orange.opacity(0.5))
        PointMark(x: .value("Time", position.x), y: .value("Height", position.y))
          .foregroundStyle(.orange)
          .symbol(BasicChartSymbolShape.circle.strokeBorder(lineWidth: 3.0))
          .symbolSize(250)
      }
    }
    .chartOverlay { proxy in
      GeometryReader { geometry in
        Rectangle().fill(.clear).contentShape(Rectangle())
          .gesture(DragGesture().onChanged { value in updateCursorPosition(at: value.location, geometry: geometry, proxy: proxy) })
          .onTapGesture { location in updateCursorPosition(at: location, geometry: geometry, proxy: proxy) }
      }
    }
  }
   
  func updateCursorPosition(at: CGPoint, geometry: GeometryProxy, proxy: ChartProxy) {
    let data = session!.heightData
    let origin = geometry[proxy.plotAreaFrame].origin
    let datePos = proxy.value(atX: at.x - origin.x, as: Date.self)
    let firstGreater = data.lastIndex(where: { $0.time.start < datePos! })
    if let index = firstGreater {
      let time = data[index].time.start
      let height = data[index].height.doubleValue(for: diveManager.heightUnit)
      position = ChartPosition(x: time, y: height)
    }
  }
Accepted Answer

I figured it out.

It doesn't come out of the box, some coding is needed.

(The algorithm is simple, get the X value for the tap/drag, convert it to chart X and Y, use Point and Rule markers. The example is for (Date, Double) chart values.)

  @State var position: ChartPosition?
   
  var body: some View {
    Chart {
      ForEach(session!.heightData) { height in
        AreaMark(
          x: .value("Time", height.time.start),
          y: .value("Height", height.height.doubleValue(for: diveManager.heightUnit))
        )
      }
      if let position = position {
        RuleMark(x: .value("Time", position.x))
          .foregroundStyle(.orange.opacity(0.5))
        RuleMark(y: .value("Height", position.y))
          .foregroundStyle(.orange.opacity(0.5))
        PointMark(x: .value("Time", position.x), y: .value("Height", position.y))
          .foregroundStyle(.orange)
          .symbol(BasicChartSymbolShape.circle.strokeBorder(lineWidth: 3.0))
          .symbolSize(250)
      }
    }
    .chartOverlay { proxy in
      GeometryReader { geometry in
        Rectangle().fill(.clear).contentShape(Rectangle())
          .gesture(DragGesture().onChanged { value in updateCursorPosition(at: value.location, geometry: geometry, proxy: proxy) })
          .onTapGesture { location in updateCursorPosition(at: location, geometry: geometry, proxy: proxy) }
      }
    }
  }
   
  func updateCursorPosition(at: CGPoint, geometry: GeometryProxy, proxy: ChartProxy) {
    let data = session!.heightData
    let origin = geometry[proxy.plotAreaFrame].origin
    let datePos = proxy.value(atX: at.x - origin.x, as: Date.self)
    let firstGreater = data.lastIndex(where: { $0.time.start < datePos! })
    if let index = firstGreater {
      let time = data[index].time.start
      let height = data[index].height.doubleValue(for: diveManager.heightUnit)
      position = ChartPosition(x: time, y: height)
    }
  }
Interactive Charts
 
 
Q