jgnash.ui.report.compiled.IncomeExpensePieChart.java Source code

Java tutorial

Introduction

Here is the source code for jgnash.ui.report.compiled.IncomeExpensePieChart.java

Source

/*
 * jGnash, a personal finance application
 * Copyright (C) 2001-2016 Craig Cavanaugh
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package jgnash.ui.report.compiled;

import java.awt.Cursor;
import java.awt.EventQueue;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.time.LocalDate;
import java.util.EnumSet;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;

import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JPanel;

import jgnash.engine.Account;
import jgnash.engine.AccountType;
import jgnash.engine.Comparators;
import jgnash.engine.CurrencyNode;
import jgnash.engine.Engine;
import jgnash.engine.EngineFactory;
import jgnash.engine.RootAccount;
import jgnash.text.CommodityFormat;
import jgnash.ui.components.AccountListComboBox;
import jgnash.ui.components.DatePanel;
import jgnash.ui.components.GenericCloseDialog;
import jgnash.ui.util.CursorUtils;
import jgnash.util.DateUtils;
import jgnash.util.ResourceUtils;

import com.jgoodies.forms.builder.DefaultFormBuilder;
import com.jgoodies.forms.layout.FormLayout;

import org.jfree.chart.ChartMouseEvent;
import org.jfree.chart.ChartMouseListener;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.entity.ChartEntity;
import org.jfree.chart.entity.PieSectionEntity;
import org.jfree.chart.labels.PieSectionLabelGenerator;
import org.jfree.chart.labels.StandardPieSectionLabelGenerator;
import org.jfree.chart.plot.PiePlot;
import org.jfree.chart.title.TextTitle;
import org.jfree.data.general.DefaultPieDataset;
import org.jfree.data.general.PieDataset;

/**
 * This displays a dialog that shows a pie chart with account distribution for Income and Expense accounts only.
 * 
 * @author Jeff Prickett
 * @author Craig Cavanaugh
 * @author Chad McHenry
 * @author Dany Veilleux
 * @author Pranay Kumar
 */
public class IncomeExpensePieChart {

    private static final String START_DATE = "startDate";

    private final ResourceBundle rb = ResourceUtils.getBundle();

    private final Preferences pref = Preferences.userNodeForPackage(IncomeExpensePieChart.class);

    private AccountListComboBox combo;

    private JCheckBox showEmptyCheck;

    private JCheckBox showPercentCheck;

    private final Cursor ZOOM_IN = CursorUtils.getCursor(CursorUtils.ZOOM_IN);

    private final Cursor ZOOM_OUT = CursorUtils.getCursor(CursorUtils.ZOOM_OUT);

    private Point lastPoint;

    private Account currentAccount; // because the list may not be showing children

    private ChartPanel chartPanel;

    // Fields to select the dates
    private DatePanel startField;

    private DatePanel endField;

    private PieSectionLabelGenerator defaultLabels;

    private PieSectionLabelGenerator percentLabels;

    public static void show() {

        EventQueue.invokeLater(() -> {
            final IncomeExpensePieChart chart = new IncomeExpensePieChart();

            final JPanel p = chart.createPanel();
            final GenericCloseDialog d = new GenericCloseDialog(p, ResourceUtils.getString("Title.AccountBalance"));
            d.pack();
            d.setModal(false);

            d.setVisible(true);
        });
    }

