I have a chart - see screenshot below. As you can see I have a gradient on the colour on the LineMark
. I also have a Circle
on the chartOverlay
which tracks where the user drags. This is currently white
, but I want to update that to be the color of the line at that point. I have found this (foregroundstyle(for:)
) in the apple docs but doesn't work with any data I feed it and always returns nil
.
Here is my code
struct ContentView: View {
@State private var numbers = (0...10).map { _ in
Int.random(in: 0...10)
}
@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)
)
.foregroundStyle(
.linearGradient(
colors: [.green, .red],
startPoint: .bottom,
endPoint: .top
)
)
}
}
.chartOverlay { proxy in
GeometryReader { geometry in
Circle()
.fill(Color.white)
.frame(width: 30, height: 30)
.offset(x: -15.0, y: -15.0)
.offset(x: indicatorLocation.x, y: indicatorLocation.y)
Rectangle().fill(.clear).contentShape(Rectangle())
.gesture(
DragGesture()
.onChanged { value in
let origin = geometry[proxy.plotAreaFrame].origin
let location = CGPoint(
x: value.location.x - origin.x,
y: value.location.y - origin.y
)
let (index, number) = proxy.value(at: location, as: (Int, Double).self) ?? (0, 0)
....logic to get indicator location
indicatorLocation = CGPoint(x: value.location.x, y: y)
}
)
}
}
}
}
First, you need a function for interpolating between colours. I have adapted this answer to work with SwiftUI's Color.Resolved
.
extension Array where Element == Color.Resolved {
// percentage should be between 0 and 1
func intermediate(percentage: Float) -> Color.Resolved {
let percentage = Swift.max(Swift.min(percentage, 1), 0)
switch percentage {
case 0: return first ?? Color.Resolved(red: 0, green: 0, blue: 0, opacity: 0)
case 1: return last ?? Color.Resolved(red: 0, green: 0, blue: 0, opacity: 0)
default:
let approxIndex = percentage / (1 / Float(count - 1))
let firstIndex = Int(approxIndex.rounded(.down))
let secondIndex = Int(approxIndex.rounded(.up))
let firstColor = self[firstIndex]
let secondColor = self[secondIndex]
var (r1, g1, b1, a1): (Float, Float, Float, Float) = (
firstColor.red, firstColor.green, firstColor.blue, firstColor.opacity
)
var (r2, g2, b2, a2): (Float, Float, Float, Float) = (
secondColor.red, secondColor.green, secondColor.blue, secondColor.opacity
)
let intermediatePercentage = approxIndex - Float(firstIndex)
return Color.Resolved(
red: (r1 + (r2 - r1) * intermediatePercentage),
green: (g1 + (g2 - g1) * intermediatePercentage),
blue: (b1 + (b2 - b1) * intermediatePercentage),
opacity: (a1 + (a2 - a1) * intermediatePercentage)
)
}
}
}
Color.Resolved
is only available on iOS 17+. If you want to support lower versions, just use the answer as it is. But note that the percentage
parameter in that answer takes a value between 0 and 100. You should change the first line a little:
let percentage = Swift.max(Swift.min(percentage, 1), 0)
Then, add a @State
to store the color:
@State private var color: Color.Resolved = .init(red: 0, green: 0, blue: 0, opacity: 0)
// before iOS 17:
@State private var color: UIColor = .clear
After that, the color can be calculated like this:
color = [
Color.red.resolve(in: env),
Color.green.resolve(in: env)
].intermediate(percentage: Float((y - origin.y) / frame.height))
// before iOS 17:
color = [
UIColor(Color.red),
UIColor(Color.green)
]
.intermediate(percentage: (y - origin.y) / frame.height)
y
here is the y coordinate of the line chart at the x coordinate of the touch location, as calculated in my answer to your previous question.frame
is geometry[proxy.plotAreaFrame]
.env
is just a @Environment(\.self) var env
declared in the view.Note that pre-iOS 17 you cannot resolve
colors with regards to the SwiftUI environment, so the colors will not be affected by SwiftUI modifiers like .environment(\.colorScheme, .dark)
. Technically, if you don't plan on doing things like .environment(\.colorScheme, .dark)
, you don't need to resolve the colors either - just use UIColor
s like you do pre-iOS 17.