Building a range selector with ShinobiCharts: Part I - Linking 2 charts
This tutorial is also available on the ShinobiControls blog. You’ll find better support and assistance on this site as part of ShinobiDeveloper
When I’m not hacking around with electronics and code, I work for ShinobiControls, and we make really cool iOS UI components, including mega-tastic charts, grids and some general purpose components essential for any discerning iOS developer. However, that’s enough of the advertising (although, it is worth a look - just have a browse of shinobicontrols.com).
One of the projects I have been involved in is building ShinobiPlay - which is an iPad app available from the app store which provides a developer using Shinobi a handy set of tools, together with showcasing what the controls are capable of. One of the most popular demos is called “impress”, which is a chart of a financial data set. It has a collection of custom-rolled advanced features which are possible due to the power of Shinobi.
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’.
As ever, the code for the completed project is available 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 Shinobi, 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 good separation. Therefore we create a singleton to manage an array of
TemperatureDataPoint
s:
This class is is created with the recommended objective-c singleton pattern, and
overrides the init
method to call an importData
method. We use this method
to generate our simulated temperature data:
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
ShinobiRangeSelector
UIView
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.
We only need one external method on the API for now:
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 ShinobiChart, 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
ShinobiChart stuff:
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
) Shinobi 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:
We are using standard factory methods provided by SChartAnnotation
, and since
we don’t yet have values for where to position them, we can pass in sensible
defaults.
In order to actually draw these annotations, we need to add an annotation manager
to the ShinobiRangeSelector
and set it up correctly:
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:
Shinobi 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:
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.
As I mentioned at the top, all the code is available on github at github.com/sammyd/Shinobi-RangeSelector. Grab that, and together with your demo of ShinobiCharts you can see how cool this is :)
Part II - creating custom handle annotations is available here.