I am trying to figure out how to pinch and zoom (x-axis only) a plot (only the line, not the whole view) in a Chart
. I can fake it using two slides, see my code below. It also includes a double-tap to zoom out and scrolling, and that works. But I cannot figure out how to convert a 'pinch' into a x-axis zoom of the data.
How can I update minXValue
and maxXValue
inside MagnifyGesture()
?
let data: [DataPoint] = (Int(minGlobalX) ..< Int(maxGlobalX)).map { DataPoint(x: Double($0), y: Double(arc4random()) / Double(UInt32.max)) }
struct ContentView: View {
@State private var minXValue = minGlobalX
@State private var maxXValue = maxGlobalX
var body: some View {
VStack {
Chart(data) {
LineMark(
x: .value("x", $0.x),
y: .value("y", $0.y)
)
.lineStyle(StrokeStyle(lineWidth: 1))
}
.chartScrollableAxes(.horizontal)
.chartXVisibleDomain(length: maxXValue - minXValue)
.chartXScale(domain: minGlobalX...maxGlobalX)
.onTapGesture(count: 2) {
minXValue = minGlobalX
maxXValue = maxGlobalX
}
.gesture(
MagnifyGesture()
.onChanged { value in
minXValue = ?? // Here
maxXValue = ?? // Here
}
)
.padding(20)
HStack(spacing: 20) {
Text("Min: \(Int(minXValue))")
Slider(value: $minXValue, in: minGlobalX ... maxXValue, step: 1)
}
.padding()
HStack(spacing: 20) {
Text("Max: \(Int(maxXValue))")
Slider(value: $maxXValue, in: minXValue ... maxGlobalX, step: 1)
}
.padding()
}
}
}
I think I figured it out, inspired by Isn't there an easy way to pinch to zoom in an image in SwiftUI?
Here is the working code:
struct DataPoint: Identifiable {
var id = UUID()
let x: Double
let y: Double
}
let minGlobalX: Double = 0
let maxGlobalX: Double = 100
let data: [DataPoint] = (Int(minGlobalX) ..< Int(maxGlobalX)).map { DataPoint(x: Double($0), y: Double(arc4random()) / Double(UInt32.max)) }
struct ContentView: View {
@State private var minXValue = minGlobalX
@State private var maxXValue = maxGlobalX
@State var scale: CGFloat = 1.0
@State var lastScaleValue: CGFloat = 1.0
var magnification: some Gesture {
MagnifyGesture()
.onChanged { value in
let delta = value.magnification / lastScaleValue
lastScaleValue = value.magnification
scale = scale * delta
let globalWidth = maxGlobalX - minGlobalX
let newWidth = 0.5 * (globalWidth - (globalWidth / scale))
let newMinX = minGlobalX + newWidth
if minGlobalX ... maxXValue ~= newMinX {
minXValue = newMinX
}
let newMaxX = maxGlobalX - newWidth
if minXValue ... maxGlobalX ~= newMaxX {
maxXValue = newMaxX
}
}
.onEnded { _ in
lastScaleValue = 1.0
}
}
var body: some View {
VStack {
Chart(data) {
LineMark(
x: .value("x", $0.x),
y: .value("y", $0.y)
)
.lineStyle(StrokeStyle(lineWidth: 1))
}
.chartScrollableAxes(.horizontal)
.chartXVisibleDomain(length: maxXValue - minXValue)
.chartXScale(domain: minXValue ... maxXValue)
.onTapGesture(count: 2) {
minXValue = minGlobalX
maxXValue = maxGlobalX
scale = 1.0 // make sure to reset the scale here as well
}
.gesture(magnification)
.padding(20)
HStack(spacing: 20) {
Text("Min: \(Int(minXValue))")
Slider(value: $minXValue, in: minGlobalX ... maxXValue, step: 1)
}
.padding(.horizontal)
HStack(spacing: 20) {
Text("Max: \(Int(maxXValue))")
Slider(value: $maxXValue, in: minXValue ... maxGlobalX, step: 1)
}
.padding(.horizontal)
}
}
}