Chart is an ordinary SwiftUI view: gestures attach to it, and state changes redraw the marks. ChartProxy closes the loop by converting between touch positions and data values, which is how scrubbing and selection work.
State In, Marks Out
The simplest interaction needs no proxy at all: a gesture flips state, and the marks read it.
Chart {
ForEach(Array(zip(numbers, numbers.indices)), id: \.0) { number, index in
LineMark(x: .value("Index", index), y: .value("Value", number))
.foregroundStyle(isDragging ? .red : .blue)
}
}
.gesture(
DragGesture()
.onChanged { _ in isDragging = true }
.onEnded { _ in isDragging = false }
)
ChartProxy Maps Touches to Values
You cannot create a ChartProxy; it arrives through chartOverlay or chartBackground. value(atX:as:) turns an x coordinate into a data value, value(atY:) and value(for:) cover the other axes, and position(for:) goes the other way. The GeometryReader is required: plotAreaFrame is an Anchor, resolvable only through a GeometryProxy subscript.
.chartOverlay { chart in
GeometryReader { geometry in
Rectangle()
.fill(Color.clear)
.contentShape(Rectangle())
.gesture(
DragGesture()
.onChanged { value in
let currentX = value.location.x - geometry[chart.plotAreaFrame].origin.x
guard currentX >= 0, currentX < chart.plotAreaSize.width else { return }
guard let index = chart.value(atX: currentX, as: Int.self) else { return }
selectedIndex = index
}
.onEnded { _ in selectedIndex = nil }
)
}
}
Subtract the plot area's origin from the gesture location before asking the proxy, or the values shift by the axis labels' width.
Render the Selection as a Mark
Store the selected value in state and add a conditional mark for it, here a translucent rectangle behind the line. Annotations, color changes, or extra content work the same way.
Chart {
ForEach(Array(zip(numbers, numbers.indices)), id: \.0) { number, index in
if let selectedIndex, selectedIndex == index {
RectangleMark(
x: .value("Index", index),
yStart: .value("Value", 0),
yEnd: .value("Value", number),
width: 16
)
.opacity(0.4)
}
LineMark(x: .value("Index", index), y: .value("Value", number))
}
}