Home > Mobile >  SwiftUI Add Bottom Gradient To Line Graph
SwiftUI Add Bottom Gradient To Line Graph

Time:12-17

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

enter image description here

enter image description here

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:

enter image description here

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:

enter image description here

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
    }
}
  • Related