Draw line graph with dynamic values objective c / swift

665 Views Asked by At

I am implementing line graph as shown in the attached video and image with dynamic values added in the array at every 1 second, mainly I want the graph to be drawn with the animation as shown in the video attached.

Till date i have tried using the https://github.com/Boris-Em/BEMSimpleLineGraph but i am not getting required result.

OUTPUT REQUIRED VIDEO LINK this reference video is from my android app.

CURRENT OUTPUT VIDEO LINK this is the output i am getting using BEMSimpleLineGraph

NOTE:- As we can see in the current output video there is no smoothness and animation as reference video, so need to achieve same

Need to get smoothness and curves exactly as shown HERE.!!

Also i have tried building the custom file for graph taking references from links during my research time, below is the code that i have tried

 class GraphCustom: UIView {
    
//    var data: [CGFloat] = [2, 6, 12, 4, 5, 7, 5, 6, 6, 3] {
//        didSet {
//            setNeedsDisplay()
//        }
//    }

    @objc var dynamicData : [CGFloat] = [0]{
        didSet {
            setNeedsDisplay()
        }
    }
    let shapeLayer = CAShapeLayer()
    var fromValue = CGPoint()
    var toValue = CGPoint()
    func coordYFor(index: Int) -> CGFloat {
        return bounds.height - bounds.height * dynamicData[index] / (dynamicData.max() ?? 0)
    }

   @objc override func draw(_ rect: CGRect) {

    
        let path = quadCurvedPath()

//        UIColor.black.setStroke()
//        path.lineWidth = 1
//        path.stroke()
    
//    let animation = CABasicAnimation(keyPath: "strokeEnd")
//    let timing = CAMediaTimingFunction()
//    animation.duration = 10.0
//    animation.fromValue = 0
//    animation.toValue  = 1
//    self.layer.add(animation, forKey: "strokeAnim")
    
    shapeLayer.path = path.cgPath
    
    shapeLayer.strokeColor = UIColor.black.cgColor
    shapeLayer.fillColor = UIColor.clear.cgColor
    shapeLayer.lineWidth = 2.0
    shapeLayer.lineCap = kCALineCapRound
    
    self.layer.addSublayer(shapeLayer)


    
    }
    
    @objc func drawLine(value:CGFloat){
        
        dynamicData.append(value)

        
        if (dynamicData.count > 3){
            dynamicData.remove(at: 0)
        }
        print("dynamicData >>>>>>>",dynamicData)

//        let path = quadCurvedPath()
//
//        let animation = CABasicAnimation(keyPath: "strokeEnd")
//
////        animation.fromValue = 0.0
////        animation.byValue = 1.0
//        animation.duration = 2.0
//
//        animation.fillMode = kCAFillModeForwards
//        animation.isRemovedOnCompletion = false
//
//        shapeLayer.add(animation, forKey: "drawLineAnimation")

//        UIColor.black.setStroke()
//        path.lineWidth = 1
//        path.stroke()

    }

    func quadCurvedPath() -> UIBezierPath {
        
        
        let path = UIBezierPath()
       

        let step = bounds.width / CGFloat(dynamicData.count - 1)

        print("Indccccc>>>>",dynamicData.count)
        
        var p1 = CGPoint(x: 0, y: coordYFor(index:dynamicData.count - 1 ))
        path.move(to: p1)

//        drawPoint(point: p1, color: UIColor.red, radius: 3)
        if (dynamicData.count == 2) {
            path.addLine(to: CGPoint(x: step, y: coordYFor(index: 1)))
            return path
        }

        var oldControlP: CGPoint?

        for i in 0..<dynamicData.count {
            let p2 = CGPoint(x: step * CGFloat(i), y: coordYFor(index: i))
            
            let mid = midPoint(p1: p1, p2: p2)
            
            path.addQuadCurve(to: mid, controlPoint: controlPoint(p1: mid, p2: p1))
            path.addQuadCurve(to: p2, controlPoint: controlPoint(p1: mid, p2: p2))
            
            p1 = p2
        }

    /*    for i in 1..<dynamicData.count {
            
            print(">>>>>>>>>>",i)
            
            let p2 = CGPoint(x: step * CGFloat(i), y: coordYFor(index: i))
            
        
//            drawPoint(point: p2, color: UIColor.red, radius: 3)
            var p3: CGPoint?
            if i < dynamicData.count - 1 {
                p3 = CGPoint(x: step * CGFloat(i + 1), y: coordYFor(index: i + 1))
            }

            let newControlP = controlPointForPoints(p1: p1, p2: p2, next: p3)
            
//            print(" >>>>>>>>>>>>>>>>",p2,oldControlP ?? p1,newControlP ?? p2)
            
            path.addCurve(to: p2, controlPoint1: oldControlP ?? p1, controlPoint2: newControlP ?? p2)

            fromValue = oldControlP ?? p1
            toValue = newControlP ?? p2
            
            p1 = p2
            oldControlP = antipodalFor(point: newControlP, center: p2)
        }*/
        


        
        return path;
    }
    func controlPoint(p1: CGPoint, p2: CGPoint) -> CGPoint {
            var controlPoint = midPoint(p1: p1, p2: p2)
            let diffY = abs(p2.y - controlPoint.y)
            
            if p1.y < p2.y {
                controlPoint.y += diffY
            } else if p1.y > p2.y {
                controlPoint.y -= diffY
            }
            return controlPoint
        }
    func midPoint(p1: CGPoint, p2: CGPoint) -> CGPoint {
            return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2)
        }
    /// located on the opposite side from the center point
    func antipodalFor(point: CGPoint?, center: CGPoint?) -> CGPoint? {
        guard let p1 = point, let center = center else {
            return nil
        }
        let newX = 2 * center.x - p1.x
        let diffY = abs(p1.y - center.y)
        let newY = center.y + diffY * (p1.y < center.y ? 1 : -1)

        return CGPoint(x: newX, y: newY)
    }

    /// halfway of two points
    func midPointForPoints(p1: CGPoint, p2: CGPoint) -> CGPoint {
        return CGPoint(x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2);
    }

    /// Find controlPoint2 for addCurve
    /// - Parameters:
    ///   - p1: first point of curve
    ///   - p2: second point of curve whose control point we are looking for
    ///   - next: predicted next point which will use antipodal control point for finded
    func controlPointForPoints(p1: CGPoint, p2: CGPoint, next p3: CGPoint?) -> CGPoint? {
        guard let p3 = p3 else {
            return nil
        }

        let leftMidPoint  = midPointForPoints(p1: p1, p2: p2)
        let rightMidPoint = midPointForPoints(p1: p2, p2: p3)

        var controlPoint = midPointForPoints(p1: leftMidPoint, p2: antipodalFor(point: rightMidPoint, center: p2)!)

        if p1.y.between(a: p2.y, b: controlPoint.y) {
            controlPoint.y = p1.y
        } else if p2.y.between(a: p1.y, b: controlPoint.y) {
            controlPoint.y = p2.y
        }


        let imaginContol = antipodalFor(point: controlPoint, center: p2)!
        if p2.y.between(a: p3.y, b: imaginContol.y) {
            controlPoint.y = p2.y
        }
        if p3.y.between(a: p2.y, b: imaginContol.y) {
            let diffY = abs(p2.y - p3.y)
            controlPoint.y = p2.y + diffY * (p3.y < p2.y ? 1 : -1)
        }

        // make lines easier
        controlPoint.x += (p2.x - p1.x) * 0.1

        return controlPoint
    }

    func drawPoint(point: CGPoint, color: UIColor, radius: CGFloat) {
        let ovalPath = UIBezierPath(ovalIn: CGRect(x: point.x - radius, y: point.y - radius, width: radius * 2, height: radius * 2))
        color.setFill()
        ovalPath.fill()
    }

}

extension CGFloat {
    func between(a: CGFloat, b: CGFloat) -> Bool {
        return self >= Swift.min(a, b) && self <= Swift.max(a, b)
    }


}

I have been trying to get required output since long time and have tried almost every link available on stackoverflow.

Any help would be great..!!

Thank You.

image

0

There are 0 best solutions below