jmemorize.gui.swing.panels.DeckChartPanel.java Source code

Java tutorial

Introduction

Here is the source code for jmemorize.gui.swing.panels.DeckChartPanel.java

Source

/*
 * jMemorize - Learning made easy (and fun) - A Leitner flashcards tool
 * Copyright(C) 2004-2008 Riad Djemili and contributors
 * 
 * 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 1, 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, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package jmemorize.gui.swing.panels;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.swing.JPanel;
import javax.swing.border.EmptyBorder;

import jmemorize.core.Card;
import jmemorize.core.Category;
import jmemorize.core.CategoryObserver;
import jmemorize.gui.LC;
import jmemorize.gui.Localization;
import jmemorize.gui.swing.ColorConstants;
import jmemorize.gui.swing.frames.MainFrame;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartMouseEvent;
import org.jfree.chart.ChartMouseListener;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.TickUnitSource;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.entity.CategoryItemEntity;
import org.jfree.chart.entity.ChartEntity;
import org.jfree.chart.event.RendererChangeEvent;
import org.jfree.chart.labels.ItemLabelAnchor;
import org.jfree.chart.labels.ItemLabelPosition;
import org.jfree.chart.labels.StandardCategoryItemLabelGenerator;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.CategoryItemRendererState;
import org.jfree.chart.renderer.category.StackedBarRenderer3D;
import org.jfree.chart.title.LegendTitle;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.ui.TextAnchor;

/**
 * This is the panel that is being displayed up the upper part of jMemorize all
 * of the time. It shows a visual representation of the current card
 * distribution among all decks in form of a 3D stacked bar chart.
 * 
 * @author djemili
 */
public class DeckChartPanel extends JPanel implements CategoryObserver {
    /**
     * A mouse listener for clicks on the chart. If a bar is clicked the view
     * changes to the selected deck.
     */
    private class MouseClicked implements ChartMouseListener {
        /* (non-Javadoc)
         * @see org.jfree.chart.ChartMouseListener
         */
        public void chartMouseClicked(ChartMouseEvent evt) {
            ChartEntity entity = evt.getEntity();
            if (entity instanceof CategoryItemEntity) {
                int cat = ((CategoryItemEntity) entity).getCategoryIndex();
                m_frame.setDeck(cat - 1);
            }
        }

        /* (non-Javadoc)
         * @see org.jfree.chart.ChartMouseListener
         */
        public void chartMouseMoved(ChartMouseEvent arg0) {
            // do nothing
        }
    }

    private class MyBarRenderer extends StackedBarRenderer3D {
        private int m_deck = -2; // HACK
        private Font m_defaultFont;
        private Font m_boldFont;

        MyBarRenderer() {
            m_defaultFont = getBaseItemLabelFont();
            m_boldFont = getBaseItemLabelFont().deriveFont(Font.BOLD);
        }

        public void drawItem(Graphics2D g, CategoryItemRendererState state, Rectangle2D dataArea, CategoryPlot plot,
                CategoryAxis domainAxis, ValueAxis rangeAxis, CategoryDataset data, int row, int column, int pass) {
            if (column - 1 == m_deck && m_category.getCards(m_deck).size() > 0) {
                setOutlinePaint(ColorConstants.SELECTION_COLOR, false);
                setBaseItemLabelFont(m_boldFont, false);
                setItemLabelFont(m_boldFont, false);
            } else {
                setOutlinePaint(Color.WHITE, false);
                setBaseItemLabelFont(m_defaultFont, false);
                setItemLabelFont(m_defaultFont, false);
            }

            //            domainAxis.setCategoryMargin(0.2 + (0.011 * getMinNumDecks()));

            super.drawItem(g, state, dataArea, plot, domainAxis, rangeAxis, data, row, column, pass);
        }

        public void setSelectedDeck(int level) {
            m_deck = level;

            notifyListeners(new RendererChangeEvent(this));
        }

