org.gnucash.android.ui.chart.PieChartActivity.java Source code

Java tutorial

Introduction

Here is the source code for org.gnucash.android.ui.chart.PieChartActivity.java

Source

/*
 * Copyright (c) 2014-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.chart;

import android.app.DatePickerDialog;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.DialogFragment;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.DatePicker;
import android.widget.ImageButton;
import android.widget.Spinner;
import android.widget.TextView;

import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuItem;
import com.github.mikephil.charting.charts.PieChart;
import com.github.mikephil.charting.components.Legend.LegendForm;
import com.github.mikephil.charting.components.Legend.LegendPosition;
import com.github.mikephil.charting.data.Entry;
import com.github.mikephil.charting.data.PieData;
import com.github.mikephil.charting.data.PieDataSet;
import com.github.mikephil.charting.listener.OnChartValueSelectedListener;
import com.github.mikephil.charting.utils.Highlight;

import org.gnucash.android.R;
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.model.Money;
import org.gnucash.android.ui.passcode.PassLockActivity;
import org.joda.time.LocalDateTime;

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

import static org.gnucash.android.db.DatabaseSchema.AccountEntry;

/**
 * Activity used for drawing a pie chart
 *
 * @author Oleksandr Tyshkovets <olexandr.tyshkovets@gmail.com>
 * @author Ngewi Fet <ngewif@gmail.com>
 */
