org.gnucash.android.ui.report.LineChartFragment.java Source code

Java tutorial

Introduction

Here is the source code for org.gnucash.android.ui.report.LineChartFragment.java

Source

/*
 * Copyright (c) 2015 Oleksandr Tyshkovets <olexandr.tyshkovets@gmail.com>
 * Copyright (c) 2015 Ngewi Fet <ngewif@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.gnucash.android.ui.report;

import android.graphics.Color;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import com.github.mikephil.charting.charts.LineChart;
import com.github.mikephil.charting.components.Legend;
import com.github.mikephil.charting.components.LimitLine;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.data.LineDataSet;
import com.github.mikephil.charting.highlight.Highlight;
import com.github.mikephil.charting.listener.OnChartValueSelectedListener;
import com.github.mikephil.charting.utils.LargeValueFormatter;

import org.gnucash.android.R;
import org.gnucash.android.app.GnuCashApplication;
import org.gnucash.android.db.AccountsDbAdapter;
import org.gnucash.android.db.TransactionsDbAdapter;
import org.gnucash.android.model.Account;
import org.gnucash.android.model.AccountType;
import org.gnucash.android.ui.report.ReportsActivity.GroupInterval;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.joda.time.Months;
import org.joda.time.Years;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Currency;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import butterknife.Bind;
import butterknife.ButterKnife;

/**
 * Fragment for line chart reports
 *
 * @author Oleksandr Tyshkovets <olexandr.tyshkovets@gmail.com>
 * @author Ngewi Fet <ngewif@gmail.com>
 */
public class LineChartFragment extends Fragment implements OnChartValueSelectedListener, ReportOptionsListener {

    private static final String TAG = "LineChartFragment";
    private static final String X_AXIS_PATTERN = "MMM YY";
    private static final String SELECTED_VALUE_PATTERN = "%s - %.2f (%.2f %%)";
    private static final int ANIMATION_DURATION = 3000;
    private static final int NO_DATA_COLOR = Color.GRAY;
    private static final int NO_DATA_BAR_COUNTS = 5;
    private static final int[] COLORS = { Color.parseColor("#68F1AF"), Color.parseColor("#cc1f09"),
            Color.parseColor("#EE8600"), Color.parseColor("#1469EB"), Color.parseColor("#B304AD"), };
    private static final int[] FILL_COLORS = { Color.parseColor("#008000"), Color.parseColor("#FF0000"),
            Color.parseColor("#BE6B00"), Color.parseColor("#0065FF"), Color.parseColor("#8F038A"), };

    private AccountsDbAdapter mAccountsDbAdapter = AccountsDbAdapter.getInstance();
    private Map<AccountType, Long> mEarliestTimestampsMap = new HashMap<>();
    private Map<AccountType, Long> mLatestTimestampsMap = new HashMap<>();
    private long mEarliestTransactionTimestamp;
    private long mLatestTransactionTimestamp;
    private boolean mChartDataPresent = true;
    private Currency mCurrency;

    private GroupInterval mGroupInterval = GroupInterval.MONTH;

    /**
     * Reporting period start time
     */
    private long mReportStartTime = -1;

    /**
     * Reporting period end time
     */
    private long mReportEndTime = -1;