        /*
         * NOTE this is a workaround. JFreeChart appears to insist on drawing
         * bars even if they are of zero width. To prevent the top of the
         * stacked bar chart from being the wrong color, we need to skip bars
         * that are zero width. Here, we intercept the draw call to remove any
         * zero width blocks in the stack before drawing
         */
        protected void drawStackVertical(List values, Comparable category, Graphics2D g2,
                CategoryItemRendererState state, Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis,
                ValueAxis rangeAxis, CategoryDataset dataset) {
            List prunedValues = new ArrayList(values);
            double lastValue = 0.0;

            Iterator it = prunedValues.iterator();
            while (it.hasNext()) {
                Object[] pair = (Object[]) it.next();
                double thisValue = ((Double) pair[1]).doubleValue();
                double delta = thisValue - lastValue;

                if (pair[0] != null) {
                    if (delta == 0.0)
                        it.remove();
                }

                lastValue = thisValue;
            }

            super.drawStackVertical(prunedValues, category, g2, state, dataArea, plot, domainAxis, rangeAxis,
                    dataset);
        }
    }

    // TODO make minimum deck bars dependent on screen resolution

    private final static String DECK0_NAME = Localization.get("DeckChart.START_DECK"); //$NON-NLS-1$
    private final static String SUMMARY_BAR_NAME = Localization.get("DeckChart.SUMMARY"); //$NON-NLS-1$

    private final static String LEARNED_CARDS_ROW = Localization.get("DeckChart.LEARNED_CARDS");
    private final static String EXPIRED_CARDS_ROW = Localization.get("DeckChart.EXPIRED_CARDS");
    private final static String UNLEARNED_CARDS_ROW = Localization.get("DeckChart.UNLEARNED_CARDS");

    /**
    * @uml.property  name="m_category"
    * @uml.associationEnd  
    */
    private Category m_category;

    /**
    * @uml.property  name="m_frame"
    * @uml.associationEnd  multiplicity="(1 1)" inverse="m_deckChartPanel:jmemorize.gui.swing.frames.MainFrame"
    */
    private MainFrame m_frame;

    /**
    * @uml.property  name="m_dataset"
    * @uml.associationEnd  
    */
    private DefaultCategoryDataset m_dataset;
    /**
    * @uml.property  name="m_chartPanel"
    * @uml.associationEnd  multiplicity="(1 1)"
    */
    private ChartPanel m_chartPanel;
    /**
    * @uml.property  name="m_barRenderer"
    * @uml.associationEnd  inverse="this$0:jmemorize.gui.swing.panels.DeckChartPanel$MyBarRenderer"
    */
    private MyBarRenderer m_barRenderer;

    public DeckChartPanel(MainFrame mainFrame) {
        m_frame = mainFrame;

        initComponents();
    }

    public void setCategory(Category category) {
        if (m_category != null) {
            m_category.removeObserver(this);
        }

        m_category = category;
        category.addObserver(this);

        createDataset();
    }

    public void setDeck(int level) {
        m_barRenderer.setSelectedDeck(level);
    }

    /* (non-Javadoc)
     * @see jmemorize.core.CategoryObserver
     */
    public void onCategoryEvent(int type, Category category) {
        // ignore. mainframe already looks for important category changes
    }

    /* (non-Javadoc)
     * @see jmemorize.core.CategoryObserver
     */
    public void onCardEvent(int type, Card card, Category category, int level) {
        updateBars();
    }

    private JFreeChart createChart() {
        m_dataset = createDefaultDataSet();

        JFreeChart chart = ChartFactory.createStackedBarChart3D(null, // chart title
                null, // domain axis label
                Localization.get("DeckChart.CARDS"), // range axis label //$NON-NLS-1$
                m_dataset, // data
                PlotOrientation.VERTICAL, // the plot orientation
                true, // include legend
                true, // tooltips
                false // urls
        );

        // setup legend
        // TODO we used to do this for the old jfreechar version, but it's not clear why.
        // can we get rid of it?
        LegendTitle legend = chart.getLegend();
        //        legend.setsetRenderingOrder(LegendRenderingOrder.REVERSE);
        legend.setItemFont(legend.getItemFont().deriveFont(11f));

        // setup plot
        CategoryPlot plot = (CategoryPlot) chart.getPlot();
        TickUnitSource tickUnits = NumberAxis.createIntegerTickUnits();
        plot.getRangeAxis().setStandardTickUnits(tickUnits); //CHECK use locale
        plot.setForegroundAlpha(0.99f);

        // setup renderer
        m_barRenderer = new MyBarRenderer();
        m_barRenderer.setItemLabelGenerator(new StandardCategoryItemLabelGenerator());
        m_barRenderer.setItemLabelsVisible(true);
        m_barRenderer
                .setPositiveItemLabelPosition(new ItemLabelPosition(ItemLabelAnchor.CENTER, TextAnchor.CENTER));
        plot.setRenderer(m_barRenderer);
        setSeriesPaint();

        return chart;
    }

