Why am I writing about charts when there are already tons of amazing libraries that could be used in your app? Because most of the time they don’t really look or work as we expect, and I’ve seen many developers who are afraid of this and spend too much time trying to customise libraries, especially when they should be dynamic.
The first reason is time. When we think about creating something new, we should consider the amount of time it takes. Is it worth the effort? If we are talking about charts, the answer could be ‘Yes’ in many cases. Why? Because creating a chart is really not black magic and it might be more time-consuming to fork a big complex “easy to customise” library, try to understand it and do the customisations. What’s more, I’ve always had trouble finding the most similar chart and then finding the one that could most easily be customised for our needs.
Well, nothing special other then understanding coordinates, really simple math and a little bit of core graphics (don’t worry! only the basics). You can download a sample app from this GitHub repository.
First, when we decide to create our awesome charts, we should decide if the chart will be static (easiest), dynamic (moderate) or interactive (hardest) because we cannot add gesture recognizers to UILayers, only to UIViews.
Then we should understand the basics of views bounds. Here’s a small illustration, which is all we need.
So 0,0 is in the top left and xMax and yMax is the bottom right.
There are some really useful methods when we are playing with the coordinates, such as:
CGRectGetMaxX(), CGRectGetWidth(), CGRectGetMidX()
and also for Y-axis of course. You just have to pass a CGRect to them. So when you are talking about the bottom of the chart then it will be CGRectGetMaxY(self.bounds). Don’t forget this!
Let’s say we have some data, like [amount: and date:]. The dates will be represented on the X-axis and the amounts on the Y-axis.
Let’s start with yPosition of the values. How do we calculate their position easily? We need to know the following values:
1. Range of the values (maxValue — minValue)
2. Height of the chart (in our case the chart is using the full bounds):
Now if we divide these two values (heightOfChart / rangeOfValues) we get a ratio. So when we want our data item’s amount yPos, we just have to multiply the amount with this ratio. But don’t forget that the chart is “upside down” so the calculation will look like this:
CGRectGetMaxY(self.bounds) - ((dataItem.amount) * ratio)
And this is the basic concept of the positions in most situations. So if we need, we can apply this on the xPosition as well. The X-axis will represent the date so the following values need to be known:
1. Number of days between the dates of the first and last items, or max date (code for this calculation is in the demo project).
2. Width of the chart: (CGRectGetMaxX(self.bounds)).
Then we can calculate the ratio for the X-axis like this:
CGRectGetMaxX(self.bounds) / numberOfTotalDays
So for the xPos:
xPos = daysBetweenItem * ratio;
Now we have everything to represent our date on the chart somehow.
We should make ourselves familiar with some basic graphical elements, such as UIView, UILayer, CAShapeLayer, UIBezierPath, etc. so we can calculate our points, but we need to present it somehow.
In this example we will draw a line chart and we will use UIBezierPath to create the path and a CAShapeLayer to draw along this path.
Here is how we can use the bezierPath this way:
UIBezierPath *graphPath = [UIBezierPath bezierPath];
[graphPath moveToPoint:CGPointMake(0, CGRectGetMaxY(self.bounds))];
First we created a path and then we created a point at the bottom left side or at the first item.
Then, while we iterate our data we just add points like this (we should calculate the x,y position while we also iterate our data).
[graphPath addLineToPoint:CGPointMake(xPos, yPos)];
Now we just have to draw a line along this path with a CAShapeLayer and add it to our layer:
CAShapeLayer *graphLayer = [CAShapeLayer layer];
graphLayer.path = [graphPath CGPath];
graphLayer.fillColor = _fillColor.CGColor;
graphLayer.strokeColor = _mainColor.CGColor;
graphLayer.shouldRasterize = YES;
graphPath.lineWidth = 3.0f;
graphLayer.rasterizationScale = UIScreen.mainScreen.scale;
graphLayer.opacity = 1.0f;
If we want to draw a line like steps, we just have to add a line to the next item’s xPos:
if (i < _values.count — 1)
Item *nextItem = _values[i+1];
CGFloat numberOfDays = [self numberOfDaysBetweenDate:[NSDate date] toDate:nextItem.date];
CGFloat xPos = fabs(numberOfDays) * xRatio;
[graphPath addLineToPoint:CGPointMake(xPos, yPos)];
Basically, if you have done these steps, you should now have a line with a fill color.
Now we just have to make it a little bit prettier, but I don’t want to go too much into detail. You can use the techniques above to draw some text for legends with this little code.
UILabel *label = [[UILabel alloc]initWithFrame:CGRectZero];
label.font = font;
label.text = text;
label.textColor = color;
label.textAlignment = NSTextAlignmentCenter;
label.center = point;
label.layer.anchorPoint = CGPointMake(0, 1);
label.layer.shouldRasterize = YES;
label.layer.rasterizationScale = UIScreen.mainScreen.scale;
or a dot :
UIView *cirleshape = [[UIView alloc] initWithFrame:CGRectMake(0, 0, radius * 2, radius * 2)];
cirleshape.backgroundColor = [color colorWithAlphaComponent:alpha];
cirleshape.layer.cornerRadius = cirleshape.bounds.size.width / 2.0f;
cirleshape.center = point;
Or draw some lines :
UIBezierPath *path = [UIBezierPath bezierPath];
CAShapeLayer *lineShapeLayer = [CAShapeLayer layer];
lineShapeLayer.path = [path CGPath];
lineShapeLayer.strokeColor = color.CGColor;
lineShapeLayer.lineWidth = width;
lineShapeLayer.fillColor = [[UIColor clearColor] CGColor];
lineShapeLayer.shouldRasterize = YES;
lineShapeLayer.rasterizationScale = UIScreen.mainScreen.scale;
lineShapeLayer.opacity = alpha;
For making the chart dynamic I use a really simple trick. Whenever I change a value, I clear the view out and redraw the chart.
[[self.layer sublayers] makeObjectsPerformSelector:@selector(removeFromSuperlayer)];
I could also have a reference for all the items that can change their properties. It works like a charm and makes the code a lot more friendly, and it won’t flicker between changes, or at least not on this level of complexity (as in the first example).
We should consider making references for some items, especially when we want some interaction. In this case the UI element should be a UIView unless you want to play with “hitTest” to determine if you tapped on a layer or not.
1. Easy to customise
2. Needs only a few hours to make
3. No forked libraries
1. Needs testing and fine-tuning
2. Could be hard to reuse if we hardcode specific logics
3. No excuses
It’s hard to find the perfect chart library, so don’t be afraid to create it yourself!
If you liked this article give us a clap and follow us here and on Facebook! ;)
Implementing Interactive, Dynamic Charts on iOS — Without Breaking a Sweat was originally published in Supercharge's Digital Product Development Guide on Medium, where people are continuing the conversation by highlighting and responding to this story.