    private JPanel createPanel() {
        EnumSet<AccountType> set = EnumSet.of(AccountType.INCOME, AccountType.EXPENSE);

        JButton refreshButton = new JButton(rb.getString("Button.Refresh"));

        startField = new DatePanel();

        endField = new DatePanel();

        showEmptyCheck = new JCheckBox(rb.getString("Label.ShowEmptyAccounts"));

        showPercentCheck = new JCheckBox(rb.getString("Button.ShowPercentValues"));

        final Engine engine = EngineFactory.getEngine(EngineFactory.DEFAULT);
        Objects.requireNonNull(engine);

        combo = AccountListComboBox.getParentTypeInstance(engine.getRootAccount(), set);

        final LocalDate dStart = DateUtils.getFirstDayOfTheMonth(LocalDate.now()).minusYears(1);

        long start = pref.getLong(START_DATE, DateUtils.asEpochMilli(dStart));

        startField.setDate(DateUtils.asLocalDate(start));

        currentAccount = combo.getSelectedAccount();
        JFreeChart chart = createPieChart(currentAccount);
        chartPanel = new ChartPanel(chart, true, true, true, false, true);
        //                         (chart, properties, save, print, zoom, tooltips)

        FormLayout layout = new FormLayout("p, 4dlu, 70dlu, 8dlu, p, 4dlu, 70dlu, 8dlu, p, 4dlu:g, left:p",
                "f:d, 3dlu, f:d, 6dlu, f:p:g");
        DefaultFormBuilder builder = new DefaultFormBuilder(layout);
        layout.setRowGroups(new int[][] { { 1, 3 } });

        builder.append(combo, 9);
        builder.append(showEmptyCheck);
        builder.nextLine();

        builder.nextLine();

        builder.append(rb.getString("Label.StartDate"), startField);
        builder.append(rb.getString("Label.EndDate"), endField);
        builder.append(refreshButton);

        builder.append(showPercentCheck);
        builder.nextLine();
        builder.nextLine();

        builder.append(chartPanel, 11);

        JPanel panel = builder.getPanel();

        combo.addActionListener(e -> {
            setCurrentAccount(combo.getSelectedAccount());
            pref.putLong(START_DATE, DateUtils.asEpochMilli(startField.getLocalDate()));
        });

        refreshButton.addActionListener(e -> {
            setCurrentAccount(currentAccount);
            pref.putLong(START_DATE, DateUtils.asEpochMilli(startField.getLocalDate()));
        });

        showEmptyCheck.addActionListener(e -> setCurrentAccount(currentAccount));

        showPercentCheck.addActionListener(e -> ((PiePlot) chartPanel.getChart().getPlot())
                .setLabelGenerator(showPercentCheck.isSelected() ? percentLabels : defaultLabels));

        ChartMouseListener mouseListener = new ChartMouseListener() {

            @Override
            public void chartMouseClicked(final ChartMouseEvent event) {
                MouseEvent me = event.getTrigger();
                if (me.getID() == MouseEvent.MOUSE_CLICKED && me.getClickCount() == 1) {
                    try {
                        ChartEntity entity = event.getEntity();
                        // expand sections if interesting, back out if in nothing
                        if (entity instanceof PieSectionEntity) {
                            Account a = (Account) ((PieSectionEntity) entity).getSectionKey();
                            if (a.getChildCount() > 0) {
                                setCurrentAccount(a);
                            }
                        } else if (entity == null) {
                            Account parent = currentAccount;
                            if (parent == null) {
                                return;
                            }
                            parent = parent.getParent();
                            if (parent == null || parent instanceof RootAccount) {
                                return;
                            }
                            setCurrentAccount(parent);
                        }
                    } catch (final Exception e) {
                        Logger.getLogger(IncomeExpensePieChart.class.getName()).log(Level.SEVERE,
                                e.getLocalizedMessage(), e);
                    }
                }
            }

            @Override
            public void chartMouseMoved(ChartMouseEvent event) {
                setChartCursor(chartPanel, event.getEntity(), event.getTrigger().getPoint());
            }
        };

        chartPanel.addChartMouseListener(mouseListener);

        return panel;
    }

    private void setCurrentAccount(Account a) {
        try {
            if (a == null) {
                return;
            }
            currentAccount = a;

            chartPanel.setChart(createPieChart(a));
            chartPanel.validate();
            // refresh the cursor for changed display
            if (lastPoint != null) {
                setChartCursor(chartPanel, null, lastPoint);
            }
        } catch (Exception e) {
            Logger.getLogger(IncomeExpensePieChart.class.getName()).log(Level.SEVERE, e.getLocalizedMessage(), e);
        }
    }

