Enhancing a Kotlin chart with Advanced Charting Kit – part 1

Written by Kai Armer

Ever since Google announced support for Kotlin on Android back in 2017, its popularity amongst developers has continued. Having initially experimented with Kotlin I was keen to see how it would get along with the shinobicharts and Advanced Charting Kit libraries. Being the owner of a wearable fitness tracking device I decided to write a simple app which would display a chart showing my heart rate over a typical working day.

I began by taking a day’s worth of data and placing into a simple file. The data is presented as a timestamp value and an integer showing the beats per minute at that time. A typical data row looks like this:

1536040890113,56

In production code your data may be real-time but a simple file is adequate for this demo. The device which I have captures a data point approximately every 6 seconds, so for a typical working day there was approximately 9,000 data points. What makes shinobicharts a favourite with our customers is its ability to handle lots of data, so this number was no cause for concern. Getting a shinobichart up and running in Kotlin really is simple. A Kotlin quick-start guide and sample app can be found in the docs section of the download bundle or here. My first attempt gave a chart which looked like this:

If you’ve seen the Kotlin quick start sample app you’ll be familiar with the basics of how to set up a chart in Kotlin. The main difference here is that I use code similar to the following to read from a text file and populate a DataAdapter:

val reader = BufferedReader(InputStreamReader(context.assets.open(filename)))
do {
    val dataRow = reader.readLine()
    if (dataRow != null) {
        val lineItem = dataRow.split(',')
        dataAdapter.add(DataPoint(Date(lineItem[0].toLong()),
                lineItem[1].toDouble()))
    }
} while (dataRow != null)
reader.close()

Its a good start but it needs a little color. Being a heart rate chart it would make sense to show the low, medium and high zones, using suitable colors within the series’ fill. The Advanced Charting Kit (from herein I’ll refer to this as ACK) library offers a nice AdvancedLineSeriesStyleclass which allows me to quickly and easily add desired colors using gradient stops. Now the chart looks a little better:

Whilst quite a lot has changed visually the code to achieve this really is rather simple:

bpmSeries.style = AdvancedLineSeriesStyle().apply {
    areaLineColor = ContextCompat.getColor(context, R.color.colorBpmLine)
    areaColor = ContextCompat.getColor(context, R.color.colorBpmLine)
    fillStyle = SeriesStyle.FillStyle.GRADIENT
    addGradientStop(GradientStop.create(ContextCompat.getColor(context, R.color
            .colorBpmLow), 0.3f))
    addGradientStop(GradientStop.create(ContextCompat.getColor(context, R.color
            .colorBpmMedium), 0.6f))
    addGradientStop(GradientStop.create(ContextCompat.getColor(context, R.color
            .colorBpmHigh), 0.9f))
}

This code demonstrates one of the benefits of Kotlin over Java – its lack of verbosity.  Using Kotlin’s applyfunction reduces the number of lines needed to perform the work and in my opinion makes for a much more readable function.

As you can see the chart shows data from around 7 am through until 10 pm with some noticeable spikes in the morning, middle of the day and evening. These spikes represent my walk to work, a lunchtime run and my walk home from work. What is also apparent is the frequency of the data results in a lot of noise, which tends to blur the message which the data seeks to present. What is needed is some way of reducing the amount of data displayed but maintaining the shape of the data. ACK addresses this requirement with its sampling functionality:

As the above screen shot shows the sampling feature of ACK makes for much clearer data; a lot of the noise has been filtered, or sampled out. In particular, the sampler which wraps the chart’s DataAdapterreturns every 30th data point to the chart for display. Changing 1 line of code is all that is needed to achieve this:

bpmSeries.dataAdapter = NthPointSampler<Date, Double>(bpmDataAdapter, 30)

Whilst the shape of the data is more transparent, noise is still present in the form of sharp edges. To really make the data’s shape stand out I can smooth the shape of the line using ACK, as you can see below. ACK’s data smoothers achieve this by wrapping the DataAdapterthen adding additional synthetic data points to the series for display. The original data remains unchanged.

To add smoothing to the heart rate series only 1 line of code is needed:

bpmSeries.linePathInterpolator = CatmullRomSplineSmoother<Date, Double>(6)

The sampled and smoothed heart rate data is much clearer as the trend can be seen at a glance; I feel however the chart can be made even more useful. A typical metric associated with some activities is pace; it would be nice to show this on the chart. My wearable device captures this data as meters per second so this should be a good fit for the chart. On my device pace data is only recorded during the activity itself so unlike the heart rate data I cannot simply add a LineSeriesto cover the whole day.

The solution is to have a LineSeriesto represent each activity. Like before I’ve extracted the data to flat files for simplicity as real time data streaming is beyond the scope of this blog. The data is presented in a similar fashion to that previous; this time we have timestamp and meters per second data, such as:

1536060702113,2.743000030517578

This data point shows that at this point in time I had a pace of approximately 6.08 minutes per KM. To aid clarity I’ve also enabled the Legendto show what each series represents.

After adding the 3 pace series we see a chart like that above. You’ll notice I’ve added an additional y axis on the reverse, or right hand side of the chart. I’ve also added code which converts the m/s data into the more common pace reading. A noteworthy point is that I’ve negated each y value before it’s given to the DataAdapters then used an OnTickMarkDrawListenerto convert the negative tick mark labels back to positive values. My helper function convertLabelSign assists with this work. This is needed to ensure the pace data is displayed with tick marks that decrease rather than increase in value. You could of course omit this step but I feel the shape of the data would be incorrect; heart rate and pace typically have a similar rather than inverse relationship. Implementing the OnTickMarkDrawListener is straightforward, I first declare that my MainActivity class implements it using code such as:

class MainActivity: ShinobiChart.OnTickMarkDrawListener

I then implement the onDrawTickMarkfunction, using the ChartUtilsclass to draw the custom tick marks:

override fun onDrawTickMark(canvas: Canvas?, tickMark: TickMark?, labelBackgroundRect: Rect?,
                            tickMarkRect: Rect?, axis: Axis<*, *>?) {
    ChartUtils.drawTickMarkLine(canvas, tickMark)
    val x = labelBackgroundRect!!.centerX()
    val y = labelBackgroundRect.centerY()
    if (axis!! == shinobiChart.allYAxes[1]) {
        tickMark!!.labelText = convertLabelSign(tickMark.value as Double)
    }
    ChartUtils.drawText(canvas, tickMark!!.labelText, x, y, paceMajorTickLabelPaint)
}

By implementing this listener I effectively disable the chart’s built in tick mark drawing function. This listener function will be called for all axes, not just the pace axis. What I need then is to firstly reinstate the standard drawing function for the time (x) and heart rate (y) axes then bring in the custom behaviour for the pace (reverse y) axis. In the code above you can see that I use the ChartUtilsconvenience functions to draw the tick mark lines and labels. I check to see if the axis associated with the tick mark I am trying to draw is the reverse y. If it is I use the convertLabelSignhelper function to covert the sign, back to a positive value. Enabling this listener on the chart is very simple:

shinobiChart.setOnTickMarkDrawListener(this)


You can take a look at the code yourself here. ACK comes with comprehensive how-to guides which demonstrate in detail how to achieve the concepts discussed. You can find these in the download bundle or here.

In this blog I’ve shown how you can use ACK to enhance data visualised by shinobicharts, using Kotlin. I will take things a little further in part 2. We hope you will experiment by using shinobicharts and ACK within your own apps. If you have not already done so, why not download a free 30 day trial? If you have any questions feel free to get in touch.

 

TRUSTED BY ENTERPRISE