I'm creating a line chart view, inspired by this blog post.
The lines are drawn with Path and the gradient with a LinearGradient and a mask, and they are in overlays on top of each other.
My main issue is that, as you can see in the screenshot, the start of all the gradients have a slope, they seem to close too soon if the value is zero (also the ends seem to be cut too early but it's not my main isssue). The starts also seem to begin too late if the value is > 0 (like the orange one).
What I want is for the gradient to fully fill the parts where the red arrows are pointing (removing the slopes and the abrupt cuts).
But I can't find a solution by just shuffling up the magic numbers. Besides, I'd prefer to understand what's happening rather than just poking around.
What am I doing wrong?
I've made a reproducible example, paste this is a ContentView:
struct ContentView: View {
@State var values: [Double] = [0, 0, 1, 2, 0, 6, 4, 0, 1, 1, 0, 0]
@State var totalMaxnum: Double = 6
var body: some View {
GeometryReader { proxy in
ScrollView {
VStack {
// some other view
VStack {
VStack {
RoundedRectangle(cornerRadius: 5)
.overlay(
ZStack {
Color.white.opacity(0.95)
drawValuesLine(values: values, color: .blue)
}
)
.overlay(
ZStack {
Color.clear
drawValuesGradient(values: values, start: .orange, end: .red)
}
)
.frame(width: proxy.size.width - 40, height: proxy.size.height / 4)
// some other view
}
// some other view
}
}
}
}
}
func drawValuesLine(values: [Double], color: Color) -> some View {
GeometryReader { geo in
Path { p in
let scale = (geo.size.height - 40) / CGFloat(totalMaxnum)
var index: CGFloat = 0
let y1: CGFloat = geo.size.height - 10 - (CGFloat(values[0]) * scale)
let y = y1.isNaN ? 0 : y1
p.move(to: CGPoint(x: 8, y: y))
for _ in values {
if index != 0 {
let x1: CGFloat = 8 + ((geo.size.width - 16) / CGFloat(values.count)) * index
let x = x1.isNaN ? 0 : x1
let yy: CGFloat = geo.size.height - 10 - (CGFloat(values[Int(index)]) * scale)
let y = yy.isNaN ? 0 : yy
p.addLine(to: CGPoint(x: x, y: y))
}
index += 1
}
}
.stroke(style: StrokeStyle(lineWidth: 3, lineCap: .round, lineJoin: .round, miterLimit: 80, dash: [], dashPhase: 0))
.foregroundColor(color)
}
}
func drawValuesGradient(values: [Double], start: Color, end: Color) -> some View {
LinearGradient(gradient: Gradient(colors: [start, end]), startPoint: .top, endPoint: .bottom)
.padding(.bottom, 1)
.opacity(0.8)
.mask(
GeometryReader { geo in
Path { p in
let scale = (geo.size.height - 40) / CGFloat(totalMaxnum)
var index: CGFloat = 0
// Move to the starting point
let y1: CGFloat = geo.size.height - (CGFloat(values[Int(index)]) * scale)
let y = y1.isNaN ? 0 : y1
p.move(to: CGPoint(x: 8, y: y))
// Draw the lines
for _ in values {
if index != 0 {
let x1: CGFloat = 8 + ((geo.size.width - 16) / CGFloat(values.count)) * index
let x = x1.isNaN ? 0 : x1
let yy: CGFloat = geo.size.height - 10 - (CGFloat(values[Int(index)]) * scale)
let y = yy.isNaN ? 0 : yy
p.addLine(to: CGPoint(x: x, y: y))
}
index += 1
}
// Close the subpath
let x1: CGFloat = 8 + ((geo.size.width - 16) / CGFloat(values.count)) * (index - 1)
let x = x1.isNaN ? 0 : x1
p.addLine(to: CGPoint(x: x, y: geo.size.height))
p.addLine(to: CGPoint(x: 8, y: geo.size.height))
p.closeSubpath()
}
}
)
}
}
You can see in the screenshot how the orange gradient doesn't properly fill all the area below the line: