Blog

Back to Blog

Plotting live data with ShinobiCharts

Posted on 12 Feb 2013 Written by Sam Davies

ShinobiControls offer great ways to visualise data in iOS apps, but how do we get hold of data to display in a grid, or plot in a chart? There are lots of different ways – e.g. hard-coding in your source code (e.g. the touch chart in ShinobiPlay), reading from a file distributed within your app (e.g. the discover chart in ShinobiPlay), or allowing the of the app to provide their own data (e.g. the chart designer or the organise grid in ShinobiPlay). However, in order that your app is the most useful to your users you will often want to read live data from the internet.

In this post we’re going to look at reading time series data from a JSON webservice, including regularly checking for new datapoints in order to ensure that we are displaying the most recent data to the user. We’ll establish the following:

  • A datasource object which pulls data from a webservice
  • Setting the data source to poll for new data
  • Create a UI which updates when new data arrives
  • Plotting the data in a chart

2012 12 11 Sample App Chart

This techniques introduced in this post are applicable to any JSON-based webservice, but here we will use a webservice I wrote which provides the current temperature of the ShinobiControls HQ. It’s a really simple service, and you can read about how it was built on my blog.

We’re working towards building the sample app I’ve put together. You can download a zip file of the project, or get it on github at github.com/sammyd/conair-ios. I’ll cover the most salient points, but won’t give an in-depth review of the app’s source code.

A DataSource

The JSON datasource looks like this:

[{"ts":"2013-01-31 08:42:00 UTC","value":21.549574351685315,"temperature":21.549574351685315},{"ts":"2013-01-31 08:44:00 UTC","value":21.67536480036259,"temperature":21.67536480036259}]

We need to pull out the temperature and timestamp (ts) properties from each data point.

We’ll create a class which is responsible for retrieving the data, and storing a local cache of it. Our different UI controllers will interact with this class to get hold of datapoints to render.

We will only want one instance of a datasource at any one time – multiple data sources in one application would be both memory intensive, and would cause multiple network requests for the same data. We therefore make the datasource a singleton:

#import <Foundation/Foundation.h>
@interface ConairDatasource : NSObject
@property (nonatomic, strong, readonly) NSMutableArray *data;
+ (ConairDatasource*)sharedDataSource;
@end
This header also exposes a readonly data array – which we will use later. The equivalent implementation is as follows:
#import "ConairDataSource.h"
@interface ConairDatasource ()
@property (nonatomic, strong, readwrite) NSMutableArray *data;
@end
@implementation ConairDatasource
+ (ConairDatasource *)sharedDataSource
{
    static ConairDatasource *sharedDataSource = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedDataSource = [[self alloc] init];
    });
    return sharedDataSource;
}
@end
We’ve redefined the readonly data property to be readwrite, and implemented the +sharedDataSource class method, to create a singleton.

In the first instance we’re going to pull the data from the webservice when the datasource is created, and to that end, we implement the standard constructor, and a utility method:

- (id)init
{
    self = [super init];
    if (self) {
        // Grab the inital data import
        [self collectDataFromInternet];
    }
    return self;
}
- (void)collectDataFromInternet
{
    NSDate *startDate = [NSDate dateWithTimeIntervalSinceNow:-24*60*60];
    NSDate *endDate = [NSDate date];
    // Generate the request URL
    NSString *urlString = [NSString stringWithFormat:@"http://sl-conair.herokuapp.com/data/?start=%@&stop=%@&step=%@",
                           [startDate dateAsISO8601String], [endDate dateAsISO8601String], @"120000"];
    NSURL *url = [NSURL URLWithString:urlString];
    // Send the request on a background thread
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^{
    		// Get the data
        NSData *data = [NSData dataWithContentsOfURL:url];
        // Parse the JSON data
        NSError *error;
        NSArray *json = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error];
        if(error) {
            NSLog(@"There was an error");
        } else {
            // Convert the JSON structure into a nice array
            NSMutableArray *dataPoints = [[NSMutableArray alloc] init];
            // Need to parse the date strings into NSDates
            NSDateFormatter *df = [[NSDateFormatter alloc] init];
            [df setTimeStyle:NSDateFormatterFullStyle];
            [df setDateFormat:@"yyyy-MM-dd HH:mm:ss Z"];
            [json enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
            		// This data source has ts and temperature keys
                NSDate *ts = [df dateFromString:obj[@"ts"]];
                NSDictionary *datapoint = @{@"ts" : ts, @"temperature" : obj[@"temperature"]};
                [dataPoints addObject:datapoint];
            }];
            if (dataPoints.count > 0) {
                // Perform the assignment on the main thread
                dispatch_async(dispatch_get_main_queue(), ^{
                    self.data = dataPoints;
                });
            }
        }
    });
}

