Building a range selector with ShinobiCharts: Part I

Written by Sam Davies

It’s been a while since we published this post, and both iOS and ShinobiCharts have moved on… but the good news is that we’ve got an updated version of the project on GitHub where you can browse, clone, or fork the project, or simply download the zip.

One of the most frequent questions we get about ShinobiPlay, our rather excellent app-store app which showcases ShinobiControls (you’d never guess I’m on the dev team), is “how can I create the impress chart”. A little background is required.

The impress chart is a chart which shows financial data, with open, close, high and low prices plotted against time. One of the more interesting features of this chart is that it has a range selector beneath it, which not only shows which part of the data the main chart is currently displaying, but also allows the user to change the visible range through intuitive interactions.

This short series of blog posts is going to run through the technical challenges associated with these advanced features. I’ll present these challenges as a sequence of requirements:

Creating a ‘range selector’. The view is comprised of 2 charts – one shows a summary of the data, and as such shows the entire data range, superimposed over which is a ‘range selector window’. The primary chart shows just the data within this range. Navigating the main chart should update the range selector chart.

Adding interaction with the range selector. Dragging the range selector should update the display in the main chart.

The ends of the range selector should have handles which, when moved, update the range displayed in the main chart.

Dragging the range selector should exhibit momentum.

The main chart should have a horizontal line and text annotation which tracks the right-most point of the currently visible data.

As you can see, we’re going to tackle quite a lot of bits and pieces, so I’ve split the project into different posts. In this first post we’re going to build the simplest first iteration of the range selector – by getting 2 charts to ‘talk to each other’.

The code for the completed project is available as a zip file download, or in a repo on GitHub. It was written in almost the same order as the write-up, so you can almost follow commit-by-commit. In order to use ShinobiControls, you’ll have to get yourself a 30 day free trial of ShinobiCharts – available on the website.

It’s not really the point of this blog series to talk about getting started with ShinobiCharts, and therefore we’ll breeze through the initial set up of the data source and the charts themselves.

The data layer

I want some time-series data for this project, and since I started writing the code on a plane, I didn’t have access to any. Therefore I’ve put together a really simple temperature data simulation. At the data access level, I’ve created a TemperatureDataPoint class which has 2 properties – temperature and timestamp:

The data layer is managed completely separately from any charting code. Although in this particular app it wouldn’t be too much of a problem, it’s good practice to keep a clean separation. Therefore we create a singleton to manage an array of TemperatureDataPoints:

Plotting basic charts

Now that we have created some sample data, we need to plot 2 charts. We could just go straight ahead and create some charts within the view controller, but I’d like to aim to create something a little more reusable than that, so I’ll create a ShinobiRangeSelectorUIView subclass, which will create and manage the two charts together. In this instance we’ll assume that both charts will use the same datasource (not always going to be true) and that we want to arrange them vertically.

The frame is as one would expect for a UIView subclass, the datasource is the data source the two charts share, and the splitProportion determines how much of the view should be allocated to the main chart and how much to the range selector chart.

We create ivars for the datasource and the two separate charts, and then in our custom constructor, we save off the data source and calculate the frames of the two charts, based on the frame we have been provided, and the splitProportion:

We have created a couple of utility methods to create the actual charts themselves. These methods are very much ShinobiCharts boiler-plate code – create a chart, pass in the license key (demo users only), assign the datasource, configure any additional functionality, and then add the chart as a subview to a UIView (in this case ourself):

These 2 methods are pretty similar – although the main chart has user interaction (i.e. the ability to pan and zoom) enabled, whereas the range chart doesn’t – we want the interaction on the range chart to be with the range selector, not the chart itself. We also remove all the axis markings from the range chart – this isn’t necessary, and is a stylistic choice – it makes for a cleaner looking UI.

In order to pull out some repetitive code here, we’ve made a couple of helper classes:

ShinobiLicense, which is a class to assist with managing the license key. In my implementation I saved the licence key into a plist and this class pulls the string out of there and returns it. Alternatively, you can just copy-paste your license code into the class itself (it’s pretty self-explanatory) when you look at the code in the repo.

ChartConfigUtilities: which pulls out some common functionality for configuring a chart when you have created it:

The methods are all pretty self-explanatory – there is nothing clever going on here. This is however, boiler-plate code that I find myself using nearly every time I create a Shinobi Chart, and therefore I use this class over and over again:

Chart Datasource

So we’ve now created a UIView subclass which, when provided with a suitable datasource, will draw 2 charts. Although we have created a singleton class to manage our data, we haven’t created a class which implements the SChartDatasource protocol – i.e. the chart datasource. This is standard Shinobi chart stuff:

