bezierPath setfill with diff colours Not Working ( to create a mountain climb color coding )

85 Views Asked by At

My goal is to create a chart much like below. Where the diff colours represent the different slope / gradient % for each segment.

enter image description here

My previous code, I have already managed to get the "mountain" outline, now further improved it such that I am now making a set of "boxes" which represents each colour segment. But am facing some issue.

Here is the code I'm using right now.

    let myBezier = UIBezierPath()
    let nextPt = fillSlopes(at: idx)

    if idx > 0 {
      let prevPt = fillSlopes(at: idx - 1)
      myBezier.move(to: CGPoint(x: prevPt.x, y: height))
      myBezier.addLine(to: CGPoint(x: prevPt.x, y: prevPt.y))
      myBezier.addLine(to: CGPoint(x: nextPt.x, y: nextPt.y))
      myBezier.addLine(to: CGPoint(x: nextPt.x, y: height))
      myBezier.addLine(to: CGPoint(x: prevPt.x, y: height))

      myBezier.close()
      // This is test code, actual code will compare current slope % 
      // (in an array) and then based on the slope %, will change
      // the colour accordingly.
      if idx % 2 == 0 {
        UIColor.systemRed.setFill()
        myBezier.fill()
      } else {
        UIColor.systemBlue.setFill()
        myBezier.fill()
      }

This is the results. Unfortunately, the colours is not coming out properly. What Am I missing?

enter image description here

In addition to this, Drawing so many little boxes is really eating up a lot of CPU cycles, if there are other suggestions to get the desired output result is also appreciated. Thanks

Update: Many Thanks for @seaspell, I realised what was happening. I was putting my let myBezier = UIBezierPath() outside of the for loop hence my [UIBezierPath] array was getting mangled up. eg:

This got things all wrong:

func xxx { 
  let myBezier = UIBezierPath() // <<<<<< this is wrong
      
  for idx in slopeBars.indices { 
      // do stuff 
  }

This is Correct and fixed some performance issue as well:

func xxx { 
  var slopeBars = [UIBezierPath]()
  var slopeBarColorArray = [UIColor]()

  for idx in slopeBars.indices { 
    let myBezier = UIBezierPath()  // <<<<< Moved here
    let nextPt = fillSlopes(at: idx)

    if idx > 0 {
      let prevPt = fillSlopes(at: idx - 1)
      myBezier.move(to: CGPoint(x: prevPt.x, y: height))
      myBezier.addLine(to: CGPoint(x: prevPt.x, y: prevPt.y))
      myBezier.addLine(to: CGPoint(x: nextPt.x, y: nextPt.y))
      myBezier.addLine(to: CGPoint(x: nextPt.x, y: height))
      myBezier.addLine(to: CGPoint(x: prevPt.x, y: height))

      myBezier.close()
      slopeBars.append(myBezier)

        if gradient < 0.0 {
          slopeBarColorArray.append(UIColor(cgColor: Consts.elevChart.slope0_0))
        } else if gradient < 4.0 {
          slopeBarColorArray.append(UIColor(cgColor: Consts.elevChart.slope0_4))
       }

    for i in 0..<slopeBars.count {
      if !slopeBarColorArray.isEmpty {
        slopeBarColorArray[i].setStroke()
        slopeBarColorArray[i].setFill()
      }
    
      slopeBars[i].stroke()
      slopeBars[i].fill()
    }
  }

enter image description here

1

There are 1 best solutions below

6
SeaSpell On BEST ANSWER

There's nothing really wrong with using bezier paths for this. You need to use a new one for each bar though and also you don't want to build them in draw(rect:). I suspect that is the cause of the CPU issues.

Take a look at this example I put together.

   class BarChartView: UIView {

    lazy var dataPoints: [CGFloat] = {
        var points = [CGFloat]()
        for _ in 1...barCount {
            let random = arc4random_uniform(UInt32(bounds.maxY / 2))
            points.append(CGFloat(random))
        }
        return points
    }()

    var bars = [UIBezierPath]()
    private let barCount = 20

    lazy var colors: [UIColor] = {
        let colors:  [UIColor] = [.red, .blue, .cyan, .brown, .darkGray, .green, .yellow, .gray, .lightGray, .magenta, .orange, .purple, .systemPink, .white]
        var random = [UIColor]()

        for i in 0...barCount {
            random.append(colors[Int(arc4random_uniform(UInt32(colors.count)))])
        }

        return random
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.bars = buildBars()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        self.bars = buildBars()
    }


    func buildBars() -> [UIBezierPath] {

        let lineWidth = Int(bounds.maxX / CGFloat(barCount))

        var bars = [UIBezierPath]()

        for i in 0..<barCount {

            let line = UIBezierPath()

            let leftX = lineWidth * i
            let rightX = leftX + lineWidth
            let centerX = leftX + (lineWidth / 2)

            let nextLeftPoint = (i == 0 ? dataPoints[i] : dataPoints[i - 1])
            let nextRightPoint = (i == barCount - 1 ? dataPoints[i] : dataPoints[i + 1])

            let currentPoint = dataPoints[i]


            //bottom left
            line.move(to:  CGPoint(x: leftX, y: Int(bounds.maxY)) )
            //bottom right
            line.addLine(to: CGPoint(x: rightX, y: Int(bounds.maxY)) )

            //top right
            line.addLine(to: CGPoint(x: rightX,  y: Int((currentPoint + nextRightPoint) / 2)) )

            //top center
            line.addLine(to: CGPoint(x: centerX, y: Int(currentPoint)) )

            //top left
            line.addLine(to: CGPoint(x: leftX,  y: Int((currentPoint + nextLeftPoint) / 2)) )

            //close the path
            line.close()

            bars.append(line)
        }

        return bars
    }

    override func draw(_ rect: CGRect) {
        super.draw(rect)

        for i in 0..<bars.count {
            colors[i].setStroke()
            colors[i].setFill()

            bars[i].stroke()
            bars[i].fill()
        }
    }
}