Thumbnail1

Back to Blog

Building a streaming chart in Android

Posted on 3 Feb 2014 Written by Alison Clarke

This tutorial is going to take you step-by-step through building a simple streaming chart using ShinobiCharts for Android. The chart uses the motion sensor in the Android device and plots its accelerometer readings. For the purposes of this tutorial we’ll assume you’re using Eclipse with the Android Development Tools plugin.

If you get stuck, you can get the finished code from GitHub, or download the zip.

Screenshot03

Setting things up

Before you start, you’ll need a copy of ShinobiCharts for Android – if you don’t have one, then download a trial version. If this is the first time you’ve used ShinobiCharts for Android, you’ll need to import it into Eclipse as an Android Library Project and then reference it from your own application, as follows:

  1. Open up Eclipse and make sure you’ve unzipped the download bundle.
  2. In Eclipse, click File > New > Other… > Android Project from Existing Code then click Next
  3. Set the root directory to the shinobicharts-android-library folder in the unzipped ShinobiCharts bundle
  4. Ensure the shinobicharts-android-library project is ticked and click Finish (you can tick “Copy projects into workspace” if you like)
  5. A new project named shinobicharts-android-library should appear in the Project Explorer. Right-click on it and select Properties
  6. Select Android from the list on the left, and tick the Is Library option
  7. Click Apply and then OK

Creating the project

In Eclipse, create a new Android Application Project, setting the Minimum Required SDK to API 12: Android 3.1 (Honeycomb), and creating a blank Activity called AccelerometerActivity with navigation type “None”.

To add a reference to ShinobiCharts, right-click on your newly created project, select Properties, go to the Android section, then click the Add button. Choose shinobicharts-android-library and click OK, then click OK to save and exit the Properties dialog.

Before we go any further, let’s change the settings so that the app will only run in portrait mode. This will mean we don’t need to worry about orientation changes. (If we were dealing with static data, the ChartFragment would cope with saving the data for us, but as we’re generating it on the fly, we would have to do extra work to keep hold of the data, which is beyond the scope of this tutorial.)

So, open up AndroidManifest.xml and edit the activity section to add in the android:screenOrientation attribute as follows:

<activity
    android:name="com.example.accelerometertest.AccelerometerActivity"
    android:label="@string/app_name" 
    android:screenOrientation="portrait" >
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

Creating a chart

The first step to creating a chart is to add a ChartFragment to your activity. Open up the XML layout file for your activity, which can be found in the res/layout/ folder. Click on the tab along the bottom called activity_accelerometer.xml (or whatever your XML file is called) to show the raw XML, then replace the TextView with a ChartFragment:

<fragment
    class="com.shinobicontrols.charts.ChartFragment"
    android:id="@+id/chart"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

This creates a ChartFragment with id “chart”, which will match the dimensions of its parent.

Now open up AccelerometerActivity.java which is in a package under the src/ folder. We’re going to need a private variable to store our chart, so add the following at the top of the class:

public class AccelerometerActivity extends Activity {

	private ShinobiChart shinobiChart;

	...
}

We want to set up the chart the first time the activity is created, so modify the onCreate() method to look like this:

@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_accelerometer);
		
	// Get the a reference to the ShinobiChart from the ChartFragment
        ChartFragment chartFragment =
                (ChartFragment) getFragmentManager().findFragmentById(R.id.chart);
        shinobiChart = chartFragment.getShinobiChart();
        
        // Only set the chart up the first time the Activity is created
        if (savedInstanceState == null) {
		// Uncomment this line to set the license key if you're using a trial version
		//shinobiChart.setLicenseKey("<license_key_here>");
			
		// Set the chart title
		shinobiChart.setTitle("Accelerometer Visualisation");
            
		// Create the axes, set their titles, and add them to the chart
		NumberAxis xAxis = new NumberAxis();
		xAxis.setTitle("Elapsed time (s)");
		shinobiChart.setXAxis(xAxis);
		NumberAxis yAxis = new NumberAxis();
		yAxis.setTitle("Acceleration (m/s^2)");
		shinobiChart.setYAxis(yAxis);
        }
}

Let’s take a look at that code in more detail. The first thing we do is to grab the ChartFragment that we just set up in the XML, then get hold of its ShinobiChart object. Next, we check for savedInstanceState == null. This checks whether the activity is being created for the first time: a ChartFragment is retained across activity recreation, so we only need to do the setup once.

