Data streaming crosshairs and custom tooltips in ShinobiCharts

Written by Alison Clarke

Data streaming is a relatively new feature of ShinobiCharts (introduced in version 2.3) and not only opens up a whole new world of possibilities, but also introduces more complexity to your apps. This blog post uses a movement tracker app to demonstrate how you can add a custom crosshair tooltip to your ShinobiChart, and how to make sure it behaves nicely as new data is added to the chart.

If you haven’t built an app using ShinobiCharts before, then it would probably be a good idea to work through the quick start guide before reading on, because this tutorial doesn’t go into detail about the basics of ShinobiCharts.

About the app

The movement tracker app graphs the user’s current speed and distance, with a tooltip which shows a map displaying their location at the selected point, and the path they’ve taken. The project can be found on GitHub, or downloaded as a zip. In order to build the project you’ll need a copy of ShinobiCharts for iOS. If you don’t have it yet, you can download a free trial from the ShinobiCharts website. If you’ve used the installer to install ShinobiCharts, everything should just work. If you haven’t, then once you’ve downloaded and unzipped ShinobiCharts, open up the project in Xcode, and drag ShinobiCharts.embeddedframework from the finder into Xcode’s ‘frameworks’ group, and Xcode will sort out all the header and linker paths for you.

If you’re using the trial version you’ll also need to add your license key. To do so, open up ViewController.m and add the following line after the chart is initialised:

chart.licenseKey=@"your license key";

Hopefully you should now be able to build the project. Before firing it up in the simulator, set up a moving location (Debug -> Location -> City Bicycle Ride works nicely with the default settings). Then run the app and watch your graph appear as the location changes; tap on a line to see a tooltip showing the location on a map.

Here’s a quick guide to what’s what in the source code, before we dive into the specifics:

ViewController owns the CLLocationManager which provides us with our data, and sets up the chart and its datasource.

MovementTrackerDatum is a simple data object representing a point on the chart. It holds a CLLocation object, and the speed and total distance traveled at a single point in time.

MovementTrackerDataSource adopts the SChartDataSource protocol. It creates and holds a list of MovementTrackerDatum objects.

MapTooltip is a custom implementation of SChartTooltip, which replaces the usual label with a map showing the location.

MapCrosshair is a custom implementation of SChartCrosshair, which makes sure the crosshair remains visible and moves to the right place when the chart is redrawn.

Location Services

The data for the app comes from the location services provided by the CoreLocation framework. If you haven’t used CoreLocation before there’s a guide here. The first thing the application does is to set up the location manager inside initWithCoder: in ViewController.m:

We set up the view controller as a CLLocationManagerDelegate so that we can respond to changes in the user’s location. The view controller implements the locationManager:didUpdateLocations: method, to pass the user’s new location to both the datasource (to update the chart) and the map tooltip (to draw the path on the map). We’ll look at the details of that method later on.

The Data Source

MovementTrackerDataSource is an implementation of SChartDatasource. It stores the movement data in an array of MovementTrackerDatum objects, _locationData. The implementations of the 4 required methods of the SChartDataSource protocol are fairly straightforward:

Note that we’ve enabled crosshairs for both data series. The sChart:dataPointAtIndex:forSeries method just grabs the relevant MovementTrackerDatum object from _locationData and creates an SChartDataPoint object from its timestamp and its speed or distance.

The data source also implements the optional method sChart:yAxisforSeriesAtIndex: because each series has its own y-axis:

This method accepts the current locations and creates a new MovementTrackerDatum object, converting the metric units received into imperial units, and calculating the total distance travelled. getLastLocation is a trivial method returning the location from the last item in _locationData.

Setting up the chart

The chart setup is done in the setupChart method inside ViewController.m:

Most of this is again fairly standard chart setup. What you may not have seen before is multiple y-axes: we have a separate y axis for speed and distance, and place the second (distance) y-axis on the right hand side of the chart. We’ll use the variable _chartSetup to check whether the chart setup is complete later on: we need to know that it’s loaded rather than just that it’s not null, to avoid race conditions.

Normally we’d call a setupChart method inside viewDidLoad. However, in this case, we don’t have any datapoints when the application starts. This would be fine if we had fixed the ranges on the axes, but we want the ranges to be automatic, and to keep updating as the data is added, so if we try to set up the chart without data points or data ranges, it will get a bit confused. We get round this by calling setupChart after we’ve received the first datapoint from the location manager, inside locationManager:didUpdateLocations:

The method first calls addLocation: method on our datasource. It then checks whether the chart has been setup, and sets it up if not. If the chart has been set up, we call appendNumberOfDataPoints:toEndOfSeriesAtIndex: for both series, and redraw it.

If you’ve got to this point (and added in all the necessary boilerplate code – see GitHub) then you should have a streaming chart:

Crosshairs and custom tooltips

We’ve now seen how to set up a live datasource to give us a streaming chart; now we’ll add a custom tooltip to make it a bit more interesting. First, let’s clarify what we mean by crosshair and tooltip, as the terms are often confused:

The crosshair consists of the vertical and horizontal lines from the selected point on the chart to the x and y axes.

The tooltip belongs to the crosshair and is a view which displays information about the selected point.

The custom tooltip

We’ll look at our custom tooltip first. MapTooltip is a subclass of SChartCrosshairTooltip, and implements the MKMapViewDelegate protocol. It is initialized with a MovementTrackerDataSource and a CLLocationManager, which it uses to retrieve its data later on. SChartCrossHairTooltip is a subclass of UIView, so our tooltip’s constructor simply sets up the view to be in a 200×200 frame with a black border, then calls the setupMap method:

This method creates an MKMapView, centered on the current location, and adds it to the current view in place of the standard tooltip label. The map view sets itself as the delegate, so we can draw an overlay on top of the map to plot our path.

The tooltip provides a method addLocations: which gets called from within locationManager:didUpdateLocations: in the view controller. The addLocations: method takes an array of locations and creates an MKPolyline between the points, then adds it as an overlay to the map view:

This method uses our MovementTrackerDataSource to get the CLLocation object from the given dataPoint. It then draws an MKPointAnnotation at that location, and sets its title and subtitle to display the distance and time. Finally it centres the map on the selected location. So we end up with a tooltip looking something like this:

The custom crosshair

The built-in crosshair can be set to use our custom tooltip, and it will work fine…until the location is updated, when the crosshair and tooltip will disappear. This is because the chart is redrawn at that point, and the default behaviour is to hide the crosshair when that happens: that’s the sensible thing to in the generic case, because the data in the newly drawn chart may bear no resemblance to the old data. In our case, however, we know that the data point which was selected when the chart was redrawn will still be there (even though it will have moved), so we want to keep the crosshair and tooltip visible, but move them to their new position on the graph. We do this in MapCrosshair, our custom implementation of SChartCrosshair.

The overridden SChartCrosshair methods in MapCrosshair simply keep track of the current state of play of the crosshair, then call the parent method:

The method makes use of the pixelValueForDataValue: method of an axis, which converts a data value to the distance in pixels from the origin, to calculate the new position of the last selected data point. It then calls the parent method to move the crosshair to the new position.

Putting it together

To use our custom crosshair and tooltip classes, we set them up inside the setupChart method in ViewController:

The reorderSubviews method ensures that the tooltip is drawn on top of the axes when it reaches the edges of the chart. We need to do this whenever the chart or crosshair has been redrawn. We also call our crosshair’s updateCrosshair method when the chart is redrawn.

And there we have it…an app which streams data to the chart, shows a custom tooltip, and persists the crosshair as new data is added. You can see the full project on GitHub, or download it as a zip. Any questions? Give us a shout.