今回の記事では、データを日付単位のグラフとして表示する実装について書いていきます。

まず、完成イメージは以下のようになります。

1つ目のグラフが、日毎にToDo完了数をカウントした棒グラフ

2つ目のグラフが、日毎のToDo完了数の累計を折れ線グラフ

で表したものになります。

 

このグラフを表示させるために、Androidアプリでグラフを表示するならデファクトスタンダードになっている

MPAndroidChartを利用しました。

なので、このライブラリを利用方法から書いていきます。

1.build.gradle(Project)にリポジトリの追加

以下のように、mavenリポジトリの新しいURLを追加します。

allprojects {
    repositories {
        google()
        mavenCentral()
        maven { url 'https://jitpack.io' }
    }
}

2.build.gradle(app)にライブラリのインポートを追加

以下のようにdependenciesにMPAndroidChartのライブラリを追加します。

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

3.レイアウトファイルを追加

今回は、棒グラフ、折れ線グラフをListViewに表示する実装とします。
res/layoutフォルダ配下に以下のファイルを用意します。

3.1.activity_log_chart.xmlを用意(ListViewを定義)


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ListView
        android:id="@+id/logChartListView"
        android:layout_width="match_parent"
        android:layout_weight="1"
        android:layout_height="match_parent" >
    </ListView>

</LinearLayout>

3.2.chart_item_bar.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="wrap_content"
    android:orientation="vertical" >

	<TextView
		android:layout_width="match_parent"
		android:layout_height="wrap_content"
		android:text="日別のToDo完了数" />

    <com.github.mikephil.charting.charts.BarChart
        android:id="@+id/bar_chart"
        android:layout_width="match_parent" />

</LinearLayout>

3.3.chart_item_line.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="wrap_content"
    android:orientation="vertical" >

	<TextView
		android:layout_width="match_parent"
		android:layout_height="wrap_content"
		android:text="ToDo完了数の累計" />

    <com.github.mikephil.charting.charts.LineChart
        android:id="@+id/line_chart"
        android:layout_width="match_parent" />

</LinearLayout>

4.グラフでデータを設定するクラスを用意

棒グラフ、折れ線グラフにデータを設定するために以下のクラスファイルを用意します。

4.1.ChartItem.javaを用意(グラフデータ設定用の共通クラス)

横軸を日付で表示するために、ValueFormatterクラスを継承したDateValueFormatterクラスを用意します。

public abstract class ChartItem {

    static final int TYPE_BARCHART = 0;
    static final int TYPE_LINECHART = 1;

    ChartData<?> mChartData;

    ChartItem(ChartData<?> cd) {
        this.mChartData = cd;
    }

    public abstract int getItemType();

    public abstract View getView(int position, View convertView, Context c);

    protected class DateValueFormatter extends ValueFormatter {
        private List<Date> mDateRange;
        SimpleDateFormat sdf = new SimpleDateFormat("MM/dd");

        public DateValueFormatter(List<Date> dates) {
            mDateRange = dates;
        }

        @Override
        public String getFormattedValue(float value) {
            return sdf.format(mDateRange.get((int)value));
        }
    }
}

4.2.BarChartItem.javaを用意(棒グラフ用のデータ設定クラス)

親クラスで用意したDateValueFormatterを利用して横軸を日付にします。

public class BarChartItem extends ChartItem {

    private List<Date> mDateRange;

    public BarChartItem(List<Date> dateRange, ChartData<?> cd, Context c) {
        super(cd);
        mDateRange = dateRange;
    }

    @Override
    public int getItemType() {
        return TYPE_BARCHART;
    }

    @SuppressLint("InflateParams")
    @Override
    public View getView(int position, View convertView, Context c) {

        ViewHolder holder;

        if (convertView == null) {

            holder = new ViewHolder();

            convertView = LayoutInflater.from(c).inflate(
                    R.layout.chart_item_bar, null);
            holder.chart = convertView.findViewById(R.id.bar_chart);

            convertView.setTag(holder);

        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        // apply styling
        holder.chart.getDescription().setEnabled(false);
        holder.chart.setDrawGridBackground(false);
        holder.chart.setDrawBarShadow(false);

        XAxis xAxis = holder.chart.getXAxis();
        xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
        xAxis.setValueFormatter(new DateValueFormatter(mDateRange));
        xAxis.setDrawGridLines(false);
        xAxis.setDrawAxisLine(true);

        YAxis leftAxis = holder.chart.getAxisLeft();
        leftAxis.setLabelCount(5, false);
        leftAxis.setSpaceTop(20f);
        leftAxis.setAxisMinimum(0f); // this replaces setStartAtZero(true)

        YAxis rightAxis = holder.chart.getAxisRight();
        rightAxis.setLabelCount(5, false);
        rightAxis.setSpaceTop(20f);
        rightAxis.setAxisMinimum(0f); // this replaces setStartAtZero(true)

        // set data
        holder.chart.setData((BarData) mChartData);
        holder.chart.setFitBars(true);

        // do not forget to refresh the chart
        holder.chart.animateY(700);

        return convertView;
    }

    private static class ViewHolder {
        BarChart chart;
    }
}

4.3.LineChartItem.javaを用意(折れ線グラフ用のデータ設定クラス)

親クラスで用意したDateValueFormatterを利用して横軸を日付にします。

public class LineChartItem extends ChartItem {

