I want to add a bottom gradient in my line graph like in the picture. I have the lines plotted out but I am not sure how to actually add the gradient. I was able to do this in UIKit but am not sure how to replicate it for SwiftUI.
SwiftUI Line Plot View:
var endingBalanceChart: some View {
GeometryReader { geometry in
Path { path in
for index in viewModel.endingBalance.indices {
let xPosition: CGFloat = geometry.size.width / CGFloat(viewModel.endingBalance.count) * CGFloat(index 1)
let yAxis: CGFloat = maxY - minY
let maxYPosition: CGFloat = (1 - CGFloat((Double(viewModel.endingBalance[index].y) - minY) / yAxis)) * geometry.size.height
let yPosition: CGFloat = index == 0 ? 200 : maxYPosition
if index == 0 {
path.move(to: CGPoint(x: 0, y: yPosition))
}
path.addLine(to: CGPoint(x: xPosition, y: yPosition))
}
}
.stroke(Color.blue, style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round))
}
}
I tried converting the path in to a UIBezierPath
and trying my UIKit implementation but no luck
This was my UIKit implementation:
func addGradient(path: UIBezierPath, hexString: String){
let color = UIColor(hexString: hexString).withAlphaComponent(0.4).cgColor
guard let clippingPath = path.copy() as? UIBezierPath else { return }
clippingPath.addLine(to: CGPoint(x: self.bounds.width, y: self.bounds.height))
clippingPath.addLine(to: CGPoint(x: 0, y: bounds.height))
clippingPath.close()
clippingPath.addClip()
let colors = [color, UIColor.clear.cgColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let colorLocations: [CGFloat] = [0.0, 1.0]
guard let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: colorLocations) else { return }
guard let context = UIGraphicsGetCurrentContext() else { return }
let startPoint = CGPoint(x: 1, y: 1)
let endPoint = CGPoint(x: 1, y: bounds.maxY)
context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: .drawsAfterEndLocation)
}
CodePudding user response:
Here would be a pure SwiftUI implementation.
I suggest to convert the var
into an own struct
that conforms to Shape
protocol. Then you can use it for both stroke and fill background.
This has the positive side effect that you don't need a GeometryReader
any more, as Shape provides you with the drawing rectangle with func path(in rect: CGRect)
.
The result looks like this:
let endingBalance: [Double] = [0, 1, 2, 4, 7, 11, 16, 22, 29, 37, 46, 56] // dummy data
struct ContentView: View {
var body: some View {
VStack {
Text("Chart")
EndingBalanceChart()
.stroke(Color.blue, style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round)) // line
.background(
EndingBalanceChart(isBackground: true)
.fill(.linearGradient(colors: [.cyan, .clear], startPoint: .top, endPoint: .bottom)) // background fill
)
.frame(height: 200)
.padding()
}
}
}
struct EndingBalanceChart: Shape { // chnaged var to a Shape struct
var isBackground: Bool = false
func path(in rect: CGRect) -> Path {
Path { path in
for index in endingBalance.indices {
let xPosition: CGFloat = rect.width / CGFloat(endingBalance.count) * CGFloat(index 1)
let maxY = endingBalance.max() ?? 0
let minY = endingBalance.min() ?? 0
let yAxis: CGFloat = maxY - minY
let yPosition: CGFloat = (1 - CGFloat((Double(endingBalance[index]) - minY) / yAxis)) * rect.height
if index == 0 {
path.move(to: CGPoint(x: 0, y: rect.height))
}
path.addLine(to: CGPoint(x: xPosition, y: yPosition))
}
if isBackground { // this is needed so the backkground shape is filled correctly (closing the shape)
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
}
}
}
}
CodePudding user response:
Here is an approach:
let amountPaid: [Double] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] // dummy data
let endingBalance: [Double] = [0, 1, 2, 4, 7, 11, 16, 22, 29, 37, 46, 56] // dummy data
struct ContentView: View {
@State private var value: Double = 0
var body: some View {
VStack {
Text("Chart")
CurveChart(data: endingBalance)
.stroke(Color.blue, style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round)) // line
.background(
CurveChart(data: endingBalance, isBackground: true)
.fill(.linearGradient(colors: [.cyan, .clear], startPoint: .top, endPoint: .bottom)) // background fill
)
.frame(height: 200)
.overlay(alignment: .topTrailing, content: {
plotPoint(data: endingBalance, index: Int(value))
.fill(.blue)
})
CurveChart(data: amountPaid)
.stroke(Color.blue, style: StrokeStyle(lineWidth: 1, lineCap: .round, lineJoin: .round)) // line
.background(
CurveChart(data: amountPaid, isBackground: true)
.fill(.linearGradient(colors: [.green, .clear], startPoint: .top, endPoint: .bottom)) // background fill
)
.frame(height: 60)
.overlay(alignment: .topTrailing, content: {
plotPoint(data: amountPaid, index: Int(value))
.fill(.green)
})
.padding(.bottom)
Slider(value: $value, in: 0...Double(endingBalance.count-1), step: 1.0)
}
.padding()
}
}
struct CurveChart: Shape { // chnaged var to a Shape struct
let data: [Double]
var isBackground: Bool = false
func path(in rect: CGRect) -> Path {
Path { path in
for index in data.indices {
let xPosition: CGFloat = rect.width / CGFloat(data.count-1) * CGFloat(index)
let maxY = data.max() ?? 0
let minY = data.min() ?? 0
let yAxis: CGFloat = maxY - minY
let yPosition: CGFloat = (1 - CGFloat((Double(data[index]) - minY) / yAxis)) * rect.height
if index == 0 {
path.move(to: CGPoint(x: 0, y: rect.height))
}
path.addLine(to: CGPoint(x: xPosition, y: yPosition))
}
if isBackground { // this is needed so the backkground shape is filled correctly (closing the shape)
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
}
}
}
}
struct plotPoint: Shape { // chnaged var to a Shape struct
let data: [Double]
let index: Int
let size = 20.0
func path(in rect: CGRect) -> Path {
let xStep = rect.width / Double(data.count-1)
let yStep = rect.height / (data.max() ?? 0)
let xCenter = Double(index) * xStep
let yCenter = rect.height - yStep * data[index]
var path = Path()
path.addEllipse(in: CGRect(x: xCenter - size/2, y: yCenter - size/2, width: size, height: size))
return path
}
}