Now let’s look inside the if block. If you’re using a trial version, you’ll need to add in your license key, so uncomment the next line and add in your key. We then set the chart’s title,  and create some axes, setting their titles and adding them to the chart. Our graph will plot acceleration over time, but to keep things simple in this example, our x-axis will be a number axis rather than a date time axis, and will plot the number of seconds elapsed since the app started.

You should now be able to run the app, and see an empty chart area:

 Screenshot01

You’ll probably see some warnings in LogCat at this point that the axes can’t be displayed. That’s because we haven’t added any data yet, or set ranges on the axes.

Setting up the accelerometer

The data we’re going to plot will come from the motion sensors provided by Android. If you want to read more about them then have a look at the Android API Guide, but I’ll try to explain what you need to know as we go along.

First we need some private variables to hold a sensor manager and an accelerometer sensor. Add these to the top of the class:

private SensorManager sensorManager;
private Sensor accelerometer;

Next, we need to assign objects to these variables. Add the following to the top of onCreate:

@Override
protected void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	setContentView(R.layout.activity_accelerometer);
		
	// Get hold of the sensor manager and accelerometer sensor
	sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
	accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
	...
}

To respond to changes in the sensor, we’ll need an implementation of the SensorEventListener interface, so let’s make our AccelerometerAcvitity class implement the interface:

public class AccelerometerActivity extends Activity implements SensorEventListener {

Eclipse will now give you an error message, because we haven’t implemented the required methods of SensorEventListener, so right-click on the error message, select “Quick Fix”, and double-click on “Add unimplemented methods”. This will generate method stubs for onAccuracyChanged and onSensorChanged. We’ll fill those in shortly.

The final step to set up the motion sensor is to register AccelerometerActivity as an event listener on the our accelerometer when the app starts or resumes, and unregister it when the app pauses. We do this by overriding the activity’s onResume and onPause methods:

@Override
public void onResume()
{
	super.onResume();
	// Register ourselves as an event listener for accelerometer events
	sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_NORMAL);
}
	
@Override
public void onPause()
{
	super.onPause();
	// Unregister the event listener for accelerometer events
	sensorManager.unregisterListener(this);
}

Adding data to the chart

Now we’ve got our accelerometer set up, let’s add some data adapters to provide data to the chart. We’re going to have 3 line series on our chart, to display accelerometer data in 3D space. We’ll need a data adapter for each series, so let’s store the adapters in a private ArrayList:

private ArrayList<SimpleDataAdapter<Double, Float>> dataAdapters;

We’re using the SimpleDataAdapter provided with ShinobiCharts, and each data point will consist of a Double x value (elapsed time) and a Float y-value (acceleration).

Now let’s create our data adapters and line series. Add the following to onCreate:

@Override
protected void onCreate(Bundle savedInstanceState) {
	...

        if (savedInstanceState == null) {
		...

 		// Create arrays to hold the data adapters, and the axis titles
		dataAdapters = new ArrayList<SimpleDataAdapter<Double, Float>>();
		String[] seriesTitles = {"x-axis", "y-axis", "z-axis"};
			
		// Create a data adapter and a line series to represent each of the x, y and z axes
		for (int i=0; i<3; i++)
		{
			// Create a data adapter
			dataAdapters.add(new SimpleDataAdapter<Double, Float>());
				
			// Create a line series
			LineSeries lineSeries = new LineSeries();
			// Set its title
			lineSeries.setTitle(seriesTitles[i]);
		        // Style it to have a gradient fill beneath the line
			lineSeries.getStyle().setFillStyle(FillStyle.GRADIENT);
			// Set its data adapter
		        lineSeries.setDataAdapter(dataAdapters.get(i));
		        
		        // Add the series to the chart
		        shinobiChart.addSeries(lineSeries);
		}			
        }
}

Here we initialise the dataAdapters array, and create an array of titles for our series. (Note that “axis” in this context refers to the coordinates returned by the accelerometer, rather than to our chart axes.)

Next, we create the data adapters and line series in a loop. We add a title to each line series, and give it a gradient fill, then set its data adapter, before adding it to the chart.

