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