I'm curious, is there a way to tell the chartXSelection(value:)
modifier to include the x-axis area when responding to user gestures? I know that starting a tap in the chart area and dragging down works, but I would like the user to just be able to tap on a value that they like in the x-axis, and have it registered by chartXSelection
.
See the example code below (edited to include tall axisMarks to demonstrate a problem with one proposed solution):
private struct PlotPoint {
let x: Double
let y: Double
}
private struct PlotSeries: Identifiable {
let id: Int
let name: String
let points: [PlotPoint]
}
private let data: [PlotSeries] = [
PlotSeries(id: 0, name: "Series A", points: stride(from: 0.0, through: 1.0, by: 0.05).map { y in PlotPoint(x: y*y, y: y) }),
PlotSeries(id: 1, name: "Series B", points: stride(from: 0.0, through: 1.0, by: 0.05).map { y in PlotPoint(x: y*y, y: y/2) })
]
struct AxisTap: View {
@State private var selectedX: Double?
var body: some View {
VStack {
Chart {
if let selectedX {
BarMark(xStart: .value("x", selectedX), xEnd: .value("x", selectedX + 0.05))
.foregroundStyle(.yellow.opacity(0.5))
}
ForEach(data) { series in
let plot = LinePlot( series.points, x: .value("x", \.x), y: .value("y", \.y))
plot
.symbol(by: .value("Series", series.name))
.foregroundStyle(by: .value("Series", series.name))
}
}
.chartXAxis {
AxisMarks() { value in
AxisGridLine()
AxisTick()
AxisValueLabel() {
if let axisValue = value.as(Double.self) {
VStack {
Text("|")
Text("v")
Text("\(axisValue.formatted())")
}
}
}
}
}
.chartXSelection(value: $selectedX)
.padding()
Text("selectedX is: \(selectedX ?? 0.0)")
}
}
}
I would like the user to be able to tap on the "0.5" in the x-axis and get a result like shown below (with "selectedX is: ~0.5" below).
A chartGesture
would also cover the area where the x axis lies.
.chartGesture { proxy in
DragGesture(minimumDistance: 0)
.onChanged { value in
selectedX = proxy.value(atX: value.location.x)
}
.onEnded { _ in
selectedX = nil
}
}
Now you don't need .chartXSelection(value: $selectedX)
anymore, consider changing selectedX
to a @GestureState
if you don't need to set it programmatically.
@GestureState private var selectedX: Double?
// ...
.chartGesture { proxy in
DragGesture(minimumDistance: 0)
.updating($selectedX) { value, state, _ in
print(value.location.x)
state = proxy.value(atX: value.location.x)
}
}
For a taller x axis, you can add a Rectangle
just below the plotFrame
rectangle for detecting gestures, in addition to chartGesture
or chartXSelection
.
.chartOverlay { proxy in
GeometryReader { geo in
let frame = geo[proxy.plotFrame!]
Rectangle()
.fill(.clear)
.frame(width: frame.width, height: 50) // choose a desired height here
.contentShape(.rect)
.gesture(
DragGesture(minimumDistance: 0)
.updating($selectedX) { value, state, _ in
state = proxy.value(atX: value.location.x)
}
)
.offset(x: frame.minX, y: frame.maxY)
}
}