This is a follow up question to a previous one about zooming in the X direction in a SwiftUI chart. I then added horizontal scrolling when the plot is zoomed in.
What happens with my code below is that zooming works, but when I scroll I cannot see the whole x range.
I am using
.chartXVisibleDomain(length: maxXValue - minXValue)
and
.chartXScale(domain: minGlobalX ... maxGlobalX)
where minXValue
and maxXValue
are the zoomed in min and max, and minGlobalX
and maxGlobalX
are the min and max of the complete data set.
If I print minXValue
and maxXValue
, they are indeed the correct zoomed values, but chartXVisibleDomain
doesn't seem to adjust, I only see part of the x domain.
How can I fix this?
Here is my 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 PlotView: 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 zoomedWidth = (globalWidth - (globalWidth / scale))
let newMinX = minGlobalX + (zoomedWidth / 2)
if minGlobalX ... maxXValue ~= newMinX {
minXValue = newMinX
}
let newMaxX = maxGlobalX - (zoomedWidth / 2)
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: minGlobalX ... maxGlobalX)
.onTapGesture(count: 2) { // double tap to reset the zoom
minXValue = minGlobalX
maxXValue = maxGlobalX
scale = 1.0
}
.gesture(magnification)
}
}
}
Finally found the solution. Turns out that I should not use chartXScale
, but instead use chartScrollPosition
.
Here is the updated 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 PlotView: View {
@State private var minXValue = minGlobalX
@State private var maxXValue = maxGlobalX
@State private var scale: Double = 1.0
@State private var lastScaleValue: Double = 1.0
@State private var scrollPosition: Double = 0.0 // <<== add here
let globalWidth = maxGlobalX - minGlobalX
var magnification: some Gesture {
MagnifyGesture()
.onChanged { value in
let delta = value.magnification / lastScaleValue
lastScaleValue = value.magnification
scale = scale * delta
let zoomedWidth = (globalWidth - (globalWidth / scale))
let newMinX = minGlobalX + zoomedWidth / 2
if minGlobalX ... maxXValue ~= newMinX {
minXValue = newMinX
}
let newMaxX = maxGlobalX - zoomedWidth / 2
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) // <<== remove here
.chartScrollPosition(initialX: minXValue) // <<== add here
.chartScrollPosition(x: $scrollPosition) // <<== add here
.onTapGesture(count: 2) {
minXValue = minGlobalX
maxXValue = maxGlobalX
scale = 1.0
}
.gesture(magnification)
}
}
}