Androidアプリ開発

MPAndroidChart 散布図の実装

この記事は約25分で読めます。
記事内に広告が含まれています。
スポンサーリンク

この記事は Android スマホ用のアプリ開発の中で、
今後の開発で再使用性が高いと思われるコーディングをまとめたものです。
Java での開発経験、XML 構文規則、Android のアプリ開発経験がある方を対象としています。
Android のアプリ開発でお役にたててれば、嬉しいです。
(これから Android のアプリ開発や Java での開発を始めたい方への案内は、
記事の最後で紹介します)

この記事のテーマ


 MPAndroidChartを使用して、散布図(ScatterChart)を実装をする

MPAndroidChart は、Androidアプリでグラフを作るためのオープンソースライブラリです。
Andorid アプリでグラフを描画する場合のベストプラクティスといっても過言ではありません。
非常に多機能で細かい部分まで制御できる素晴らしいライブラリですが、
使用方法について、詳しい日本語ドキュメントがなく、
機能の一部しか利用されていない、残念な状況です。

MPAndroidChart は、さまざまなグラフを扱うことが可能です。
今回は、散布図(
ScatterChart )にフォーカスを当てて、
開発したアプリのソースを参考に、使用方法について説明したいと思います。

折れ線グラフ(LineChart)については、こちらです↓↓↓

棒グラフ(BarChart)については、こちらです↓↓↓

MPAndroidChart を使用するための準備

MPAndroidChart を使用するには、プロジェクトおよび、モジュールの build.gradle ファイルに定義の追加が必要です。

◎build.gradle(プロジェクト)

:
allprojects {
    repositories {
    :
        maven { url "https://jitpack.io" }
    }
}
:

◎build.gradle(モジュール)
2022年11月現在の最新バージョンは 3.1.0 です。

dependencies {
    :
    implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0'
}

◎ライセンス表記
MPAndroidChart は Apache License, Version 2.0 です。
アプリで使用する場合、ライセンス表記が必要です。

ソース提供ではないため、ライセンス条文の記載がある公式サイトのリンクをアプリに実装しています

散布図( ScatterChart)

散布図は、あるデータを元にして、2つの軸でそれぞれの値を計測し、分布を表現するグラフです。
散布図を使用する目的は、主に2つの軸でそれぞれの値(要素)に相関関係があるかの可視化です。
主に実験結果などのエビデンスで使用されるグラフですが、2軸をXY軸と捉えると、平面図の描画に応用できます。
折れ線グラフや棒グラフほど使用する機会はありませんが、散布図の特長を生かした
GUIを紹介します。

現在位置と計測ラインを表示するレーダーを散布図を使って実装します

オレンジ色の磁北(方位)の表示は円グラフ(PieChart)を使用しています。

グラフ表示