The collectDataFromInternet method does all the heavy lifting. Firstly we construct the URL from which we can collect the data. This requires a start date, end date and a sampling period. The dateAsISO8601String method is added on NSDate with a category (using a NSDateFormatter to construct a string of the correct format).

We request the data on a background thread so as to not lock the main thread whilst we wait for a response. Once the data has been retrieved, we can parse the JSON into an NSArray using iOS methods – a lot easier since iOS 5 when these were introducted.

We then iterate through this array, and create a datapoint object for each of the received data points. Finally, we assign this newly created array to the data property on the datasource object. Note that we do this operation back on the main thread. By working on the main thread we can keep data property as nonatomic, without worrying about multi-threading issues.

Now that we have a collected the data we could display the latest temperature in a label really simply:

ConairDatasource *datasource = [ConairDatasource sharedDataSource];
NSNumber *latestTemperature = [[datasource.data lastObject] objectForKey:@"temperature"];
self.lblTemperature.text = [NSString stringWithFormat:@"%.1f", [latestTemperature floatValue]];

There main issue with this as it stands is that the data property of the data source will be nil until the data has been collected from the internet. We will address this issue in the “auto-updating UI” section.

Polling for new data

The method we wrote before to pull data down from the internet performs the operation once. If it is called again, it’ll request a new 24-hr period of data (based on the current time), and replace the original. In order to support some polling behaviour, we want to update the collectDataFromInternet method which to collect any new data since the most recent data point we already have.

Firstly, we need to make sure that the start time is the timestamp of the last datapoint we already have:

NSDate *startDate = [NSDate dateWithTimeIntervalSinceNow:-24*60*60];
if(self.data && self.data.count != 0) {
    startDate = [[self.data lastObject] objectForKey:@"ts"];
}

When we are parsing the returned JSON, we should check that the data points returned are newer than our latest one. This is primarily for the case where we get a repeated data point at the boundary. Given that we’ve pulled the latest date out into a local variable currentLatestDate (i.e. the date of the last object in the data array), then we add the following conditional to the enumeration block:

// Let's check that we have a new data point
if(!currentLatestDate || !([ts isEqualToDate:currentLatestDate] || ([ts compare:currentLatestDate] == NSOrderedAscending))) {
    NSDictionary *datapoint = @{@"ts" : ts, @"temperature" : obj[@"temperature"]};
    [dataPoints addObject:datapoint];
}

And finally, rather than replacing the data property with the newly collected datapoints, we might need to append the new datapoints to the existing data array:

if(!self.data) {
    self.data = dataPoints;
} else {
    [self.data addObjectsFromArray:dataPoints];
}

Now, repeatedly calling this collectDataFromInternet method will update our cached data array with new datapoints, if they are available. In order to repeatedly call this we add an NSTimer to the datasource, and add methods to start and stop the polling process. The data is only refreshed every 10 seconds, so we poll at the same rate.

@interface ConairDataSource () {
    NSTimer *pollingTimer;
}
...
@end
- (void)startPolling
{
    // Make sure we don't already have a timer running
    [self stopPolling];
    pollingTimer = [NSTimer scheduledTimerWithTimeInterval:self.pollingPeriod target:self selector:@selector(collectDataFromInternet) userInfo:nil repeats:YES];
}
- (void)stopPolling
{
    [pollingTimer invalidate];
    pollingTimer = nil;
}