    private void createDataset() {
        m_dataset = createDefaultDataSet();
        updateBars();
        CategoryPlot plot = (CategoryPlot) m_chartPanel.getChart().getPlot();
        plot.setDataset(m_dataset);
    }

    private void initComponents() {
        // add the chart to a panel...
        m_chartPanel = new ChartPanel(createChart());
        m_chartPanel.addChartMouseListener(new MouseClicked());

        m_chartPanel.setMinimumDrawHeight(100);
        m_chartPanel.setMinimumDrawWidth(400);

        m_chartPanel.setMaximumDrawHeight(1600);
        m_chartPanel.setMaximumDrawWidth(10000);

        setLayout(new BorderLayout());
        setBorder(new EmptyBorder(10, 2, 2, 2));
        add(m_chartPanel);

        addComponentListener(new ComponentAdapter() {
            public void componentResized(ComponentEvent e) {
                if (m_category != null)
                    updateBars();
            }
        });
    }

    private DefaultCategoryDataset createDefaultDataSet() {
        DefaultCategoryDataset dataset = new DefaultCategoryDataset();

        setValues(dataset, SUMMARY_BAR_NAME, 0, 0, 0);
        for (int i = 0; i < getMinNumDecks(); i++) {
            setValues(dataset, getDeckLabel(i), 0, 0, 0);
        }

        return dataset;
    }

    private void updateBars() //CHECK put Dataset as argument!?
    {
        updateSummaryBar();

        while (m_dataset.getColumnCount() > getNumDecks()) {
            m_dataset.removeColumn(m_dataset.getColumnCount() - 1);
        }

        for (int i = 0; i < getNumDecks() - 1; i++) {
            updateBar(i);
        }
    }

    private void updateSummaryBar() {
        int learned = m_category.getLearnedCards().size();
        int expired = m_category.getExpiredCards().size();
        int unlearned = m_category.getUnlearnedCards().size();

        setValues(m_dataset, SUMMARY_BAR_NAME, unlearned, expired, learned);
    }

    private void updateBar(int level) {
        if (level == 0) {
            int unlearnedCards = m_category.getCards(level).size();
            setValues(m_dataset, DECK0_NAME, unlearnedCards, 0, 0);
        } else {
            String deckLabel = getDeckLabel(level);
            if (level >= m_category.getNumberOfDecks()) {
                setValues(m_dataset, deckLabel, 0, 0, 0);
            } else {
                int learnedCards = m_category.getLearnedCards(level).size();
                int expiredCards = m_category.getExpiredCards(level).size();

                setValues(m_dataset, deckLabel, 0, expiredCards, learnedCards);
            }
        }
    }

    /**
     * Sets the values for the column with given id. This method also handles
     * the order in which the rows will appear in the column.
     */
    private void setValues(DefaultCategoryDataset dataset, String column, int unlearned, int expired, int learned) {
        // if you change the order of the rows, don't forget to also update the
        // method setSeriesPaint

        dataset.setValue(unlearned, UNLEARNED_CARDS_ROW, column);
        dataset.setValue(expired, EXPIRED_CARDS_ROW, column);
        dataset.setValue(learned, LEARNED_CARDS_ROW, column);
    }

    private String getDeckLabel(int level) {
        return (level == 0) ? DECK0_NAME : Localization.get(LC.DECK) + " " + level; //$NON-NLS-1$
    }

    private int getNumDecks() {
        return Math.max(m_category.getNumberOfDecks(), getMinNumDecks()) + 1;
    }

    /**
     * The minimal amount of deck bars show at all time exclusive the summary
     * bar.
     */
    private int getMinNumDecks() {
        int width = getSize().width - 250;
        return width / 135;
    }

    private void setSeriesPaint() {
        m_barRenderer.setSeriesPaint(0, ColorConstants.UNLEARNED_CARDS);
        m_barRenderer.setSeriesPaint(1, ColorConstants.EXPIRED_CARDS);
        m_barRenderer.setSeriesPaint(2, ColorConstants.LEARNED_CARDS);
    }
}