I'm currently using achartengine to display real-time data received using bluetooth. The data is correctly collected on a specific thread and sent ever 100ms in a Bundle to my main activity.
The main activity contains 4 charts from the achartengine library (LineChart class), added to its view using GraphicalView
To summarize, every 100ms, my main activity receives a callback and
When calling repaint, the datasets won't be updated until the next bundle arrives from the other thread, 100ms later. I assume 100 ms should be more than enough to redraw the views.
I post here a small version of the class that holds the datasets and view, and parts of the main activity where it is instanciated and used
/** This class contains the data and renderer used to draw a chart */
public class LineChartData
{
public static enum Series {X, Y, Z};
public static final int MAX_NB_VALUES_PER_SERIE = 1000;
private XYMultipleSeriesDataset mDataset = new XYMultipleSeriesDataset();
private XYMultipleSeriesRenderer mRenderer = new XYMultipleSeriesRenderer();
private int[] mColors = new int[]{Color.RED, Color.GREEN, Color.BLUE};
private GraphicalView mChartView;
private boolean mFollow = true;
private boolean mTouchedDown = false;
public LineChartData(Context context, String title, String[] seriesNames)
{
// Since we save only 3 colors, the programm will not support more than 3 series per chart
for(int i = 0; i < seriesNames.length; ++i)
{
XYSeries serie = new XYSeries(seriesNames[i].toString());
mDataset.addSeries(serie);
XYSeriesRenderer serieRenderer = new XYSeriesRenderer();
serieRenderer.setColor(mColors[i]);
mRenderer.addSeriesRenderer(serieRenderer);
}
mRenderer.setMargins(new int[] { 20, 30, 0, 0 });
mRenderer.setLegendHeight(50);
//mRenderer.setShowLegend(true);
mRenderer.setChartTitle(title);
mRenderer.setMarginsColor(Color.WHITE);
mRenderer.setXAxisMax(SettingsActivity.DEFAULT_DISPLAY_RANGE);
mRenderer.setXAxisMin(0);
mChartView = ChartFactory.getLineChartView(context, mDataset, mRenderer);
mChartView.repaint();
}
public void addData(int serieId, double x, double y)
{
mDataset.getSeriesAt(serieId).add(x, y);
}
public void repaint()
{
mChartView.invalidate();
}
public void updateDisplayWindow(int displayRange)
{
if(!mFollow)
{
return;
}
double maxX = mDataset.getSeries()[0].getMaxX();
if(maxX > displayRange)
{
mRenderer.setXAxisMax(maxX);
mRenderer.setXAxisMin(maxX-displayRange);
}
else
{
mRenderer.setXAxisMax(displayRange);
mRenderer.setXAxisMin(0);
}
}
public void deleteOldestValues()
{
XYSeries[] series = mDataset.getSeries();
for(XYSeries serie : series)
{
while(serie.getItemCount() > MAX_NB_VALUES_PER_SERIE)
{
serie.remove(0);
}
}
}
}
Creating the 4 charts:
protected void onCreate(Bundle b)
{
mAccelChartView = new LineChartData(this, getString(R.string.Accel), new String[]
{getString(R.string.xAxis), getString(R.string.zAxis), getString(R.string.zAxis)});
mGyroChartView = new LineChartData(this, getString(R.string.Gyro), new String[]
{getString(R.string.xAxis), getString(R.string.zAxis), getString(R.string.zAxis)});
mPressureChartView = new LineChartData(this, getString(R.string.Pressure), new String[]
{getString(R.string.pressureAxis)});
mEcgChartView = new LineChartData(this, getString(R.string.ECG), new String[]
{getString(R.string.ecgAxis)});
LinearLayout layoutAccel = (LinearLayout) findViewById(R.id.accelChart);
LinearLayout layoutGyro = (LinearLayout) findViewById(R.id.gyroChart);
LinearLayout layoutPressure = (LinearLayout) findViewById(R.id.pressureChart);
LinearLayout layoutEcg = (LinearLayout) findViewById(R.id.ecgChart);
layoutAccel.addView(mAccelChartView.getView(), 0);
layoutGyro.addView(mGyroChartView.getView(), 0);
layoutPressure.addView(mPressureChartView.getView(), 0);
layoutEcg.addView(mEcgChartView.getView(), 0);
}
Updating the charts
private void updateViewUsingMessage(String action, Bundle newData)
{
if(action.equals(BluetoothCollecterThread.MSG_UPDATE_CHART))
{
addFromBundle(newData, mLogger.isLogging());
mAccelChartView.updateDisplayWindow(mDisplayRange);
mAccelChartView.deleteOldestValues();
mGyroChartView.updateDisplayWindow(mDisplayRange);
mGyroChartView.deleteOldestValues();
mPressureChartView.updateDisplayWindow(mDisplayRange);
mPressureChartView.deleteOldestValues();
mEcgChartView.updateDisplayWindow(mDisplayRange);
mEcgChartView.deleteOldestValues();
mAccelChartView.repaint();
mGyroChartView.repaint();
mPressureChartView.repaint();
mEcgChartView.repaint();
}
}
It might seem big but it's actually quite easy. I have two problems with this, I don't know if thay are linked: Firstly, several times per seconds, the first serie from one chart is drawn on another chart (see pictures below). I checked my code and couldn't find any solution. Moreover, it really seems to happen at random so it looks more like a concurrent issue comming from the achartengine library.
This bring my second issues. The log shows way too many messages from the garbage collector, especially messages of type WAIT_FOR_CONCURRENT_GC blocked, which is obviously bad.
I found this link: http://stackoverflow.com/questions/14187716/is-achartengine-ready-for-realtime-graphing suggest that memory allocation problems remains in achartengine, but that the last version should be sufficient to display a few graphs with 1000 points each in real time. Even when I set the data update to 1 second instead of 100ms I still get the visual glitch
The correct behaviour: 4 charts updated in real time (two first with 3 series, two last with 1 serie) http://s903.photobucket.com/user/bperreno/media/correct_zpsebd731f6.png.html?sort=3&o=1
Wrong: the red serie from chart 2 is displayed on the other charts http://s903.photobucket.com/user/bperreno/media/wrong1_zpsd33eec83.png.html?sort=3&o=0
The data is moving in realtime, but from time to time, the glitch shown in the second picture happen, juste for a frame or two, and then disapear. Does anyone have an idea how to, at least, fix the display problem? and maybe hopefully tell me how to solve the overuse of garbage collection from achartengine
Thanks a lot!
EDIT 04.12.2013 I'm now using the achartengine sources instead of the .jar so I'm able to look at the values. After seeing that "setInScroll" didn't work, I took a look in the code. In GraphicalView.onDraw method, I compared the values used when setInScroll is true or false.
int top = mRect.top;
int left = mRect.left;
int width = mRect.width();
int height = mRect.getheight();
if(mRenderer.isInScroll())
{
top = 0;
left = 0;
width = getMeasuredWidth();
height = getMeasuredHeight();
}
The values are always the same wither comming from mRect, or from getMeasuredXXX(). top and left are zeros in both cases. So (in this case at least), inScroll = true has no effect
Ok, I'm answering my question:
After a few test, I found out that it is crlearely a concurent problem that occures when too many operations are performed on the achartengine lib.
The heavy operations seem to include
So the best I could do to lessen the problems was to set the X max position further away, so I only have to update it once in a while (eg. once per second)
Adding a method to the library allowing to delete many points at once could also be of great use. A deletion is currently O(n) and deleting m points one after the other is thus O(nm).
Note that more powerful devices than my galaxy S2 (like the S4mini) handle the display without glitch. But the efficiency remains an issue for more graphs and higher frequency.
reaching 10 FPS for 4 graphs isn't really what I would call real time