Now we can send a startPolling message to the shared data source, and be assured that it will contain the latest data at any given time.

In the app which demonstrates this we start the polling when the app loads and stop it when the app is no longer active:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Start polling for data
    [[ConairDataSource sharedDataSource] startPolling];
    return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application
{
    // Stop polling for data
    [[ConairDataSource sharedDataSource] stopPolling];
}

- (void)applicationDidBecomeActive:(UIApplication *)application
{    
    // Restart polling for data
    [[ConairDataSource sharedDataSource] startPolling];
}

A simple auto-updating UI

Now we have a datasource which will always have the most recent data, we want to ensure that we are always displaying the latest data. iOS has a helpful mechanism which we can utilise to assist with this task – Key-Value Observing.

KVO allows us to subscribe to be notified whenever a specified key-path is updated, and hence (in this instance) update the UI.

In our view controller, we subscribe to changes in the datasource’s data property:

- (void)viewDidLoad
{
    [super viewDidLoad];
    // We keep an ivar of the datasource
    dataSource = [ConairDataSource sharedDataSource];
    // Subscribe to listen to changes on the data property
    [dataSource addObserver:self forKeyPath:@"data" options:NSKeyValueObservingOptionNew context:NULL];
    // Placeholder text for the temperature label
    self.lblTemperature.text = @"updating...";
}
- (void)dealloc
{
    [dataSource removeObserver:self forKeyPath:@"data"];
}

The import message to send KVC-compliant objects to observe their property changes is addObserver:forKeyPath:options:context. The will mean that the listener will receive messages when the appropriate key-path changes. It is important to remove observers when the instance is dealloc’ed, otherwise you’ll start getting zombie issues.

Whenever any KVO changes occur, they all pass a message of the same signature to the observer object. We implement the appropriate method, and update our temperature label:

