Search code examples
swiftswiftuiswiftui-charts

show correct position on chart y-axis of chart overlay


I have the following swiftUI chart. I am trying to get the value of y based on where the user is dragging so I can position the Capsule on the correct position on the chart. Unfortunately I am unable to get the correct position of the y, but instead this code shows me where the user is dragging on the chart, but doesn't keep the indicator on the chart line.

Can anyone advise where I am going wrong. Here is my code. And if anything is unclear let me know and I can provide more details.

I thought this value(atY:as:) could help, but also didn't work.

struct ContentView: View {
    @State private var numbers = (0...10).map { _ in
        Int.random(in: 0...10)
    }
    
    @State private var indicatorIndex = 0
    @State private var indicatorNumber = 0.0
    @State private var indicatorLocation = CGPointMake(0, 0)
    
    var body: some View {
        Chart {
            ForEach(Array(zip(numbers, numbers.indices)), id: \.0) { number, index in
                LineMark(
                    x: .value("Index", index),
                    y: .value("Value", number)
                )
            }
        }
        .chartOverlay { proxy in
            GeometryReader { geometry in
                Capsule()
                    .strokeBorder(Color.red, lineWidth: 1.0)
                    .background(Color.blue)
                    .frame(width: 80, height: 20)
                    .overlay {
                        HStack {
                            Text("\(indicatorIndex)")
                                .font(.system(size: 12.0))
                                .fontWeight(.semibold)
                                .foregroundStyle(.red)
                            Text("\(indicatorNumber)")
                                .font(.system(size: 12.0))
                                .fontWeight(.semibold)
                                .foregroundStyle(.white)
                            
                        }
                       
                    }
                    .offset(x: indicatorLocation.x, y: indicatorLocation.y)
                Rectangle().fill(.clear).contentShape(Rectangle())
                    .gesture(
                        DragGesture()
                            .onChanged { value in
                                // Convert the gesture location to the coordinate space of the plot area.
                                let origin = geometry[proxy.plotAreaFrame].origin
                                let location = CGPoint(
                                    x: value.location.x - origin.x,
                                    y: value.location.y - origin.y
                                )
                                
                                // Get the x (date) and y (price) value from the location.
                                let (index, number) = proxy.value(at: location, as: (Int, Double).self) ?? (0, 0)
                                let test = proxy.value(atY: origin.x, as: Double.self)
                                indicatorIndex = index
                                indicatorNumber = number
                                indicatorLocation = CGPoint(x: value.location.x, y: test ?? 0.0)
                                print("Location: \(number) - number, \(index) - index , \(test) test")
                            }
                    )
            }
        }
        
    }
}

Here are some screenshots showing where the capsule is and where I want it to be placed

enter image description here

enter image description here


Solution

  • A ChartProxy has absolutely no idea of the data that is plotted in the chart. You should find the y coordinates of the line using your data source, i.e. the numbers array.

    // get the x value of the tapped location
    let x = proxy.value(atX: location.x, as: Double.self) ?? 0
    
    // get which two plotted points is the tapped location between
    let (lowerIndex, upperIndex) = (x.rounded(.down), x.rounded(.up))
    
    // get the y values for the above x values
    // these indices might be out of range - you should decide what to do in that case
    let (lowerNumber, upperNumber) = (numbers[Int(lowerIndex)], numbers[Int(upperIndex)])
    
    // now convert these to y *coordinates*
    let (lowerY, upperY) = (proxy.position(forY: lowerNumber) ?? 0, proxy.position(forY: upperNumber) ?? 0)
    
    // finally, linear interpolation
    func lerp(_ a: Double, _ b: Double, _ x: Double) -> Double {
        a + (b - a) * x
    }
    let y = lerp(lowerY, upperY, x - lowerIndex)
    
    indicatorLocation = CGPoint(x: value.location.x, y: y)
    

    This is relatively easy in this case since you plot every index of the array, and the interpolation is linear. In general, this can get very complicated. In any case though, the general idea is the same (see the code comments).