Android & Xamarin.Android Charting Documentation - SciChart Android Charts SDK v2.x
Tutorial 06 - Adding Realtime Updates

This article is a continuation of a tutorial series on how to create a chart using SciChart Android. So far we have focused on static charts where the data doesn't change. Assuming you have completed Tutorial 05 - Adding Tooltips and Legends, we will now make some changes to update the data dynamically.

Updating Data Values

In our DataSeries, we have some static data so far. Lets animate it now.

We are going to add a Timer and schedule updating the data on timer tick. To update data in a DataSeries, we have to call one of the available UpdateXXX methods on that DataSeries:

Copy Code
// What we have so far
final XyDataSeries lineData = sciChartBuilder.newXyDataSeries(Integer.class, Double.class).build();
final XyDataSeries scatterData = sciChartBuilder.newXyDataSeries(Integer.class, Double.class).build();

final int dataCount = 1000;
for (int i = 0; i < dataCount; i++)
{
    lineData.append(i, Math.sin(i * 0.1));
    scatterData.append(i, Math.cos(i * 0.1));
}

// New code below
// Set up an update
TimerTask updateDataTask = new TimerTask() {
    double _phase = 0.0;

    @Override
    public void run() {
        for(int i =0; i< dataCount;++i){
            lineData.updateYAt(i, Math.sin(i * 0.1 + _phase));
            scatterData.updateYAt(i, Math.cos(i * 0.1 + _phase));
        }
        _phase += 0.01;
    }
};

// Create a Timer and schedule updates at every 10 ms
Timer timer = new Timer();

long delay = 0;
long interval = 10;
timer.schedule(updateDataTask, delay, interval);

If you run the application now, you should see a chart with constantly updating series.

So far, so good. The only problem with this code is that it isn't very efficient. Calling an UpdateXXX method triggets chart update, and the entire chart redraws at once. Besides that, the values are passed in as Double objects, causing undesired boxing/unboxing and increasing number of objects to be removed during Garbage Collection.

There is a couple of suggestions on how to avoid such kind of issues and achieve the maximal performance with SciChart:

There is a special class for accumulating data called DoubleValues. It stores data in an array internally and allows accessing it without boxing/unboxing overhead. Also, SciChart provides the UpdateSuspender class which prevents a chart from being updated while a Runnable is being executed.

So our code has to be rewritten as follows:

Copy Code
final XyDataSeries lineData = sciChartBuilder.newXyDataSeries(Integer.class, Double.class).build();
final XyDataSeries scatterData = sciChartBuilder.newXyDataSeries(Integer.class, Double.class).build();

final int dataCount = 1000;
for (int i = 0; i < dataCount; i++)
{
    lineData.append(i, Math.sin(i * 0.1));
    scatterData.append(i, Math.cos(i * 0.1));
}

// New code below
// Set up an update
final DoubleValues lineDoubleData = new DoubleValues(dataCount);
final DoubleValues scatterDoubleData = new DoubleValues(dataCount);
lineDoubleData.setSize(dataCount);
scatterDoubleData.setSize(dataCount);

TimerTask updateDataTask = new TimerTask() {
    private double _phaseShift = 0.0;
    @Override
    public void run() {
        UpdateSuspender.using(surface, new Runnable() {
            @Override
            public void run() {
                // Fill the DoubleValues collections
                for (int i = 0; i < dataCount; i++)
                {
                    lineDoubleData.set(i, Math.sin(i * 0.1 + _phaseShift));
                    scatterDoubleData.set(i, Math.cos(i * 0.1 + _phaseShift));
                }
                // Update DataSeries using bunch update
                lineData.updateRangeYAt(0, lineDoubleData);
                scatterData.updateRangeYAt(0, scatterDoubleData);
            }
        });
        _phaseShift += 0.01;
    }
};

Timer timer = new Timer();
long delay = 0;
long interval = 10;
timer.schedule(updateDataTask, delay, interval);

Also, we should mention here that it is worth rewriting initial call to the append() method in the same way. It will result in a noticeable decrease of the load time in case of large data amount.

Finally, you should end up with a chart looking like this:

 

Adding New Data Values

The DataSeries class declares a bunch of append() methods. You should call a suitable one and pass new data as a param into it to add the data to a DataSeries. The code from above can be rewritten as follows to append new data constantly to the DataSeries:

Copy Code
final XyDataSeries lineData = sciChartBuilder.newXyDataSeries(Integer.class, Double.class).build();
final XyDataSeries scatterData = sciChartBuilder.newXyDataSeries(Integer.class, Double.class).build();

// New code below
// Add new data on update       
TimerTask updateDataTask = new TimerTask() {
    @Override
    public void run() {
        UpdateSuspender.using(surface, new Runnable() {
            @Override
            public void run() {
                int x = lineData.getCount();

                lineData.append(x, Math.sin(x * 0.1));
                scatterData.append(x, Math.cos(x * 0.1));

                // Zoom series to fit the viewport
                surface.zoomExtents();
            }
        });
    }
};

Timer timer = new Timer();
long delay = 0;
long interval = 10;
timer.schedule(updateDataTask, delay, interval);

The resulting chart should look very much like this:

 

Scrolling Realtime (FIFO) Charts

What if you wanted to scroll as new data was appended? You have a few choices.

We're going to show you both techniques below.

FIFO Scrolling Charts (Discarding Old Data)

The most memory efficient way to achieve scrolling is to call setFifoCapacity() on a DataSeries to set the maximum size of a DataSeries before old points are discarded. DataSeries in FIFO mode act as a circular (first-in-first-out) buffer. Once the capacity is exceeded, old points are discarded. You cannot zoom back to see the old points, once they are lost, they are lost.

To make a DataSeries use the FIFO buffer, you have to set fifo capacity on the DataSeries, which defines the size of the FIFO buffer. The code from our application should be rewritten like the following to utilize a FIFO buffer:

Copy Code
// Set FIFO capacity to 500 on DataSeries
final int fifoCapacity = 500;
final XyDataSeries lineData = sciChartBuilder.newXyDataSeries(Integer.class, Double.class)
        .withFifoCapacity(fifoCapacity)
        .build();
final XyDataSeries scatterData = sciChartBuilder.newXyDataSeries(Integer.class, Double.class)
        .withFifoCapacity(fifoCapacity)
        .build();

TimerTask updateDataTask = new TimerTask() {
    private int x = 0;
    @Override
    public void run() {
        UpdateSuspender.using(surface, new Runnable() {
            @Override
            public void run() {
                lineData.append(x, Math.sin(x * 0.1));
                scatterData.append(x, Math.cos(x * 0.1));

                // Zoom series to fit the viewport
                surface.zoomExtents();

                ++x;
            }
        });
    }
};

Timer timer = new Timer();
long delay = 0;
long interval = 10;
timer.schedule(updateDataTask, delay, interval);

Hopefully, the code explains itself. As a quick walckthrough, we now set FifoCapacity on the DataSeries. Then, after apending new data we call zoomExtents() to make series to fit the viewport in Y direction.

This should be the result when you run the application:

 

See Also