Now we’ve got our data adapters and our accelerometer, we need to hook them together. We can do this in the onSensorChanged method we stubbed out earlier. This method gets called whenever the accelerometer values change. We need to take the new values and add them to our data adapters.

To calculate the elapsed time we’ll need a starting point, so create another private instance variable:

private long startTimestamp = 0;

Now modify the previously auto-generated method onSensorChanged as follows:

@Override
public void onSensorChanged(SensorEvent event) {
	// If this is the first event, save its timestamp to use as the base value
	if (startTimestamp == 0) {
		startTimestamp = event.timestamp;
	}
	
	// Calculate the elapsed time in seconds (from the nanosecond timestamps)
	double elapsedTime = (event.timestamp - startTimestamp)/1000000000.0;
	
	// event.values for an accelerometer event contains 3 values, one for each of the x, y and z axes
	// Create a data point for each of the values, and add it to the relevant DataAdapter
	for(int i=0; i<3; i++) {
		// Check we've got a data adapter before attempting to add a point
		if (dataAdapters != null && dataAdapters.get(i) != null) {
			// Create a data point for the value, and add it to the relevant DataAdapter
			DataPoint<Double,Float> dataPoint = new DataPoint<Double,Float>(elapsedTime, event.values[i]);
			DataAdapter<Double,Float> dataAdapter = dataAdapters.get(i);
			dataAdapter.add(dataPoint);
			
			// Remove any data points from more than 20s ago
			DataPoint<Double,Float> oldPoint = (DataPoint<Double,Float>) dataAdapter.get(0);
			while (oldPoint.getX() < elapsedTime - 20)
			{
				dataAdapter.remove(0);
				oldPoint = (DataPoint<Double,Float>) dataAdapter.get(0);
			}
		}
	}
	
	// Finally, redraw the chart to display the new values
	shinobiChart.redrawChart();
}

The first time this method is called, the value of startTimestamp is set to the event’s timestamp. This value is then used to calculate the elapsed time (converting from nanoseconds to seconds). Next, because the accelerometer event’s values array will contain 3 items, respresenting the acceleration in 3 dimensions, we loop 3 times, creating a data point for each value and adding it to the corresponding data adapter. (We do a safety check that the data adapter exists before trying to do anything with it, in case the activity has been recreated for any reason.) The last part of the loop prevents the graph from showing more than 20s worth of data (so it doesn’t get too squashed), by removing any data points from more than 20s ago. Finally, the redrawChart method is called to redraw the chart with the new values.

You should now be able to run the app, move the device around, and watch the movements being plotted.

 Screenshot02

Finishing touches

We’ve got our chart, but it would be nice to know what the three different colours represent, so let’s add a legend. This is done in onCreate, after setting up the data:

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...		
    if (savedInstanceState == null) {
        ...
			
        // Get the legend from the chart
        Legend legend = shinobiChart.getLegend();
        // Make the legend visible
        legend.setVisibility(View.VISIBLE);
        legend.setPosition(Legend.Position.BOTTOM_CENTER);
        
        // Change the legend's style
        LegendStyle legendStyle = legend.getStyle();
        legendStyle.setPadding(10.0f);
        legendStyle.setSymbolLabelGap(2.0f);
        legendStyle.setSymbolWidth(15.0f);
        legendStyle.setTextSize(13.0f);
    }
}

First, we get the legend from the chart object, and set it to be visible. We also position it at the bottom of the chart area. Next, we style the legend so it fits nicely in the available space, adjusting the spacing between the symbols and labels, and the sizes of the symbols and labels. 

If you run the app now, you’ll see the legend at the bottom of the chart:

 Screenshot03

Performance tips

This app works pretty well on newer devices, but you might notice some sluggish behaviour on older devices. Here are a couple of things you could try to improve the performance:

  • Change the rate at which sensor events are delivered from the accelerometer, by adjusting the rate parameter to the call to registerListener in onResume – see the documentation for more details.
  • Write a custom data adapter which doesn’t update the chart on every added point, but updates when it has X new points, or every Y seconds – see the CustomDataAdapter sample in the shipped code and read the how-to guide to find out how to do this.

 

So there we have it: a streaming chart. If you got stuck at any point, the finished code is available on GitHub, or you can download the zip. Let us know if you have any questions!

Back to Blog