- (void)updateTemperatureLabel
{
    self.lblTemperature.text = [NSString stringWithFormat:@"%2.1f°C", [[dataSource.data lastObject][@"temperature"] floatValue]];
    static NSDateFormatter *dateFormatter = nil;
    if (dateFormatter == nil) {
        dateFormatter = [[NSDateFormatter alloc] init];
        [dateFormatter setTimeStyle:NSDateFormatterFullStyle];
        [dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss Z"];
    }
    self.lblLastUpdated.text = [dateFormatter stringFromDate:[dataSource.data lastObject][@"ts"]];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if([keyPath isEqualToString:@"data"] && [object isEqual:datasource]) {
        [self updateTemperatureLabel];
    }
}

It’s (almost) that simple. Now, whenever the data property is changed on the datasource the temperature label will be updated. If you run up your app at this point you’ll see that the label appears with updating... in it, and then after a short time a temperature value is displayed. Fantastic.

However, there is a slight issue. The KVO will only be triggered when the property itself is changed. This happens when we first receive data, but subsequent polling updates don’t change the property, but instead add the new datapoints to the end of the array. This doesn’t trigger the KVO, so won’t update the label.

In order to get updates when objects are added to our array, we need to implement (and use) the Key-Value Coding collection accessor methods on our datasource.

#pragma mark - KVC methods
// We implement these so that we get KVO updates on array insertion
- (NSUInteger)countOfData
{
    return self.data.count;
}
- (id)objectInDataAtIndex:(NSUInteger)index
{
    return self.data[index];
}
- (void)insertObject:(NSDictionary *)object inDataAtIndex:(NSUInteger)index
{
    [self.data insertObject:object atIndex:index];
}
- (void)removeObjectFromDataAtIndex:(NSUInteger)index
{
    [self.data removeObjectAtIndex:index];
}
- (void)replaceObjectInDataAtIndex:(NSUInteger)index withObject:(NSDictionary *)object
{
    [self.data replaceObjectAtIndex:index withObject:object];
}

There are more details on these methods in the apple documentation. We wrap the standard NSMutableArray methods as appropriate.

Then, when we add data to our array, we simply need to use these KVC accessor methods instead of the array directly – this will then trigger the KVO update:

if(!self.data) {
    self.data = dataPoints;
} else {
    for (NSDictionary *dp in dataPoints) {
        [self insertObject:dp inDataAtIndex:self.data.count];
    }
}

If you run the app up now, then you’ll see that the date label gets updated as the polling process pulls in more data (note, new data on the ConAir service arrives approximately every 2 minutes). Perfect – and with no changes to code in the view controller.

2012 12 11 Sample App Text

Plotting data in a chart

Now that we’ve got the data auto-updating in our datasource object, the next step is to get it plotted in a ShinobiChart. We could equally display it in a ShinobiGrid, but once you see how easy it is to wire in a chart, you’ll realise that a grid isn’t any more difficult.

If you don’t already have a licence to use ShinobiCharts, then you can download a 30-day trial. If you want more information on how to use charts, then check out the ShinobiDeveloper zone, which includes API docs, quickstarts, FAQs and a developer forum.

A chart is a UIView subclass, so we add one to a new view controller, and then provide it with a data source.

- (void)viewDidLoad
{
    ...
    chart = [[ShinobiChart alloc] initWithFrame:self.view.bounds withPrimaryXAxisType:SChartAxisTypeDateTime withPrimaryYAxisType:SChartAxisTypeNumber];
    chart.licenseKey = [ShinobiLicense getShinobiLicenseKey];
    chart.datasource = dataSource;
    chart.theme = [SChartDarkTheme new];
    chart.xAxis.enableGesturePanning = YES;
    chart.xAxis.enableGestureZooming = YES;
    chart.xAxis.enableMomentumPanning = YES;
    chart.xAxis.enableMomentumZooming = YES;
    chart.yAxis.enableGesturePanning = YES;
    chart.yAxis.enableGestureZooming = YES;
    chart.yAxis.enableMomentumPanning = YES;
    chart.yAxis.enableMomentumZooming = YES;
    chart.yAxis.rangePaddingHigh = @1;
    chart.yAxis.rangePaddingLow = @1;
    chart.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    [self.view addSubview:chart];
}

We update our existing datasource to adopt the SChartDatasource protocol, which can then be used to provide the data to the chart:

#pragma mark - SChartDatasource methods
- (int)numberOfSeriesInSChart:(ShinobiChart *)chart
{
    return 1;
}
- (int)sChart:(ShinobiChart *)chart numberOfDataPointsForSeriesAtIndex:(int)seriesIndex
{
    return self.data.count;
}
- (NSArray *)sChart:(ShinobiChart *)chart dataPointsForSeriesAtIndex:(int)seriesIndex
{
    NSMutableArray *datapointArray = [[NSMutableArray alloc] init];
    [self.data enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        SChartDataPoint *dp = [[SChartDataPoint alloc] init];
        dp.xValue = obj[@"ts"];
        dp.yValue = obj[@"temperature"];
        [datapointArray addObject:dp];
    }];
    return [NSArray arrayWithArray:datapointArray];
}
- (SChartSeries *)sChart:(ShinobiChart *)chart seriesAtIndex:(int)index
{
    SChartLineSeries *series = [[SChartLineSeries alloc] init];
    return series;
}
These are the required methods of an SChartDatasource and are all pretty self explanatory.

We repeat the same KVO process we did on the label updating view controller:

- (void)redrawChart
{
    [chart reloadData];
    [chart redrawChartAndGL:YES];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if([keyPath isEqualToString:@"data"] && [object isEqual:dataSource]) {
        [self redrawChart];
    }
}

2012 12 11 Sample App Chart

And we’re done! That gives us a chart of the last 24 hours of temperature data, which will live-update as new readings are send from the arduino board we put together way back when!

The source code for the sample app I’ve based this post on is available to download as a zip file, and is also on github at github.com/sammyd/conair-ios. If you don’t already have a license for ShinobiCharts then you can get a 30-day trial here. You’ll then need to add the framework to the Xcode project and replace the license key in the github repo with the one provided in your trial email. The ShinobiLicense class contains the place to paste your license key string.

Back to Blog