MPAndroidChart のグラフ表示は、グラフに表示する値(要素)をデータセット(ScatterDataSet)として作成し、散布図(ScatterChart)に散布図データ(scatterData)を介して、セットします。
グラフの背景、表示範囲、振る舞いなどの属性は、グラフに定義します。
凡例、軸ラベル、点の形や大きさ、色などの属性は、データセットに定義します。
複数のデータセットを散布図データにセットすることで、1つの散布図に複数の値(要素)を重ねて表示できます。
また、散布図データにセットする順番が表示順になります(最初にセットしたデータセットが一番下のレイヤー)
散布図の描画ではグラフに表示する値の並びが重要になります。
このため、値が複数あるデータセット(ScatterDataSet)では、データセットを直接操作するのではなく、リスト(ArrayList)を用意して、そちらを操作するように実装します。

    :
    private ScatterChart                locus;
    private final ArrayList<Entry>      scatterEntries1 = new ArrayList<>(); // 計測ライン(点)
    private final ArrayList<Entry>      scatterEntries3 = new ArrayList<>(); // 走行軌跡
    private final ArrayList<Entry>      scatterEntries4 = new ArrayList<>(); // 計測ライン(線)
    private final ScatterDataSet        scatterDataSet2 = new ScatterDataSet(null,"");  // 現在位置
    private List<Double>                startLatitude = new ArrayList<>();  //計測ライン始点緯度
    private List<Double>                startLongitude = new ArrayList<>(); //計測ライン始点経度
    private List<Double>                endLatitude = new ArrayList<>();    //計測ライン終点緯度
    private List<Double>                endLongitude = new ArrayList<>();   //計測ライン終点経度
    :
    // 計測ライン
    ScatterDataSet scatterDataSet1 = new ScatterDataSet(scatterEntries1, "");
    scatterDataSet1.setScatterShape(ScatterChart.ScatterShape.X);
    scatterDataSet1.setScatterShapeSize(16);
    scatterDataSet1.setColor(context.getColor(R.color.blue));
    ScatterDataSet scatterDataSet4 = new ScatterDataSet(scatterEntries4, "");
    scatterDataSet4.setScatterShape(ScatterChart.ScatterShape.CIRCLE);
    scatterDataSet4.setScatterShapeSize(4);
    scatterDataSet4.setColor(context.getColor(R.color.blue));
    // 現在地
    scatterDataSet2.setDrawIcons(true);
    // 軌跡
    ScatterDataSet scatterDataSet3 = new ScatterDataSet(scatterEntries3, "");
    scatterDataSet3.setScatterShape(ScatterChart.ScatterShape.CIRCLE);
    scatterDataSet3.setScatterShapeSize(60);
    scatterDataSet3.setColor(getColor(R.color.red));
    ScatterData scatterData = new ScatterData(scatterDataSet3);
    scatterData.addDataSet(scatterDataSet4);
    scatterData.addDataSet(scatterDataSet1);
    scatterData.addDataSet(scatterDataSet2);
    scatterData.setDrawValues(false);
    // 走行軌跡ビュー
    locus.getDescription().setEnabled(false);
    locus.setDrawGridBackground(false);
    locus.getXAxis().setDrawGridLines(false);
    locus.getXAxis().setDrawAxisLine(false);
    locus.getAxisLeft().setDrawGridLines(false);
    locus.getAxisRight().setDrawGridLines(false);
    locus.getAxisLeft().setDrawAxisLine(false);
    locus.getAxisRight().setDrawAxisLine(false);
    locus.setData(scatterData);
    locus.getAxisLeft().setDrawLabels(false);
    locus.getAxisRight().setDrawLabels(false);
    locus.getXAxis().setDrawLabels(false);
    locus.getLegend().setEnabled(false);
    locus.getData().setHighlightEnabled(false);
    :

グラフ描画の実装方法は、基本的に折れ線グラフ(LineChart)と同じです。
散布図の大きさや背景などの設定は、レイアウトXML、Javaのコーディングのいずれも指定可能です。
上記サンプルでは、現在位置のアイコン(画像)、位置の軌跡、計測ラインの始点と終点とそれを結ぶ点線をデータセットをして定義し、散布図上に表示しています。
散布図上に表示する値(要素)として表示するタイプ(点の大きさ、色、形)毎にデータセットが必要になります。
現在位置(
scatterDataSet2)はアイコン表示するため、setDrawIconstrueをセットしています。
位置の軌跡(
scatterDataSet3)は、60pxの円を複数表示するため、値を格納するリスト(scatterEntries3)を用意しています。
計測ラインの始点と終点(
scatterDataSet1)は、16pxの×印を複数表示するため、値を格納するリスト(scatterEntries1)を用意しています。
計測ラインの結ぶ点線(
scatterDataSet4)は、4pxの円を複数表示するため、値を格納するリスト(scatterEntries4)を用意しています。

Layoutリソース

        :
        <com.github.mikephil.charting.charts.ScatterChart
            android:id="@+id/locus"
            android:layout_width="400dp"
            android:layout_height="400dp"
            android:background="@drawable/green"
            android:layout_centerInParent="true"/>
        :

散布図の背景(サンプルではgreen)をDrawableリソースとして登録し、backgroundに指定します。

散布図のアイコン表示ですが、表示数に制限があるようです。
制限(アイコン以外の値も含む50個程度)を超えた場合、アイコンは非表示(■表示)になります。

グラフの描画