    private List<Date> mDateRange;

    public LineChartItem(List<Date> dateRange, ChartData<?> cd, Context c) {
        super(cd);
        mDateRange = dateRange;
    }

    @Override
    public int getItemType() {
        return TYPE_LINECHART;
    }

    @SuppressLint("InflateParams")
    @Override
    public View getView(int position, View convertView, Context c) {

        ViewHolder holder;

        if (convertView == null) {

            holder = new ViewHolder();

            convertView = LayoutInflater.from(c).inflate(
                    R.layout.chart_item_line, null);
            holder.chart = convertView.findViewById(R.id.line_chart);

            convertView.setTag(holder);

        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        // apply styling
        holder.chart.getDescription().setEnabled(false);
        holder.chart.setDrawGridBackground(false);

        XAxis xAxis = holder.chart.getXAxis();
        xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
        xAxis.setValueFormatter(new DateValueFormatter(mDateRange));
        xAxis.setDrawGridLines(false);
        xAxis.setDrawAxisLine(true);

        YAxis leftAxis = holder.chart.getAxisLeft();
        leftAxis.setLabelCount(5, false);
        leftAxis.setAxisMinimum(0f); // this replaces setStartAtZero(true)

        YAxis rightAxis = holder.chart.getAxisRight();
        rightAxis.setLabelCount(5, false);
        rightAxis.setDrawGridLines(false);
        rightAxis.setAxisMinimum(0f); // this replaces setStartAtZero(true)

        // set data
        holder.chart.setData((LineData) mChartData);

        // do not forget to refresh the chart
        holder.chart.animateX(750);

        return convertView;
    }

    private static class ViewHolder {
        LineChart chart;
    }
}

5.グラフを表示するアクティビティクラスを用意

4.で準備したグラフ用のデータ設定クラスをListViewにバインドしていく処理を実装します。

public class LogChartActivity extends AppCompatActivity {
    // チャート表示用アダプタ
    ChartDataAdapter mCda;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_log_chart);

        ListView lv = findViewById(R.id.logChartListView);

        ArrayList<ChartItem> list = new ArrayList<>();

        // データベースなどに以下のデータクラスとしてデータが存在している状態
        // public class DoneCount {
        //     public Date mDate;
        //     public int mDoneCount;
        // }
        // 例)
        // 日付:2021/7/1   8:00:15  完了数:5
        // 日付:2021/7/5  14:10:00  完了数:1
        // 日付:2021/7/5   9:02:09  完了数:2
        // 日付:2021/7/10 22:34:11  完了数:2
        // ・・・
        List<DoneCount> doneCounts = getData(); // 日付と日付に対する完了数のデータを取得
        List<Date> dateRange = createDateRange(doneCounts);
        HashMap<Date, Integer> dateHashMap = summarizeDateCount(doneCounts);
        list.add(new BarChartItem(dateRange, generateDataBar(dateRange, dateHashMap), getApplicationContext()));
        list.add(new LineChartItem(dateRange, generateDataLine(dateRange, dateHashMap), getApplicationContext()));
        lv.setAdapter(mCda);

        mCda = new ChartDataAdapter(getApplicationContext(), list);
        lv.setAdapter(mCda);
    }

    private class ChartDataAdapter extends ArrayAdapter<ChartItem> {

        ChartDataAdapter(Context context, List<ChartItem> objects) {
            super(context, 0, objects);
        }

        @NonNull
        @Override
        public View getView(int position, View convertView, @NonNull ViewGroup parent) {
            return getItem(position).getView(position, convertView, getContext());
        }

        @Override
        public int getItemViewType(int position) {
            ChartItem ci = getItem(position);
            return ci != null ? ci.getItemType() : 0;
        }

        @Override
        public int getViewTypeCount() {
            return 2;
        }
    }

    /**
     * 与えられた日時の時分秒を切り捨てて返却する
     *
     * @param date 日時
     * @return 時分秒を切り捨てた日時
     */
    private Date adjustDate(Date date) {
        Calendar c = Calendar.getInstance();
        c.setTime(date);
        c.set(Calendar.HOUR_OF_DAY, 0);
        c.set(Calendar.MINUTE, 0);
        c.set(Calendar.SECOND, 0);
        c.set(Calendar.MILLISECOND, 0);
        return new Date(c.getTime().getTime());
    }