    @Bind(R.id.line_chart)
    LineChart mChart;
    @Bind(R.id.selected_chart_slice)
    TextView mChartSliceInfo;

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_line_chart, container, false);
        ButterKnife.bind(this, view);
        return view;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        ((AppCompatActivity) getActivity()).getSupportActionBar().setTitle(R.string.title_line_chart);
        setHasOptionsMenu(true);

        mCurrency = Currency.getInstance(GnuCashApplication.getDefaultCurrencyCode());

        ReportsActivity reportsActivity = (ReportsActivity) getActivity();
        mReportStartTime = reportsActivity.getReportStartTime();
        mReportEndTime = reportsActivity.getReportEndTime();

        mChart.setOnChartValueSelectedListener(this);
        mChart.setDescription("");
        mChart.getXAxis().setDrawGridLines(false);
        mChart.getAxisRight().setEnabled(false);
        mChart.getAxisLeft().enableGridDashedLine(4.0f, 4.0f, 0);
        mChart.getAxisLeft().setValueFormatter(new LargeValueFormatter(mCurrency.getSymbol(Locale.getDefault())));

        // below we can add/remove displayed account's types
        mChart.setData(getData(new ArrayList<>(Arrays.asList(AccountType.INCOME, AccountType.EXPENSE))));

        Legend legend = mChart.getLegend();
        legend.setPosition(Legend.LegendPosition.BELOW_CHART_CENTER);
        legend.setTextSize(16);
        legend.setForm(Legend.LegendForm.CIRCLE);

        if (!mChartDataPresent) {
            mChart.getAxisLeft().setAxisMaxValue(10);
            mChart.getAxisLeft().setDrawLabels(false);
            mChart.getXAxis().setDrawLabels(false);
            mChart.setTouchEnabled(false);
            mChartSliceInfo.setText(getResources().getString(R.string.label_chart_no_data));
        } else {
            mChart.animateX(ANIMATION_DURATION);
        }
        mChart.invalidate();
    }

    @Override
    public void onResume() {
        super.onResume();
        ((ReportsActivity) getActivity()).setAppBarColor(R.color.account_blue);
    }

    /**
     * Returns a data object that represents a user data of the specified account types
     * @param accountTypeList account's types which will be displayed
     * @return a {@code LineData} instance that represents a user data
     */
    private LineData getData(List<AccountType> accountTypeList) {
        Log.w(TAG, "getData");
        calculateEarliestAndLatestTimestamps(accountTypeList);
        // LocalDateTime?
        LocalDate startDate;
        LocalDate endDate;
        if (mReportStartTime == -1 && mReportEndTime == -1) {
            startDate = new LocalDate(mEarliestTransactionTimestamp).withDayOfMonth(1);
            endDate = new LocalDate(mLatestTransactionTimestamp).withDayOfMonth(1);
        } else {
            startDate = new LocalDate(mReportStartTime).withDayOfMonth(1);
            endDate = new LocalDate(mReportEndTime).withDayOfMonth(1);
        }

        int count = getDateDiff(new LocalDateTime(startDate.toDate().getTime()),
                new LocalDateTime(endDate.toDate().getTime()));
        Log.d(TAG, "X-axis count" + count);
        List<String> xValues = new ArrayList<>();
        for (int i = 0; i <= count; i++) {
            switch (mGroupInterval) {
            case MONTH:
                xValues.add(startDate.toString(X_AXIS_PATTERN));
                Log.d(TAG, "X-axis " + startDate.toString("MM yy"));
                startDate = startDate.plusMonths(1);
                break;
            case QUARTER:
                int quarter = getQuarter(new LocalDateTime(startDate.toDate().getTime()));
                xValues.add("Q" + quarter + startDate.toString(" yy"));
                Log.d(TAG, "X-axis " + "Q" + quarter + startDate.toString(" MM yy"));
                startDate = startDate.plusMonths(3);
                break;
            case YEAR:
                xValues.add(startDate.toString("yyyy"));
                Log.d(TAG, "X-axis " + startDate.toString("yyyy"));
                startDate = startDate.plusYears(1);
                break;
            //                default:
            }
        }

        List<LineDataSet> dataSets = new ArrayList<>();
        for (AccountType accountType : accountTypeList) {
            LineDataSet set = new LineDataSet(getEntryList(accountType), accountType.toString());
            set.setDrawFilled(true);
            set.setLineWidth(2);
            set.setColor(COLORS[dataSets.size()]);
            set.setFillColor(FILL_COLORS[dataSets.size()]);

            dataSets.add(set);
        }

        LineData lineData = new LineData(xValues, dataSets);
        if (lineData.getYValueSum() == 0) {
            mChartDataPresent = false;
            return getEmptyData();
        }
        return lineData;
    }

    /**
     * Calculates difference between two date values accordingly to {@code mGroupInterval}
     * @param start start date
     * @param end end date
     * @return difference between two dates or {@code -1}
     */
    private int getDateDiff(LocalDateTime start, LocalDateTime end) {
        switch (mGroupInterval) {
        case QUARTER:
            int y = Years.yearsBetween(start.withDayOfYear(1).withMillisOfDay(0),
                    end.withDayOfYear(1).withMillisOfDay(0)).getYears();
            return (getQuarter(end) - getQuarter(start) + y * 4);
        case MONTH:
            return Months.monthsBetween(start.withDayOfMonth(1).withMillisOfDay(0),
                    end.withDayOfMonth(1).withMillisOfDay(0)).getMonths();
        case YEAR:
            return Years.yearsBetween(start.withDayOfYear(1).withMillisOfDay(0),
                    end.withDayOfYear(1).withMillisOfDay(0)).getYears();
        default:
            return -1;
        }
    }

    /**
     * Returns a quarter of the specified date
     * @param date date
     * @return a quarter
     */
    private int getQuarter(LocalDateTime date) {
        return ((date.getMonthOfYear() - 1) / 3 + 1);
    }

    /**
     * Returns a data object that represents situation when no user data available
     * @return a {@code LineData} instance for situation when no user data available
     */
    private LineData getEmptyData() {
        List<String> xValues = new ArrayList<>();
        List<Entry> yValues = new ArrayList<>();
        for (int i = 0; i < NO_DATA_BAR_COUNTS; i++) {
            xValues.add("");
            yValues.add(new Entry(i % 2 == 0 ? 5f : 4.5f, i));
        }
        LineDataSet set = new LineDataSet(yValues, getResources().getString(R.string.label_chart_no_data));
        set.setDrawFilled(true);
        set.setDrawValues(false);
        set.setColor(NO_DATA_COLOR);
        set.setFillColor(NO_DATA_COLOR);

        return new LineData(xValues, Collections.singletonList(set));
    }

    /**
     * Returns entries which represent a user data of the specified account type
     * @param accountType account's type which user data will be processed
     * @return entries which represent a user data
     */
    private List<Entry> getEntryList(AccountType accountType) {
        List<String> accountUIDList = new ArrayList<>();
        for (Account account : mAccountsDbAdapter.getSimpleAccountList()) {
            if (account.getAccountType() == accountType && !account.isPlaceholderAccount()
                    && account.getCurrency() == mCurrency) {
                accountUIDList.add(account.getUID());
            }
        }

        LocalDateTime earliest;
        LocalDateTime latest;
        if (mReportStartTime == -1 && mReportEndTime == -1) {
            earliest = new LocalDateTime(mEarliestTimestampsMap.get(accountType));
            latest = new LocalDateTime(mLatestTimestampsMap.get(accountType));
        } else {
            earliest = new LocalDateTime(mReportStartTime);
            latest = new LocalDateTime(mReportEndTime);
        }
        Log.d(TAG, "Earliest " + accountType + " date " + earliest.toString("dd MM yyyy"));
        Log.d(TAG, "Latest " + accountType + " date " + latest.toString("dd MM yyyy"));

        int xAxisOffset = getDateDiff(new LocalDateTime(mEarliestTransactionTimestamp), earliest);
        int count = getDateDiff(earliest, latest);
        List<Entry> values = new ArrayList<>(count + 1);
        for (int i = 0; i <= count; i++) {
            long start = 0;
            long end = 0;
            switch (mGroupInterval) {
            case QUARTER:
                int quarter = getQuarter(earliest);
                start = earliest.withMonthOfYear(quarter * 3 - 2).dayOfMonth().withMinimumValue().millisOfDay()
                        .withMinimumValue().toDate().getTime();
                end = earliest.withMonthOfYear(quarter * 3).dayOfMonth().withMaximumValue().millisOfDay()
                        .withMaximumValue().toDate().getTime();

                earliest = earliest.plusMonths(3);
                break;
            case MONTH:
                start = earliest.dayOfMonth().withMinimumValue().millisOfDay().withMinimumValue().toDate()
                        .getTime();
                end = earliest.dayOfMonth().withMaximumValue().millisOfDay().withMaximumValue().toDate().getTime();

                earliest = earliest.plusMonths(1);
                break;
            case YEAR:
                start = earliest.dayOfYear().withMinimumValue().millisOfDay().withMinimumValue().toDate().getTime();
                end = earliest.dayOfYear().withMaximumValue().millisOfDay().withMaximumValue().toDate().getTime();

                earliest = earliest.plusYears(1);
                break;
            }
            float balance = (float) mAccountsDbAdapter.getAccountsBalance(accountUIDList, start, end).asDouble();
            values.add(new Entry(balance, i + xAxisOffset));
            Log.d(TAG, accountType + earliest.toString(" MMM yyyy") + ", balance = " + balance);

        }

        return values;
    }

    /**
     * Calculates the earliest and latest transaction's timestamps of the specified account types
     * @param accountTypeList account's types which will be processed
     */
    private void calculateEarliestAndLatestTimestamps(List<AccountType> accountTypeList) {
        if (mReportStartTime != -1 && mReportEndTime != -1) {
            mEarliestTransactionTimestamp = mReportStartTime;
            mLatestTransactionTimestamp = mReportEndTime;
            return;
        }

        TransactionsDbAdapter dbAdapter = TransactionsDbAdapter.getInstance();
        for (Iterator<AccountType> iter = accountTypeList.iterator(); iter.hasNext();) {
            AccountType type = iter.next();
            long earliest = dbAdapter.getTimestampOfEarliestTransaction(type, mCurrency.getCurrencyCode());
            long latest = dbAdapter.getTimestampOfLatestTransaction(type, mCurrency.getCurrencyCode());
            if (earliest > 0 && latest > 0) {
                mEarliestTimestampsMap.put(type, earliest);
                mLatestTimestampsMap.put(type, latest);
            } else {
                iter.remove();
            }
        }

        if (mEarliestTimestampsMap.isEmpty() || mLatestTimestampsMap.isEmpty()) {
            return;
        }

        List<Long> timestamps = new ArrayList<>(mEarliestTimestampsMap.values());
        timestamps.addAll(mLatestTimestampsMap.values());
        Collections.sort(timestamps);
        mEarliestTransactionTimestamp = timestamps.get(0);
        mLatestTransactionTimestamp = timestamps.get(timestamps.size() - 1);
    }

    @Override
    public void onTimeRangeUpdated(long start, long end) {
        if (mReportStartTime != start || mReportEndTime != end) {
            mReportStartTime = start;
            mReportEndTime = end;
            mChart.setData(getData(new ArrayList<>(Arrays.asList(AccountType.INCOME, AccountType.EXPENSE))));
            mChart.invalidate();
        }
    }

    @Override
    public void onGroupingUpdated(GroupInterval groupInterval) {
        if (mGroupInterval != groupInterval) {
            mGroupInterval = groupInterval;
            mChart.setData(getData(new ArrayList<>(Arrays.asList(AccountType.INCOME, AccountType.EXPENSE))));
            mChart.invalidate();
        }
    }

    @Override
    public void onAccountTypeUpdated(AccountType accountType) {
        //nothing to see here, line chart shows both income and expense
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        inflater.inflate(R.menu.chart_actions, menu);
    }

    @Override
    public void onPrepareOptionsMenu(Menu menu) {
        menu.findItem(R.id.menu_toggle_average_lines).setVisible(mChartDataPresent);
        // hide pie/bar chart specific menu items
        menu.findItem(R.id.menu_order_by_size).setVisible(false);
        menu.findItem(R.id.menu_toggle_labels).setVisible(false);
        menu.findItem(R.id.menu_percentage_mode).setVisible(false);
        menu.findItem(R.id.menu_group_other_slice).setVisible(false);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.isCheckable())
            item.setChecked(!item.isChecked());
        switch (item.getItemId()) {
        case R.id.menu_toggle_legend:
            mChart.getLegend().setEnabled(!mChart.getLegend().isEnabled());
            mChart.invalidate();
            return true;

        case R.id.menu_toggle_average_lines:
            if (mChart.getAxisLeft().getLimitLines().isEmpty()) {
                for (LineDataSet set : mChart.getData().getDataSets()) {
                    LimitLine line = new LimitLine(set.getYValueSum() / set.getEntryCount(), set.getLabel());
                    line.enableDashedLine(10, 5, 0);
                    line.setLineColor(set.getColor());
                    mChart.getAxisLeft().addLimitLine(line);
                }
            } else {
                mChart.getAxisLeft().removeAllLimitLines();
            }
            mChart.invalidate();
            return true;

        default:
            return super.onOptionsItemSelected(item);
        }
    }

    @Override
    public void onValueSelected(Entry e, int dataSetIndex, Highlight h) {
        if (e == null)
            return;
        String label = mChart.getData().getXVals().get(e.getXIndex());
        double value = e.getVal();
        double sum = mChart.getData().getDataSetByIndex(dataSetIndex).getYValueSum();
        mChartSliceInfo.setText(String.format(SELECTED_VALUE_PATTERN, label, value, value / sum * 100));
    }

    @Override
    public void onNothingSelected() {
        mChartSliceInfo.setText("");
    }
}