charts-in-listviews3

Back to Blog

Charts in ListViews – Part 3: Streaming Data

Posted on 9 Mar 2015 Written by Joel Magee

This post is part of a series describing how to add ShinobiCharts to your ListView in Android. You can read this post independently but it might help to read part 1 and part 2 first if you haven’t already done so.


As you work through the tutorial you might want to take a look at our sample project: you can browse the code on GitHub, or download the zip. You’ll also need a copy of our Android Charts to follow the tutorial – if you don’t have one, then download a trial version.

Finished example

Over the last two blog posts we have managed to create a demo app that displays charts in a list view and connected them with panning gestures. The final progression in this series will be to allow the streaming of data to the charts.

Those of you that have gone through our how to guides will be aware that we can create a custom data adapter and we will be doing something similar in this demo app.

Dynamically updating data

We’re going to simulate streaming data by utilising the Runnable interface to periodically add to and remove data from our charts. The first thing that we do to achieve this is to implement the Runnable interface on our ListActivity:

public class ChartListActivity extends ListActivity implements Runnable {

    ...

    @Override
    public void run() {

    }

}

Now if you add a call to run at the end of the onResume method block then that Runnable will start when the app becomes visible. At the moment nothing is being executed in that method so we need to fill it out with the desired logic. In our case we only want the last 20 data points, so when a new one comes in the oldest one goes out.

To achieve this we iterate over each DataAdapter in the list and add a new DataPoint which will get added to the end of the line series. Then we remove the first DataPoint in that DataAdapter.

Once we have finished doing this for each DataAdapter, we need to make sure that this Runnable will be called over and over again. Calling postDelayed will add this Runnable to the message queue to be called after the amount of specified time. We could create a Handler to call postDelayed on, but instead we call it on a View (in our case the ListView) to ensure that it is run on the ui thread. Now that we are adding this runnable to the message queue we need to be able to clean up when the app pauses. To do this we call removeCallbacks(this) in the onPause method on the ListView which will prevent any further calls to this Runnable.

@Override
public void run() {
    for (DataAdapter dataAdapter : dataAdapters) {
        dataAdapter.add(new DataPoint<Integer, Integer>(((Integer) dataAdapter.get(dataAdapter.size() - 1).getX()) + 1, (int) (Math.floor(Math.random() * Y_AXIS_RANGE))));
        dataAdapter.remove(0);
    }

    getListView().postDelayed(this, DELAY);
}

Displaying the updated data

Adding these changes in will allow the data to be periodically updated however there are still a few issues that we need to iron out before this is complete. When we add to or remove data from our DataAdapter, the displayed range will go back to either the default or the data range. For our usage we don’t want this to happen so we need to add a line to the createXAxis method in the ChartArrayAdapter. If we set setCurrentDisplayedRangePreservedOnUpdate to true for our X axis then the displayed range won’t change when we are adding and removing data.

Adding this line in solves our visual range from expanding to the size of the dataset but now it doesn’t follow the dataset. To make sure that it does we need to loop through the visible charts, the same way we do in the onPause and onResume methods, and request to change the current visible range. We just work out the difference between the minimum of both the visible and the data range and add that on to the visible range.

@Override
public void run() {

    ...


    int start = getListView().getFirstVisiblePosition();
    int end = getListView().getLastVisiblePosition();
    ChartArrayAdapter.ViewHolder viewHolder;

    for (int i = start; i <= end; i++) {
        viewHolder = (ChartArrayAdapter.ViewHolder) getListView().getChildAt(i - start).getTag();
        ShinobiChart chart = viewHolder.chart.getShinobiChart();

        NumberAxis xAxis = (NumberAxis) chart.getXAxis();
        NumberRange xRange = (NumberRange) xAxis.getCurrentDisplayedRange();
        NumberRange xDataRange = (NumberRange) xAxis.getDataRange();
        final double delta = xDataRange.getMinimum() - xRange.getMinimum();
        xAxis.requestCurrentDisplayedRange(xRange.getMinimum() + delta, xRange.getMaximum() + delta, false, false);
    }

    ...

}

Extra Optimisations

Although the app performs fairly well there is another slight optimisation that we can add. Currently we are using a SimpleDataAdapter to store the data points for each chart which will fire an update every time you either add or remove a DataPoint. We are adding a new point to the end of the adapter and removing one from the beginning which will fire two updates, however it is all part of the same transaction so we can get away with only firing one update.

To do this we create our own custom data adapter that extends the abstract DataAdapter. This doesn’t fire any updates at all when modifying the data; the responsibility for this is left up to its subclasses. In this subclass we add a method to our custom adapter that will allow us to choose when to fire an update. This is achieved by calling the DataAdapter’s protected method fireUpdateHandler.

private static class FireableDataAdapter<X, Y> extends DataAdapter<X, Y> {

    public void fire() {
        fireUpdateHandler();
    }
}

To use this new method we need to change the type of DataAdapter we create to be a FireableDataAdapter. Then, after adding the new and removing the old data point, call the fire method. We have now halved the number of updates that are being fired.

Conclusion

With that final change we have our completed demo app. Over the course of the last three posts we have progressed from a rather basic list of charts to something which is much more dynamic. We have also demonstrated a few techniques which have allowed us, at various stages, to optimise the way our app works.

Joel

Back to Blog