MPAndroidChartを使って「リアルタイム更新のセンサーデータ時系列グラフ」のサンプルを作ってみた

BoothLowEnergyの電波強度の測定アプリを作る際に「JFreeChart以外で何か良いグラフ描画ライブラリがないだろうか？」って思って見つけた「MPAndroidChart」を使ってサンプルアプリを作った話です。ネット上では日本語でこのライブラリについて書いているドキュメントはいくつかあるのですが、リアルタイム更新について書かれたものは見つかりませんでした。（MPAndroidChartについて書かれた日本語のブログは一番下の「参考サイト」で紹介します。）なので、リアルタイム更新について取り扱った記事を書きます！
このグラフで何ができるか？リアルタイムでグラフを更新していくにはどうしたらいいか？というのをつかんでいただけたら幸いです。

# MPAndroidChartとは?
Philipp Johada氏が作ったAndroid用グラフ描画ライブラリです。（2015/12/30時点での最新バージョンは2.1.6）
https://github.com/PhilJay/MPAndroidChart

MPAndroidChartを使えば以下のことができます。
・折れ線グラフ、棒グラフ、円グラフ、箱ひげ図、レーダーチャートなど数多くのグラフの描画。
・ピンチインによるグラフの拡大・縮小、グラフの表示領域のスクロール。
・X座標・Y座標のスタイル・最大値・グリッドなどきめ細かい調整。
・凡例の表示位置・スタイルなどのきめ細かい調整。
・**動的にグラフの中身の更新**

Apache2.0で公開されているので気軽にプロダクトに取り込むことができます。

# 今回作ったリアルタイム更新のセンサーデータ時系列グラフサンプル
「センサー開始」ボタンをタップしたら、波形グラフの点がどんどん描画されていくようなアプリです。(0.5秒おきに点が追加されていきます)