    private void setChartCursor(final ChartPanel chartPanel, final ChartEntity e, final Point point) {
        lastPoint = point;

        EventQueue.invokeLater(new Runnable() {

            ChartEntity entity = e;

            @Override
            public void run() {
                if (entity == null && point != null) {
                    entity = chartPanel.getEntityForPoint(lastPoint.x, lastPoint.y);
                }
                Account parent = currentAccount;
                if (entity instanceof PieSectionEntity) {
                    // change cursor if section is interesting
                    Account a = (Account) ((PieSectionEntity) entity).getSectionKey();
                    if (a.getChildCount() > 0 && a != parent) {
                        chartPanel.setCursor(ZOOM_IN);
                    } else {
                        chartPanel.setCursor(Cursor.getDefaultCursor());
                    }
                    return;
                } else if (entity == null && parent != null) {
                    parent = parent.getParent();
                    if (parent != null && !(parent instanceof RootAccount)) {
                        chartPanel.setCursor(ZOOM_OUT);
                        return;
                    }
                }
                chartPanel.setCursor(Cursor.getDefaultCursor());
            }
        });
    }

    private JFreeChart createPieChart(final Account a) {
        final Engine engine = EngineFactory.getEngine(EngineFactory.DEFAULT);
        Objects.requireNonNull(engine);
        Objects.requireNonNull(a);

        PieDataset data = createPieDataSet(a);
        PiePlot plot = new PiePlot(data);

        // rebuilt each time because they're based on the account's commodity
        CurrencyNode defaultCurrency = engine.getDefaultCurrency();
        NumberFormat valueFormat = CommodityFormat.getFullNumberFormat(a.getCurrencyNode());
        NumberFormat percentFormat = new DecimalFormat("0.0#%");
        defaultLabels = new StandardPieSectionLabelGenerator("{0} = {1}", valueFormat, percentFormat);
        percentLabels = new StandardPieSectionLabelGenerator("{0} = {1}\n{2}", valueFormat, percentFormat);

        plot.setLabelGenerator(showPercentCheck.isSelected() ? percentLabels : defaultLabels);

        plot.setLabelGap(.02);
        plot.setInteriorGap(.1);

        // if we had to add a section for the account (because it has it's
        // own transactions, not just child accounts), separate it from children.
        if (data.getIndex(a) != -1) {
            plot.setExplodePercent(a, .10);
        }

        String title;

        // pick an appropriate title
        if (a.getAccountType() == AccountType.EXPENSE) {
            title = rb.getString("Title.PercentExpense");
        } else if (a.getAccountType() == AccountType.INCOME) {
            title = rb.getString("Title.PercentIncome");
        } else {
            title = rb.getString("Title.PercentDist");
        }

        title = title + " - " + a.getPathName();

        JFreeChart chart = new JFreeChart(title, JFreeChart.DEFAULT_TITLE_FONT, plot, false);

        BigDecimal total = a.getTreeBalance(startField.getLocalDate(), endField.getLocalDate()).abs();

        String subtitle = valueFormat.format(total);
        if (!defaultCurrency.equals(a.getCurrencyNode())) {
            BigDecimal totalDefaultCurrency = total.multiply(a.getCurrencyNode().getExchangeRate(defaultCurrency));
            NumberFormat defaultValueFormat = CommodityFormat.getFullNumberFormat(defaultCurrency);
            subtitle += "  -  " + defaultValueFormat.format(totalDefaultCurrency);
        }
        chart.addSubtitle(new TextTitle(subtitle));
        chart.setBackgroundPaint(null);

        return chart;
    }

    private PieDataset createPieDataSet(Account a) {
        DefaultPieDataset returnValue = new DefaultPieDataset();
        if (a != null) {

            BigDecimal total = a.getTreeBalance(startField.getLocalDate(), endField.getLocalDate(),
                    a.getCurrencyNode());

            // abs() on all values won't work if children aren't of uniform sign,
            // then again, this chart is not right to display those trees
            boolean negate = total != null && total.floatValue() < 0;

            // accounts may have balances independent of their children
            BigDecimal value = a.getBalance(startField.getLocalDate(), endField.getLocalDate());

            if (value.compareTo(BigDecimal.ZERO) != 0) {
                returnValue.setValue(a, negate ? value.negate() : value);
            }

            for (final Account child : a.getChildren(Comparators.getAccountByCode())) {
                value = child.getTreeBalance(startField.getLocalDate(), endField.getLocalDate(),
                        a.getCurrencyNode());

                if (showEmptyCheck.isSelected() || value.compareTo(BigDecimal.ZERO) != 0) {
                    returnValue.setValue(child, negate ? value.negate() : value);
                }
            }
        }
        return returnValue;
    }
}