    /**
     * 与えられた完了日時データから、日付の始まりから終わりまでの日付のリストを作成する
     *
     * @param doneCounts 完了日時データ
     * @return 日付のリスト
     */
    private List<Date> createDateRange(List<DoneCount> doneCounts) {
        if (doneCounts.size() == 0) return null;

        List<Date> dateRange = new ArrayList<>();
        Date firstDate = adjustDate(doneCounts.get(0).mDate);
        Date lastDate = adjustDate(doneCounts.get(doneCounts.size() - 1).mDate);
        Calendar c = Calendar.getInstance();
        c.setTime(firstDate);
        // 開始日の1日前からをグラフにする(完了数を0スタートで表示させるため)
        for (c.add(Calendar.DATE, -1); c.getTime().getTime() <= lastDate.getTime(); c.add(Calendar.DATE, 1)) {
            Date date = new Date(c.getTime().getTime());
            dateRange.add(date);
        }

        return dateRange;
    }

    /**
     * 与えられた完了日時データから、日付単位で数をカウントする
     *
     * @param doneCounts 完了日時データ
     * @return 日付単位で完了数をサマリしたデータ
     */
    private HashMap<Date, Integer> summarizeDateCount(List<DoneCount> doneCounts) {
        HashMap<Date, Integer> dateHashMap = new HashMap<>();

        for (DoneCount doneCount : doneCounts) {
            Date date = adjustDate(doneCount.mDate);
            Integer count = dateHashMap.get(date);
            if (count != null) {
                count += doneCount.mDoneCount;
                dateHashMap.put(date, count);
            } else {
                dateHashMap.put(date, doneCount.mDoneCount);
            }
        }

        return dateHashMap;
    }

    private LineData generateDataLine(List<Date> dateRange, HashMap<Date, Integer> dateHashMap) {

        ArrayList<Entry> values1 = new ArrayList<>();

        if (dateHashMap.size() > 0) {
            int cnt = 0;
            int total = 0;
            for (Date date : dateRange) {
                Integer count = dateHashMap.get(date);
                if (count != null) {
                    total += count;
                }
                values1.add(new Entry(cnt, total));
                cnt++;
            }
        }

        LineDataSet d1 = new LineDataSet(values1, getString(R.string.chart_line_guide));
        d1.setLineWidth(2.5f);
        d1.setCircleRadius(4.5f);
        d1.setHighLightColor(Color.rgb(244, 117, 117));
        d1.setDrawValues(false);

        ArrayList<ILineDataSet> sets = new ArrayList<>();
        sets.add(d1);

        return new LineData(sets);
    }

    private BarData generateDataBar(List<Date> dateRange, HashMap<Date, Integer> dateHashMap) {
        ArrayList<BarEntry> entries = new ArrayList<>();

        if (dateHashMap.size() > 0) {
            int cnt = 0;
            for (Date date : dateRange) {
                Integer count = dateHashMap.get(date);
                if (count != null) {
                    entries.add(new BarEntry(cnt, count));
                } else {
                    entries.add(new BarEntry(cnt, 0));
                }
                cnt++;
            }
        }

        BarDataSet d = new BarDataSet(entries, getString(R.string.chart_bar_guide));
        d.setColor(rgb(Log.LOG_CHANGE_STATUS_DONE_COLOR));
        d.setHighLightAlpha(255);
        // Barグラフ上の数値を整数にする
        d.setValueFormatter(new ValueFormatter() {
            @Override
            public String getFormattedValue(float value) {
                if ((int) value == 0) {
                    return "";
                } else {
                    return "" + (int) value;
                }
            }
        });

        BarData cd = new BarData(d);
        cd.setBarWidth(0.9f);
        return cd;
    }

    private int rgb(String hex) {
        int color = (int) Long.parseLong(hex.replace("#", ""), 16);
        int r = (color >> 16) & 0xFF;
        int g = (color >> 8) & 0xFF;
        int b = (color >> 0) & 0xFF;
        return Color.rgb(r, g, b);
    }
}

この中の処理で特筆すべきところは以下。
・adjustDateメソッドで日付データの時分秒を切り捨てています。
・createDateRangeメソッドで入っているデータの開始日から終了日までの1日づずの日付データを生成しています。
・summarizeDateCountで日付単位でデータを集計しています。
 
以上がMPAndroidChartを利用したグラフの表示方法でした。
 
このグラフを利用したウィジェット機能付きの履歴が残るToDoリストでは、日々のToDoの完了数を毎日の積み上げとして表示できるような実装をしています。ぜひ利用してみてくださいひらめき電球