サンプルでは、GPSの位置情報から現在位置と軌跡を散布図にプロットしています。
GPSの位置情報は小数点以下7桁(日本経緯度原点 139.4428887、35.3929157)の数値です。
MPAndroidChartの散布図では、小数点以下7桁もある数値は直接扱えません。
このため、現在地を基準値として、相対位置の10,000,000倍にした値を使用するようにしています。

    :
    private final double[] measured = new double[2];         // LocationService(GPS)
    private double[] lat = new double[16];                   // 軌跡Y(GPS)
    private double[] lon = new double[16];                   // 軌跡X(GPS)
    private final double[] basePoint = new double[] { 0,0 }; //基準位置(GPS)
    private final float[] scope = new float[] { 0, 0, 0, 0 };//スコープ(基準位置からの相対位置)
    private List<Double> startLatitude = new ArrayList<>();  //計測ライン始点緯度
    private List<Double> startLongitude = new ArrayList<>(); //計測ライン始点経度
    private List<Double> endLatitude = new ArrayList<>();    //計測ライン終点緯度
    private List<Double> endLongitude = new ArrayList<>();   //計測ライン終点経度
    private int R_SCALE = 50;
    private int LATITUDE = 1; //GPS緯度補正
    private int LONGITUDE = 1; //GPS経度補正
  :
    // グラフの描画
    if (measured[0] != 0 && measured[1] != 0) {
        if (lat[0] != 0 && lon[0] != 0) {
            for (int i = 14; i > 4; i--) {
                lat[i + 1] = lat[i];
                lon[i + 1] = lon[i];
            }
            lat[0] = measured[0];
            lon[0] = measured[1];
            for (int i = 5; i > 0; i--) {
                 lat[i] = lat[6] - ((lat[6] - lat[0]) / i);
                 lon[i] = lon[6] - ((lon[6] - lon[0]) / i);
            }
        } else {
            for (int i = 0; i < 16; i++) {
                 lat[i] = measured[0];
                 lon[i] = measured[1];
            }
        }
        scatterEntriesscatterEntries3.clear();
        for (int i = 1; i < 16; i++)
            scatterEntries3.add(new Entry(coordinate(lon[i], 1), coordinate(lat[i], 0)));
        scatterEntries3.sort(new EntryXComparator());
        drawRadar(measured);
    }
    :

    // 座標変換 //
    private float coordinate(double point, int axis) {
        int sign = axis == 0 ? LATITUDE:LONGITUDE;
        return (float) ((point - basePoint[axis]) * 10000000 * sign);
    }

    // 現在位置と軌跡表示 //
    private void drawRadar(double[] pos) {
        scope[0] = (R_SCALE * -100f) + coordinate(pos[0], 0);
        scope[1] = (R_SCALE * -100f) + coordinate(pos[1], 1);
        scope[2] = scope[0] + (R_SCALE * 200f);
        scope[3] = scope[1] + (R_SCALE * 200f);
        locus.getXAxis().setAxisMinimum(scope[1]);
        locus.getXAxis().setAxisMaximum(scope[3]);
        locus.getAxisLeft().setAxisMinimum(scope[0]);
        locus.getAxisLeft().setAxisMaximum(scope[2]);
        scatterDataSet2.clear();
        scatterDataSet2.addEntry(new Entry(coordinate(pos[1], 1), coordinate(pos[0], 0), ContextCompat.getDrawable(context, R.drawable.ball3)));
        // 計測ラインの再描画
        setScatterEntries1();
        locus.invalidate();
        locus.fitScreen();
    }

    // 計測ラインの再描画 //
    private void setScatterEntries1() {
        scatterEntries4.clear();
        scatterEntries1.clear();
        int POINTER = 20;
        for (int i = 0; i < startLatitude.size(); i++) {
            double dy = startLatitude.get(i) - endLatitude.get(i);
            double dx = startLongitude.get(i) - endLongitude.get(i);
            dy = dy != 0 ? dy / POINTER : 0;
            dx = dx != 0 ? dx / POINTER : 0;
            scatterEntries1.add(new Entry(coordinate(startLongitude.get(i), 1), coordinate(startLatitude.get(i), 0)));
            scatterEntries1.add(new Entry(coordinate(endLongitude.get(i), 1), coordinate(endLatitude.get(i), 0)));
            for (int j = 1; j < POINTER; j++)
                scatterEntries4.add(new Entry(coordinate(endLongitude.get(i) + (dx * j), 1), coordinate(endLatitude.get(i) + (dy * j), 0)));
        }
        scatterEntries4.sort(new EntryXComparator());
        scatterEntries1.sort(new EntryXComparator());
    }

サンプルでは、GPSの位置情報(measured)が更新されたタイミングごとに、散布図(グラフ)を描画しています。
現在位置を
measuredとして、軌跡の配列(latlon)を更新して、scatterEntries3を再構築しています。
値が複数あるデータセットでは、
MPAndroidChartのグラフ描画の仕様上の問題で、X値の昇順でソートする必要があります。
MPAndroidChartのライブラリ(EntryXComparator)でソートします。
グラフの表示では、最初に表示するエリア(
scope)を現在位置から計算しています。
現在位置の表示(
drawRadar)は、相対位置を計算(coordinate)、表示する画像(ball3)を指定しています。
計測ラインの表示(
setScatterEntries1)は、計測ラインの位置情報の配列(startLatitude startLongitude endLatitude