![device-2015-12-30-172922.png](https://qiita-image-store.s3.amazonaws.com/0/14989/7127b2a2-1420-0269-077a-21afecb80bae.png)

このアプリで使ったMPAndroidChartはv2.1.6です。

このアプリのソースはGithubにあげていますので、ダウンロードして動かしてみてください。
https://github.com/LyricalMaestro/MPAndroidChartSample

# MPAndroidChartをプロジェクトに取り込む
MPAndroidChartをプロジェクトに取り込むにはappモジュール直下のbuild.gradleに以下のコードを追記します。

```groovy:build.gradle
android{
   repositories {
       maven { url "https://jitpack.io" }
   }
   // 中略
}

dependencies {
    compile 'com.github.PhilJay:MPAndroidChart:v2.1.6'
}
```
そのあと、GradleSyncを行えばOKです。

# MPAndroidChartを使った実装解説

今回実装の際に参考にしたソースは[RealtimeLineChartActivity.java](https://github.com/PhilJay/MPAndroidChart/blob/master/MPChartExample/src/com/xxmassdeveloper/mpchartexample/RealtimeLineChartActivity.java)になります。これを踏まえて解説していきます。

## MPAndroidChart周りのクラス構成
今回のサンプルアプリを作るのに必要なクラスとその関連について以下にまとめます。
![スクリーンショット 2015-12-30 18.06.43.png](https://qiita-image-store.s3.amazonaws.com/0/14989/0439c39a-104d-41e8-ea8f-6591ffcfa620.png)

各クラスについての説明は以下の通りです。
・LineChart:グラフを描画するためのビュー。
・LineData:グラフ描画対象のモデルクラス。1つのグラフに複数本折れ線を描くためにLineDataSetをn個参照できるようになっている。（が、今回は1本しかかかないのでnのところは常に1。）
・LineDataSet:折れ線一本に対応するモデル。
・Entry：節一つ一つに対応する。上のキャプチャで示しているサンプルアプリでは水色のドッドに対応する（青い線上にのっています）

## Chartの初期化
Activity起動時に行います。上記のER図でいうChart, LineDataの初期化を行います。

### レイアウト
Activityでセットするlayout.xmlは以下のように書きます。LineChartをレイアウトに組み込むのに特殊なことはしなくてもOKです。

```xml:layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.lyricaloriginal.mpandroidchartsample.MainActivity">

    // 中略

    <com.github.mikephil.charting.charts.LineChart
        android:id="@+id/chart"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

</LinearLayout>
```

### コード上でのChartの設定

Activity初期化時にChartのX軸・Y軸・凡例などの初期化を行っています。

```java:Chartの初期化(サンプルアプリから抜粋)
        // enable touch gestures
        mChart.setTouchEnabled(true);

        // enable scaling and dragging
        mChart.setDragEnabled(true);
        mChart.setScaleEnabled(true);
        mChart.setDrawGridBackground(false);

        // if disabled, scaling can be done on x- and y-axis separately
        mChart.setPinchZoom(true);

        // set an alternative background color
        mChart.setBackgroundColor(Color.LTGRAY);

        LineData data = new LineData();
        data.setValueTextColor(Color.BLACK);

        // add empty data
        mChart.setData(data);

        //  ラインの凡例の設定
        Legend l = mChart.getLegend();
        l.setForm(Legend.LegendForm.LINE);
        l.setTextColor(Color.BLACK);

        XAxis xl = mChart.getXAxis();
        xl.setTextColor(Color.BLACK);
        xl.setLabelsToSkip(9); 

        YAxis leftAxis = mChart.getAxisLeft();
        leftAxis.setTextColor(Color.BLACK);
        leftAxis.setAxisMaxValue(3.0f);
        leftAxis.setAxisMinValue(-3.0f);
        leftAxis.setStartAtZero(false);
        leftAxis.setDrawGridLines(true);

        YAxis rightAxis = mChart.getAxisRight();
        rightAxis.setEnabled(false); 
```

コードを見て、前半については各メソッドがどんな設定をしているか察しがつくと思いますが、凡例・X軸・Y軸についてはピンとこないものもあるのでそれを解説します。

・`xl.setLabelsToSkip(9)`
X軸につけるEntry(ここでいう点)に対するラベルの間隔を指定しています。「Entry N個とばしでつける」というような指定をします。
MPAndroidChartのX軸はデフォルトでEntryごとにX軸のラベルをつけるようになっていますが、Entryが多くなるとX軸のラベルが非常に読みにくくなるのでこの場合はあえて「5秒おき」という意味を込めてつけています。

・`leftAxis.setAxisMaxValue`,`leftAxis.setAxisMinValue`,`leftAxis.setStartAtZero`
Y軸の表示範囲を指定しています。
MaxValueとMinValueの設定がないと設定されているLineDataSet内のEntryたちを基に計算してしまいます。静的なグラフの場合はこれで問題ないと思いますがリアルタイムにデータが追加されていくようなものだとデータ追加の旅にY軸の表示範囲が変わってしまうのでグラフとしてはみずらくなります。
また、`setStartAtZero`はデフォルトではtrueになっておりこの状態ではMinValueにマイナスの値を設定する場合は効果がなくなってしまいます。(このメソッドの意味は「左下は必ず0始まりにする」という意味)

・`rightAxis.setEnabled(false)`
Y軸（右側）を消すためのメソッドです。
デフォルトではY軸は左側・右側両方に表示するのですが、少し邪魔だったので今回は消しました。

## データ追加描画処理
データが追加されるたびに以下の処理を実行します。上記のER図でいうLineDataSet, Entryについての処理になります。

```java:データ追加描画処理
        LineData data = mChart.getData();
        if (data == null) {
            return;

        }

        LineDataSet set = data.getDataSetByIndex(0);
        if (set == null) {
            set = new LineDataSet(null, "サンプルデータ"); //  解説
            set.setColor(Color.BLUE);
            set.setDrawValues(false);
            data.addDataSet(set);
        }

        //  追加描画するデータを追加
        SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
        data.addXValue(format.format(date));
        data.addEntry(new Entry((float) value, set.getEntryCount()), 0);

        //  データを追加したら必ずよばないといけない
        mChart.notifyDataSetChanged();

        mChart.setVisibleXRangeMaximum(60); //  解説

        mChart.moveViewToX(data.getXValCount() - 61);   //  移動する
```

・LineDataSet初期化
一発目この処理が流れる時にだけLineDataSetのインスタンスを設定します。上記コードの第一引数はEntryのリストを設定するのですが、のちの処理で追加していくので今回はなしです。また第２引数は凡例に表示されるテキストになります。

・データの追加
`data.addXValue(format.format(date))`, `data.addEntry(new Entry((float) value, set.getEntryCount()), 0)`のメソッドでデータ追加を行っています。普通に考えれば`addEntry`だけすればいいとは思いますが、対応するX軸ラベルも付けないとうまくいかないので`addXValue`も実行しています。

[公式ドキュメント](https://github.com/PhilJay/MPAndroidChart/wiki/Dynamic-&-Realtime-Data)にも書いているのですが、データ追加した結果をグラフに反映させるためには**必ず**以下のコードを実行しなければなりません。

```java
mChart.notifyDataSetChanged();
mChart.invalidate();
```

しかし、「データ追加描画処理」のコードでは`mChart.invalidate`を呼び出していません。これは`mChart.moveViewToX`の内部で`mChart.invalidate`を呼び出しているためです。(ちなみにこの`moveViewToX`はデータ追加していくたびに昔のデータをどんどん左側によせるようにみせるためにやっています。オシロスコープやMemory Monitorの動きを想像していただければわかるかなと思います)

・` mChart.setVisibleXRangeMaximum(60);`
これはX軸に表示する最大のEntryの数を指定しています。
Chartそのものの設定なはずなのにChart初期化のところでなくデータ追加描画のたびに呼び出しているのは、`notifyDataSetChanged`を実行するとこの設定が一回リセットされて60個以上点が描画されてしまうと現象が起こったからです。(どうしてそうなるか？というのはソースコードを読めばわかると思うのですが、まだです、すみません)

## これらの処理を書き終えたら…
きちんとうごくか確認してOKという感じです。
スタイルなどをアレンジしたければ適宜変えていっても面白いと思います。

# まとめ
今回は「リアルタイム更新のセンサーデータ時系列グラフ」のサンプルを作っていく、という体を以ってMPAndroidChartを使ったプログラム作成の例を示しました。
MPAndroidChartをうまくつかえばこれ以外にももっと素晴らしいグラフを実装することができます。ぜひみなさんもこのライブラリを使って、アプリ内でかっこいいグラフを描画してみてはいかがでしょうか。

※おまけ
実はiOS版もあったりするらしいです。
https://github.com/danielgindi/ios-charts

# 参考サイト
・マネーフォワード開発者ブログ
https://moneyforward.com/engineers_blog/2015/10/20/mpandroidchart/
・Anysense Blog
http://blog.anysense.co.jp/app/mpandroidchart/