So far we have focused on static charts. Assuming you have completed Tutorial 05 - Adding Tooltips and Legends, now we will show how to create live charts.
Adding New Data Values
We change the way we populate lineData and scatterData by spinning up a new thread and using java.util.Timer to update it in the background.
Copy Code | |
---|---|
final XyDataSeries lineData = sciChartBuilder.newXyDataSeries(Integer.class, Double.class).build(); final XyDataSeries scatterData = sciChartBuilder.newXyDataSeries(Integer.class, Double.class).build(); // 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); |
Making the Code More Efficient
This code works, but it isn't very efficient:
- Calling an UpdateXXX method triggers a chart update, which redraws the entire chart.
- The values are passed in as Double objects, which require boxing/unboxing, which increases the number of objects to be removed during Garbage Collection.
Here is how to improve that:
- Always append or update data in a DataSeries in batches instead of one at a time.
- Suspend updates on a SciChartSurface using UpdateSuspender to prevent redrawing until you have updated the whole DataSeries.
- Use the class DoubleValues. It stores data in an array internally and requires not require boxing/unboxing.
So we rewrite the code 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)); } 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 that it is worth rewriting initial call to the append() method the same way. This will result in a noticeable decrease in load time in when there is a large amount of data.
Finally, you should end up with a chart looking like this:
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.
- If you want to be memory efficient, and you don't mind if you discard old data, you can use our FIFO (first-in-first-out) functionality.
- If you want to preserve old data, you can simply update the VisibleRange of an axis.
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); |
The code for the most part is self-explanatory. But we can point that that we 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: