Published: Oct 20, 2024
If you’re using linear gradients in SwiftUI’s Charts framework, make sure that the delta between every color stop is at least 0.0001.
Let’s say you want to draw a linear gradient with hard stops, like the one used in Sidecar’s trip logger:
You might start with something like this:
import Charts import SwiftUI Rectangle() .foregroundStyle(.linearGradient( Gradient(stops: [ .init(color: .blue, location: 0), .init(color: .blue, location: 0.5), .init(color: .red, location: 0.500001), .init(color: .red, location: 1), ]), startPoint: .bottom, endPoint: .top )) .frame(width: 100, height: 100)
Linear gradients really want to draw smooth color gradients, so to achieve a hard stop we use two pairs of color stops separated by a teensy tiny gap where the “gradient” part actually occurs. The size of this gap seems pretty arbitrary, so you might pick a correspondingly arbitrarily small value.
Looks good! Let’s port this over to a SwiftUI Chart now.
import Charts import SwiftUI Chart { LineMark(x: .value("x", 0), y: .value("y", 0)) .lineStyle(StrokeStyle(lineWidth: 10)) .foregroundStyle(.linearGradient( Gradient(stops: [ .init(color: .blue, location: 0), .init(color: .blue, location: 0.5), .init(color: .red, location: 0.500001), .init(color: .red, location: 1), ]), startPoint: .bottom, endPoint: .top )) LineMark(x: .value("x", 10), y: .value("y", 10)) }
Easy peas….
oh no.
Turns out — casually said, after spending 3 hours trying to figure out what was going on here 😅 — there appears to be a bug in SwiftUI’s Charts framework that causes gradient stops with very small floating point deltas to result in weird behavior.
#
The workaroundThe workaround here is to increase the size of the gap between the two stops, specifically a delta of at least 0.00002, but I recommend using a minimum of 0.0001 for good measure.
With that adjustment in place, we get our desired effect:
import Charts import SwiftUI Chart { LineMark(x: .value("x", 0), y: .value("y", 0)) .lineStyle(StrokeStyle(lineWidth: 10)) .foregroundStyle(.linearGradient( Gradient(stops: [ .init(color: .blue, location: 0), .init(color: .blue, location: 0.5), .init(color: .red, location: 0.5001), .init(color: .red, location: 1), ]), startPoint: .bottom, endPoint: .top )) LineMark(x: .value("x", 10), y: .value("y", 10)) }
#
Complete playgroundYou can play with this bug using the following playground, which I’ve included in FB15549992.
import Charts import Foundation import PlaygroundSupport import SwiftUI struct Content: View { @State private var amount: Double = 50 var body: some View { Text("Drag me to the right to fix the Chart") Slider(value: $amount, in: 1...100) { Text("Breakpoint") } minimumValueLabel: { Text("0.5") } maximumValueLabel: { Text("0.51") } .frame(width: 500) let breakpoint = 0.5 + amount / 10000000 Text(breakpoint, format: .number) VStack { Chart { LineMark(x: .value("x", 0), y: .value("y", 0)) .lineStyle(StrokeStyle(lineWidth: 10)) .foregroundStyle(.linearGradient( Gradient(stops: [ .init(color: .blue, location: 0), .init(color: .blue, location: 0.5), .init(color: .red, location: breakpoint), .init(color: .red, location: 1), ]), startPoint: .bottom, endPoint: .top )) LineMark(x: .value("x", 10), y: .value("y", 10)) } Rectangle() .foregroundStyle(.linearGradient( Gradient(stops: [ .init(color: .blue, location: 0), .init(color: .blue, location: 0.5), .init(color: .red, location: breakpoint), .init(color: .red, location: 1), ]), startPoint: .bottom, endPoint: .top )) .frame(width: 100, height: 100) } .padding() } } PlaygroundPage.current.liveView = NSHostingController(rootView: Content())