My goal is to create a chart much like below. Where the diff colours represent the different slope / gradient % for each segment.
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?
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()
}
}
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()
}
}
}