public class PieChartActivity extends PassLockActivity
        implements OnChartValueSelectedListener, DatePickerDialog.OnDateSetListener {

    private static final int[] COLORS = { Color.parseColor("#17ee4e"), Color.parseColor("#cc1f09"),
            Color.parseColor("#3940f7"), Color.parseColor("#f9cd04"), Color.parseColor("#5f33a8"),
            Color.parseColor("#e005b6"), Color.parseColor("#17d6ed"), Color.parseColor("#e4a9a2"),
            Color.parseColor("#8fe6cd"), Color.parseColor("#8b48fb"), Color.parseColor("#343a36"),
            Color.parseColor("#6decb1"), Color.parseColor("#a6dcfd"), Color.parseColor("#5c3378"),
            Color.parseColor("#a6dcfd"), Color.parseColor("#ba037c"), Color.parseColor("#708809"),
            Color.parseColor("#32072c"), Color.parseColor("#fddef8"), Color.parseColor("#fa0e6e"),
            Color.parseColor("#d9e7b5") };

    private static final String DATE_PATTERN = "MMMM\nYYYY";
    private static final String TOTAL_VALUE_LABEL_PATTERN = "%s\n%.2f %s";
    private static final int ANIMATION_DURATION = 1800;

    private PieChart mChart;

    private LocalDateTime mChartDate = new LocalDateTime();
    private TextView mChartDateTextView;

    private ImageButton mPreviousMonthButton;
    private ImageButton mNextMonthButton;

    private AccountsDbAdapter mAccountsDbAdapter;
    private TransactionsDbAdapter mTransactionsDbAdapter;

    private LocalDateTime mEarliestTransactionDate;
    private LocalDateTime mLatestTransactionDate;

    private AccountType mAccountType = AccountType.EXPENSE;

    private boolean mChartDataPresent = true;

    private boolean mUseAccountColor = true;

    private double mSlicePercentThreshold = 6;

    private String mCurrencyCode;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //it is necessary to set the view first before calling super because of the nav drawer in BaseDrawerActivity
        setContentView(R.layout.activity_pie_chart);
        super.onCreate(savedInstanceState);
        getSupportActionBar().setTitle(R.string.title_pie_chart);

        mUseAccountColor = PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
                .getBoolean(getString(R.string.key_use_account_color), false);

        mPreviousMonthButton = (ImageButton) findViewById(R.id.previous_month_chart_button);
        mNextMonthButton = (ImageButton) findViewById(R.id.next_month_chart_button);
        mChartDateTextView = (TextView) findViewById(R.id.chart_date);

        mAccountsDbAdapter = AccountsDbAdapter.getInstance();
        mTransactionsDbAdapter = TransactionsDbAdapter.getInstance();

        mCurrencyCode = PreferenceManager.getDefaultSharedPreferences(this)
                .getString(getString(R.string.key_report_currency), Money.DEFAULT_CURRENCY_CODE);

        mChart = (PieChart) findViewById(R.id.pie_chart);
        mChart.setCenterTextSize(18);
        mChart.setDescription("");
        mChart.getLegend().setEnabled(false);
        mChart.setOnChartValueSelectedListener(this);

        setUpSpinner();

        mPreviousMonthButton.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View view) {
                mChartDate = mChartDate.minusMonths(1);
                setData(true);
            }
        });
        mNextMonthButton.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View view) {
                mChartDate = mChartDate.plusMonths(1);
                setData(true);
            }
        });

        mChartDateTextView.setOnClickListener(new View.OnClickListener() {

            @Override
            public void onClick(View view) {
                DialogFragment newFragment = ChartDatePickerFragment.newInstance(PieChartActivity.this,
                        mChartDate.toDate().getTime(), mEarliestTransactionDate.toDate().getTime(),
                        mLatestTransactionDate.toDate().getTime());
                newFragment.show(getSupportFragmentManager(), "date_dialog");
            }
        });
    }

    /**
     * Sets the chart data
     * @param forCurrentMonth sets data only for current month if {@code true}, otherwise for all time
     */
    private void setData(boolean forCurrentMonth) {
        mChartDateTextView.setText(forCurrentMonth ? mChartDate.toString(DATE_PATTERN)
                : getResources().getString(R.string.label_chart_overall));
        ((TextView) findViewById(R.id.selected_chart_slice)).setText("");
        mChart.highlightValues(null);
        mChart.clear();

        mChart.setData(getData(forCurrentMonth));
        if (mChartDataPresent) {
            mChart.animateXY(ANIMATION_DURATION, ANIMATION_DURATION);
        }
        mChart.invalidate();

        mChartDateTextView.setEnabled(mChartDataPresent);
        setImageButtonEnabled(mNextMonthButton, mChartDate.plusMonths(1).dayOfMonth().withMinimumValue()
                .withMillisOfDay(0).isBefore(mLatestTransactionDate));
        setImageButtonEnabled(mPreviousMonthButton,
                (mEarliestTransactionDate.getYear() != 1970 && mChartDate.minusMonths(1).dayOfMonth()
                        .withMaximumValue().withMillisOfDay(86399999).isAfter(mEarliestTransactionDate)));
    }

    /**
     * Returns {@code PieData} instance with data entries and labels
     * @param forCurrentMonth sets data only for current month if {@code true}, otherwise for all time
     * @return {@code PieData} instance
     */
    private PieData getData(boolean forCurrentMonth) {
        List<Account> accountList = mAccountsDbAdapter.getSimpleAccountList(
                AccountEntry.COLUMN_TYPE + " = ? AND " + AccountEntry.COLUMN_PLACEHOLDER + " = ?",
                new String[] { mAccountType.name(), "0" }, null);
        List<String> uidList = new ArrayList<>();
        for (Account account : accountList) {
            uidList.add(account.getUID());
        }
        double sum;
        if (forCurrentMonth) {
            long start = mChartDate.dayOfMonth().withMinimumValue().millisOfDay().withMinimumValue().toDate()
                    .getTime();
            long end = mChartDate.dayOfMonth().withMaximumValue().millisOfDay().withMaximumValue().toDate()
                    .getTime();
            sum = mAccountsDbAdapter.getAccountsBalance(uidList, start, end).absolute().asDouble();
        } else {
            sum = mAccountsDbAdapter.getAccountsBalance(uidList, -1, -1).absolute().asDouble();
        }

        double otherSlice = 0;
        PieDataSet dataSet = new PieDataSet(null, "");
        List<String> names = new ArrayList<>();
        List<String> skipUUID = new ArrayList<>();
        for (Account account : getCurrencyCodeToAccountMap(accountList).get(mCurrencyCode)) {
            if (mAccountsDbAdapter.getSubAccountCount(account.getUID()) > 0) {
                skipUUID.addAll(mAccountsDbAdapter.getDescendantAccountUIDs(account.getUID(), null, null));
            }
            if (!skipUUID.contains(account.getUID())) {
                double balance;
                if (forCurrentMonth) {
                    long start = mChartDate.dayOfMonth().withMinimumValue().millisOfDay().withMinimumValue()
                            .toDate().getTime();
                    long end = mChartDate.dayOfMonth().withMaximumValue().millisOfDay().withMaximumValue().toDate()
                            .getTime();
                    balance = mAccountsDbAdapter.getAccountBalance(account.getUID(), start, end).absolute()
                            .asDouble();
                } else {
                    balance = mAccountsDbAdapter.getAccountBalance(account.getUID()).absolute().asDouble();
                }

                if (balance / sum * 100 > mSlicePercentThreshold) {
                    dataSet.addEntry(new Entry((float) balance, dataSet.getEntryCount()));
                    if (mUseAccountColor) {
                        dataSet.getColors().set(dataSet.getColors().size() - 1,
                                (account.getColorHexCode() != null) ? Color.parseColor(account.getColorHexCode())
                                        : COLORS[(dataSet.getEntryCount() - 1) % COLORS.length]);
                    }
                    dataSet.addColor(COLORS[(dataSet.getEntryCount() - 1) % COLORS.length]);
                    names.add(account.getName());
                } else {
                    otherSlice += balance;
                }
            }
        }
        if (otherSlice > 0) {
            dataSet.addEntry(new Entry((float) otherSlice, dataSet.getEntryCount()));
            dataSet.getColors().set(dataSet.getColors().size() - 1, Color.LTGRAY);
            names.add(getResources().getString(R.string.label_other_slice));
        }

        if (dataSet.getEntryCount() == 0) {
            mChartDataPresent = false;
            dataSet.addEntry(new Entry(1, 0));
            dataSet.setColor(Color.LTGRAY);
            dataSet.setDrawValues(false);
            names.add("");
            mChart.setCenterText(getResources().getString(R.string.label_chart_no_data));
            mChart.setTouchEnabled(false);
        } else {
            mChartDataPresent = true;
            dataSet.setSliceSpace(2);
            mChart.setCenterText(String.format(TOTAL_VALUE_LABEL_PATTERN,
                    getResources().getString(R.string.label_chart_total), dataSet.getYValueSum(),
                    Currency.getInstance(mCurrencyCode).getSymbol(Locale.getDefault())));
            mChart.setTouchEnabled(true);
        }

        return new PieData(names, dataSet);
    }

    /**
     * Returns a map with a currency code as key and corresponding accounts list
     * as value from a specified list of accounts
     * @param accountList a list of accounts
     * @return a map with a currency code as key and corresponding accounts list as value
     */
    private Map<String, List<Account>> getCurrencyCodeToAccountMap(List<Account> accountList) {
        Map<String, List<Account>> currencyAccountMap = new HashMap<>();
        for (Currency currency : mAccountsDbAdapter.getCurrencies()) {
            currencyAccountMap.put(currency.getCurrencyCode(), new ArrayList<Account>());
        }

        for (Account account : accountList) {
            currencyAccountMap.get(account.getCurrency().getCurrencyCode()).add(account);
        }
        return currencyAccountMap;
    }

    /**
     * Sets the image button to the given state and grays-out the icon
     *
     * @param enabled the button's state
     * @param button the button item to modify
     */
    private void setImageButtonEnabled(ImageButton button, boolean enabled) {
        button.setEnabled(enabled);
        Drawable originalIcon = button.getDrawable();
        if (enabled) {
            originalIcon.clearColorFilter();
        } else {
            originalIcon.setColorFilter(Color.GRAY, PorterDuff.Mode.SRC_IN);
        }
        button.setImageDrawable(originalIcon);
    }

    /**
     * Sorts the pie's slices in ascending order
     */
    private void bubbleSort() {
        List<String> labels = mChart.getData().getXVals();
        List<Entry> values = mChart.getData().getDataSet().getYVals();
        List<Integer> colors = mChart.getData().getDataSet().getColors();
        float tmp1;
        String tmp2;
        Integer tmp3;
        for (int i = 0; i < values.size() - 1; i++) {
            for (int j = 1; j < values.size() - i; j++) {
                if (values.get(j - 1).getVal() > values.get(j).getVal()) {
                    tmp1 = values.get(j - 1).getVal();
                    values.get(j - 1).setVal(values.get(j).getVal());
                    values.get(j).setVal(tmp1);

                    tmp2 = labels.get(j - 1);
                    labels.set(j - 1, labels.get(j));
                    labels.set(j, tmp2);

                    tmp3 = colors.get(j - 1);
                    colors.set(j - 1, colors.get(j));
                    colors.set(j, tmp3);
                }
            }
        }

        mChart.notifyDataSetChanged();
        mChart.highlightValues(null);
        mChart.invalidate();
    }

    /**
     * Sets up settings and data for the account type spinner. Currently used only {@code EXPENSE} and {@code INCOME}
     * account types.
     */
    private void setUpSpinner() {
        Spinner spinner = (Spinner) findViewById(R.id.chart_data_spinner);
        ArrayAdapter<AccountType> dataAdapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_item,
                Arrays.asList(AccountType.EXPENSE, AccountType.INCOME));
        dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
        spinner.setAdapter(dataAdapter);
        spinner.setOnItemSelectedListener(new OnItemSelectedListener() {
            @Override
            public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
                mAccountType = (AccountType) ((Spinner) findViewById(R.id.chart_data_spinner)).getSelectedItem();
                mEarliestTransactionDate = new LocalDateTime(
                        mTransactionsDbAdapter.getTimestampOfEarliestTransaction(mAccountType, mCurrencyCode));
                mLatestTransactionDate = new LocalDateTime(
                        mTransactionsDbAdapter.getTimestampOfLatestTransaction(mAccountType, mCurrencyCode));
                mChartDate = mLatestTransactionDate;
                setData(false);
            }

            @Override
            public void onNothingSelected(AdapterView<?> adapterView) {
            }
        });
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getSupportMenuInflater().inflate(R.menu.chart_actions, menu);
        return true;
    }

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

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
        case R.id.menu_order_by_size: {
            bubbleSort();
            break;
        }
        case R.id.menu_toggle_legend: {
            mChart.getLegend().setEnabled(!mChart.getLegend().isEnabled());
            mChart.getLegend().setForm(LegendForm.CIRCLE);
            mChart.getLegend().setPosition(LegendPosition.RIGHT_OF_CHART_CENTER);
            mChart.notifyDataSetChanged();
            mChart.invalidate();
            break;
        }
        case R.id.menu_toggle_labels: {
            mChart.getData().setDrawValues(!mChart.isDrawSliceTextEnabled());
            mChart.setDrawSliceText(!mChart.isDrawSliceTextEnabled());
            mChart.invalidate();
            break;
        }
        case R.id.menu_group_other_slice: {
            mSlicePercentThreshold = Math.abs(mSlicePercentThreshold - 6);
            setData(false);
            break;
        }
        case android.R.id.home: {
            finish();
            break;
        }
        }
        return true;
    }

    /**
     * Since JellyBean, the onDateSet() method of the DatePicker class is called twice i.e. once when
     * OK button is pressed and then when the DatePickerDialog is dismissed. It is a known bug.
     */
    @Override
    public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
        if (view.isShown()) {
            mChartDate = new LocalDateTime(year, monthOfYear + 1, dayOfMonth, 0, 0);
            setData(true);
        }
    }

    @Override
    public void onValueSelected(Entry e, int dataSetIndex, Highlight h) {
        if (e == null)
            return;
        ((TextView) findViewById(R.id.selected_chart_slice))
                .setText(mChart.getData().getXVals().get(e.getXIndex()) + " - " + e.getVal() + " ("
                        + String.format("%.2f", (e.getVal() / mChart.getYValueSum()) * 100) + " %)");
    }

    @Override
    public void onNothingSelected() {
        ((TextView) findViewById(R.id.selected_chart_slice)).setText("");
    }
}