Welkin's Secret Garden

Implementation of a Dynamic Line Graph

Some time ago, I need a broken line gragh in a project. Considering that it is pressed for time, I just found a lib on Github and used it in my product. At that time, I was moved by its dynamic effect. Thus I have to re-consider it again to find out how it works now!

Firstly, I’d like to show you the pretty effect.

The whole animation effect can be devided into 3 parts:

  1. Draw the coordinate axis
  2. Mark the coordinate points
  3. Connect all the points

I do not want to paste too much code this time, so I will just explain my thought.

Draw the coordinate axis

Figure out how many vertical lines should be drawn in the coordinate axis according to the amount of points inputted (each point has its place on the x-axis). As for y-axis, it is casual to define how many horizontal lines there should be, simply letting the spaces between two lines fixed. Additionally, defining the first horizontal and vertical lines’ position and the space will let you know positions of all the lines.

Because the lines’ position need to be used many times, we first save them into two arrays.

let coordinateLayer = CAShapeLayer()
coordinateLayer.strokeColor = UIColor.gray.cgColor
coordinateLayer.lineWidth = 0.5

let linePath = UIBezierPath()
for xLinePosition in xVerticalLinePositions {
linePath.move(to: CGPoint(x: CGFloat(xLinePosition), y: yInitPosition))
linePath.addLine(to: CGPoint(x: CGFloat(xLinePosition), y: yEndPosition))
for yLinePosition in yHorizontalLinePositions {
linePath.move(to: CGPoint(x: xInitPosition, y: CGFloat(yLinePosition)))
linePath.addLine(to: CGPoint(x: xEndPosition, y: CGFloat(yLinePosition)))
coordinateLayer.path = linePath.cgPath

Create a CAShapeLayer and add it as subLayer like this, and draw the axis with UIBezierPath’s addLine and move methods.

Mark the coordinate point

Here we meet an issue: How to figure out the max value on y-axis?

There is no doubt that we can merely take the max value that user inputted as the max value of y-axis. However, this may make the scale of the y-axis in a mess because it may not be a integer. The best solution is to find an integer which is slightly larger than the max value inputted, and it shoud be a tidy number such as 50, 100, 300 and so on. Then find a number which is easy to get an integer after division.

We generate the tidy number, depending on the second digit of the max value.

if >= 5, then the first digit plus 1 and set other digit to 0.

if < 5, then set the second digit to 5 and set other digit to 0.

Here we get the maxY.

Connect all the points

Just connect the points like step 1, and add a CABasicAnimation with the keypath strokeEnd , and at last make it changed from 0 to 1.

let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = 2.5
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animation.fromValue = 0
animation.toValue = 1
animation.autoreverses = false
valueLayer.add(animation, forKey: "pathAnimation")

That’s all.