swiftuizoominghorizontal-scrollingswiftui-charts

chartXVisibleDomain not showing all of visible domain when scrolling


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)
        }
    }
}

Solution

  • 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)
            }
        }
    }