@interface ChartDatasource : NSObject <SChartDatasource>
@end

And in the implementation, we grab hold of a reference to our shared data store and then implement the required SChartDatasource protocol methods by mapping from our data store to the structures required for a ShinobiChart:

We’ve now created all the bits so that we can plot the 2 charts really simply. There is a lot of ground work here, but it’ll make all the upcoming clever stuff a lot easier to implement now it’s nicely designed.

Therefore, in our app’s view controller, it’s as simple as this to display our two charts:

We define some ivars to keep hold of our range selector view, and our data source. Then we create these two objects, specifying that we want the main chart to be three times the height of the range chart, and that we want the entire view to fill the view controller’s view. Really simple, clean view controller. It’s worth planning ahead like this, to avoid the massive, sprawling view controllers that evolve. Well, ‘planning ahead’ and refactoring…

Annotations

So far all we’ve actually achieved is plotting 2 charts from a shared datasource – that’s hardly difficult. Now we need to start doing some clever stuff – firstly we’ll build the range selector on the range chart, and then get it to move as the user interacts with the main chart.

If you want to draw on top of ShinobiCharts you can can use standard UIKit techniques. However, if you want to draw in the chart’s data coordinate system (i.e. at particular values of x and y) ShinobiCharts provides the SChartAnnotation class. Since this is exactly what we need to do with the range selector, we will use annotations to place the constituent parts in the correct places.

We’re going to create a class to manage the range selector annotations, which we’ll call ShinobiRangeAnnotationManager. For now it has a simple interface, although we’ll add a few bits and pieces as we continue:

We add some private ivars in the implementation file – one for the chart and then some for the annotations which will make up the range selector. We’re going to construct it out of some simple parts. The central section (i.e. the selected range itself) doesn’t yet need an annotation (although it will later) as it is a transparent block. This region will be bounded by vertical lines, and these will be surrounded by shaded regions which will stretch to the extent of the chart.

Each of these 4 annotations will be an ivar so we can update their size and position when required:

As you can see, we override the default constructor to throw an exception, as we never want a user to be able to create a range selector without providing a chart. You might notice that the line annotations are of type SChartAnnotation, whereas the shaded regions are of SChartAnnotationZooming. This is due to the behaviour we want – so-called ‘zooming’ annotations are anchored to 2 points on the axis, whereas the non-zooming variety have only one anchor point. The ‘zooming’ name comes from how they behave when the chart undergoes zooming operations, which isn’t relevant in our case because the range chart has zooming disabled.

We then implement our custom constructor, which saves off the chart, and then calls a method to create the annotations:

Responding to user interaction

The range selector doesn’t look like very much yet, but that’s because we haven’t actually told it which range it should be displaying. Let’s do that now, by wiring it up to the main chart in the ShinobiRangeSelector. First of all we need to add a method to the API of the range annotation manager which will move the range selector as required:

ShinobiCharts has provided us with the SChartRange class, which contins maximum and minimum properties, and is used to specify ranges on axes. We provide a method on the API of our annotation manager which accepts a range and then redraws the annotations to highlight this specified range.

As mentioned before, the line annotations only require one x-value to determine where to position them, so we place one at the range maximum, and one at the minimum. The shaded regions require 2 values to render – so we use the x-axis extrema in combination with the provided range values to correctly place the regions.

In order to get the annotations to update positions it’s necessary to redraw the chart by sending it an aptly named redraw message.

As a final piece to this mammoth first blog post on this project, we need to wire this API method into our main chart. Charts have delegate methods to let you know when a user is interacting with them – both zooming and panning will change the range so we need to listen for these.

First of all, we need to make the ShinobiRangeSelector a delegate of the main chart:

@interface ShinobiRangeSelector : UIView <SChartDelegate>

- (void)createMainChartWithFrame:(CGRect)frame
{
...
// We use ourself as the chart delegate to get zoom/pan details
mainChart.delegate = self;
}

Then, we just need to implement the SChartDelegate methods we require:

These methods are called as the chart is panned or zoomed, and we simply find out the current axis range on the main chart and pass it to the annotation manager so that it can update its display. It’s that simple!

Conclusion, and what’s next…

Phew – that was quite a lot of stuff. We’ve gone from nothing to an app which displays 2 charts of the same data – one of which allows user interaction, the other of which has a cool-looking range selection overlay, which updates as the user interacts with the primary chart. When you consider all that this is actually quite a short post!

However, there’s so much more we can do – at the moment, we can’t interact with the range selector – something we really want to do. Try it – if you fire up the app you instinctively want to play around with that range selector. So, next post we’ll fix that.