    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
    006     *
    007     * Project Info:  http://www.jfree.org/jfreechart/index.html
    008     *
    009     * This library is free software; you can redistribute it and/or modify it
    010     * under the terms of the GNU Lesser General Public License as published by
    011     * the Free Software Foundation; either version 2.1 of the License, or
    012     * (at your option) any later version.
    013     *
    014     * This library is distributed in the hope that it will be useful, but
    015     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
    016     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
    017     * License for more details.
    018     *
    019     * You should have received a copy of the GNU Lesser General Public
    020     * License along with this library; if not, write to the Free Software
    021     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
    022     * USA.
    023     *
    024     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
    025     * in the United States and other countries.]
    026     *
    027     * --------------------
    028     * MultiplePiePlot.java
    029     * --------------------
    030     * (C) Copyright 2004-2007, by Object Refinery Limited.
    031     *
    032     * Original Author:  David Gilbert (for Object Refinery Limited);
    033     * Contributor(s):   -;
    034     *
    035     * $Id: MultiplePiePlot.java,v 1.12.2.8 2007/01/17 11:05:42 mungady Exp $
    036     *
    037     * Changes
    038     * -------
    039     * 29-Jan-2004 : Version 1 (DG);
    040     * 31-Mar-2004 : Added setPieIndex() call during drawing (DG);
    041     * 20-Apr-2005 : Small change for update to LegendItem constructors (DG);
    042     * 05-May-2005 : Updated draw() method parameters (DG);
    043     * 16-Jun-2005 : Added get/setDataset() and equals() methods (DG);
    044     * ------------- JFREECHART 1.0.x ---------------------------------------------
    045     * 06-Apr-2006 : Fixed bug 1190647 - legend and section colors not consistent
    046     *               when aggregation limit is specified (DG);
    047     * 27-Sep-2006 : Updated draw() method for deprecated code (DG);
    048     * 17-Jan-2007 : Updated prefetchSectionPaints() to check settings in
    049     *               underlying PiePlot (DG);
    050     *
    051     */
    052
    053    package org.jfree.chart.plot;
    054
    055    import java.awt.Color;
    056    import java.awt.Font;
    057    import java.awt.Graphics2D;
    058    import java.awt.Paint;
    059    import java.awt.Rectangle;
    060    import java.awt.geom.Point2D;
    061    import java.awt.geom.Rectangle2D;
    062    import java.io.IOException;
    063    import java.io.ObjectInputStream;
    064    import java.io.ObjectOutputStream;
    065    import java.io.Serializable;
    066    import java.util.HashMap;
    067    import java.util.Iterator;
    068    import java.util.List;
    069    import java.util.Map;
    070
    071    import org.jfree.chart.ChartRenderingInfo;
    072    import org.jfree.chart.JFreeChart;
    073    import org.jfree.chart.LegendItem;
    074    import org.jfree.chart.LegendItemCollection;
    075    import org.jfree.chart.event.PlotChangeEvent;
    076    import org.jfree.chart.title.TextTitle;
    077    import org.jfree.data.category.CategoryDataset;
    078    import org.jfree.data.category.CategoryToPieDataset;
    079    import org.jfree.data.general.DatasetChangeEvent;
    080    import org.jfree.data.general.DatasetUtilities;
    081    import org.jfree.data.general.PieDataset;
    082    import org.jfree.io.SerialUtilities;
    083    import org.jfree.ui.RectangleEdge;
    084    import org.jfree.ui.RectangleInsets;
    085    import org.jfree.util.ObjectUtilities;
    086    import org.jfree.util.PaintUtilities;
    087    import org.jfree.util.TableOrder;
    088
    089    /**
    090     * A plot that displays multiple pie plots using data from a
    091     * {@link CategoryDataset}.
    092     */
    093    public class MultiplePiePlot extends Plot implements Cloneable, Serializable {
    094
    095        /** For serialization. */
    096        private static final long serialVersionUID = -355377800470807389L;
    097
    098        /** The chart object that draws the individual pie charts. */
    099        private JFreeChart pieChart;
    100
    101        /** The dataset. */
    102        private CategoryDataset dataset;
    103
    104        /** The data extract order (by row or by column). */
    105        private TableOrder dataExtractOrder;
    106
    107        /** The pie section limit percentage. */
    108        private double limit = 0.0;
    109
    110        /**
    111         * The key for the aggregated items.
    112         * @since 1.0.2
    113         */
    114        private Comparable aggregatedItemsKey;
    115
    116        /**
    117         * The paint for the aggregated items.
    118         * @since 1.0.2
    119         */
    120        private transient Paint aggregatedItemsPaint;
    121
    122        /**
    123         * The colors to use for each section.
    124         * @since 1.0.2
    125         */
    126        private transient Map sectionPaints;
    127
    128        /**
    129         * Creates a new plot with no data.
    130         */
    131        public MultiplePiePlot() {
    132            this(null);
    133        }
    134
    135        /**
    136         * Creates a new plot.
    137         *
    138         * @param dataset  the dataset (<code>null</code> permitted).
    139         */
    140        public MultiplePiePlot(CategoryDataset dataset) {
    141            super();
    142            this.dataset = dataset;
    143            PiePlot piePlot = new PiePlot(null);
    144            this.pieChart = new JFreeChart(piePlot);
    145            this.pieChart.removeLegend();
    146            this.dataExtractOrder = TableOrder.BY_COLUMN;
    147            this.pieChart.setBackgroundPaint(null);
    148            TextTitle seriesTitle = new TextTitle("Series Title",
    149                    new Font("SansSerif", Font.BOLD, 12));
    150            seriesTitle.setPosition(RectangleEdge.BOTTOM);
    151            this.pieChart.setTitle(seriesTitle);
    152            this.aggregatedItemsKey = "Other";
    153            this.aggregatedItemsPaint = Color.lightGray;
    154            this.sectionPaints = new HashMap();
    155        }
    156
    157        /**
    158         * Returns the dataset used by the plot.
    159         *
    160         * @return The dataset (possibly <code>null</code>).
    161         */
    162        public CategoryDataset getDataset() {
    163            return this.dataset;
    164        }
    165
    166        /**
    167         * Sets the dataset used by the plot and sends a {@link PlotChangeEvent}
    168         * to all registered listeners.
    169         *
    170         * @param dataset  the dataset (<code>null</code> permitted).
    171         */
    172        public void setDataset(CategoryDataset dataset) {
    173            // if there is an existing dataset, remove the plot from the list of
    174            // change listeners...
    175            if (this.dataset != null) {
    176                this.dataset.removeChangeListener(this);
    177            }
    178
    179            // set the new dataset, and register the chart as a change listener...
    180            this.dataset = dataset;
    181            if (dataset != null) {
    182                setDatasetGroup(dataset.getGroup());
    183                dataset.addChangeListener(this);
    184            }
    185
    186            // send a dataset change event to self to trigger plot change event
    187            datasetChanged(new DatasetChangeEvent(this, dataset));
    188        }
    189
    190        /**
    191         * Returns the pie chart that is used to draw the individual pie plots.
    192         *
    193         * @return The pie chart.
    194         */
    195        public JFreeChart getPieChart() {
    196            return this.pieChart;
    197        }
    198
    199        /**
    200         * Sets the chart that is used to draw the individual pie plots.
    201         *
    202         * @param pieChart  the pie chart.
    203         */
    204        public void setPieChart(JFreeChart pieChart) {
    205            this.pieChart = pieChart;
    206            notifyListeners(new PlotChangeEvent(this));
    207        }
    208
    209        /**
    210         * Returns the data extract order (by row or by column).
    211         *
    212         * @return The data extract order (never <code>null</code>).
    213         */
    214        public TableOrder getDataExtractOrder() {
    215            return this.dataExtractOrder;
    216        }
    217
    218        /**
    219         * Sets the data extract order (by row or by column) and sends a
    220         * {@link PlotChangeEvent} to all registered listeners.
    221         *
    222         * @param order  the order (<code>null</code> not permitted).
    223         */
    224        public void setDataExtractOrder(TableOrder order) {
    225            if (order == null) {
    226                throw new IllegalArgumentException("Null 'order' argument");
    227            }
    228            this.dataExtractOrder = order;
    229            notifyListeners(new PlotChangeEvent(this));
    230        }
    231
    232        /**
    233         * Returns the limit (as a percentage) below which small pie sections are
    234         * aggregated.
    235         *
    236         * @return The limit percentage.
    237         */
    238        public double getLimit() {
    239            return this.limit;
    240        }
    241
    242        /**
    243         * Sets the limit below which pie sections are aggregated.
    244         * Set this to 0.0 if you don't want any aggregation to occur.
    245         *
    246         * @param limit  the limit percent.
    247         */
    248        public void setLimit(double limit) {
    249            this.limit = limit;
    250            notifyListeners(new PlotChangeEvent(this));
    251        }
    252
    253        /**
    254         * Returns the key for aggregated items in the pie plots, if there are any.
    255         * The default value is "Other".
    256         *
    257         * @return The aggregated items key.
    258         *
    259         * @since 1.0.2
    260         */
    261        public Comparable getAggregatedItemsKey() {
    262            return this.aggregatedItemsKey;
    263        }
    264
    265        /**
    266         * Sets the key for aggregated items in the pie plots.  You must ensure
    267         * that this doesn't clash with any keys in the dataset.
    268         *
    269         * @param key  the key (<code>null</code> not permitted).
    270         *
    271         * @since 1.0.2
    272         */
    273        public void setAggregatedItemsKey(Comparable key) {
    274            if (key == null) {
    275                throw new IllegalArgumentException("Null 'key' argument.");
    276            }
    277            this.aggregatedItemsKey = key;
    278            notifyListeners(new PlotChangeEvent(this));
    279        }
    280
    281        /**
    282         * Returns the paint used to draw the pie section representing the
    283         * aggregated items.  The default value is <code>Color.lightGray</code>.
    284         *
    285         * @return The paint.
    286         *
    287         * @since 1.0.2
    288         */
    289        public Paint getAggregatedItemsPaint() {
    290            return this.aggregatedItemsPaint;
    291        }
    292
    293        /**
    294         * Sets the paint used to draw the pie section representing the aggregated
    295         * items and sends a {@link PlotChangeEvent} to all registered listeners.
    296         *
    297         * @param paint  the paint (<code>null</code> not permitted).
    298         *
    299         * @since 1.0.2
    300         */
    301        public void setAggregatedItemsPaint(Paint paint) {
    302            if (paint == null) {
    303                throw new IllegalArgumentException("Null 'paint' argument.");
    304            }
    305            this.aggregatedItemsPaint = paint;
    306            notifyListeners(new PlotChangeEvent(this));
    307        }
    308
    309        /**
    310         * Returns a short string describing the type of plot.
    311         *
    312         * @return The plot type.
    313         */
    314        public String getPlotType() {
    315            return "Multiple Pie Plot";
    316             // TODO: need to fetch this from localised resources
    317        }
    318
    319        /**
    320         * Draws the plot on a Java 2D graphics device (such as the screen or a
    321         * printer).
    322         *
    323         * @param g2  the graphics device.
    324         * @param area  the area within which the plot should be drawn.
    325         * @param anchor  the anchor point (<code>null</code> permitted).
    326         * @param parentState  the state from the parent plot, if there is one.
    327         * @param info  collects info about the drawing.
    328         */
    329        public void draw(Graphics2D g2,
    330                         Rectangle2D area,
    331                         Point2D anchor,
    332                         PlotState parentState,
    333                         PlotRenderingInfo info) {
    334
    335
    336            // adjust the drawing area for the plot insets (if any)...
    337            RectangleInsets insets = getInsets();
    338            insets.trim(area);
    339            drawBackground(g2, area);
    340            drawOutline(g2, area);
    341
    342            // check that there is some data to display...
    343            if (DatasetUtilities.isEmptyOrNull(this.dataset)) {
    344                drawNoDataMessage(g2, area);
    345                return;
    346            }
    347
    348            int pieCount = 0;
    349            if (this.dataExtractOrder == TableOrder.BY_ROW) {
    350                pieCount = this.dataset.getRowCount();
    351            }
    352            else {
    353                pieCount = this.dataset.getColumnCount();
    354            }
    355
    356            // the columns variable is always >= rows
    357            int displayCols = (int) Math.ceil(Math.sqrt(pieCount));
    358            int displayRows
    359                = (int) Math.ceil((double) pieCount / (double) displayCols);
    360
    361            // swap rows and columns to match plotArea shape
    362            if (displayCols > displayRows && area.getWidth() < area.getHeight()) {
    363                int temp = displayCols;
    364                displayCols = displayRows;
    365                displayRows = temp;
    366            }
    367
    368            prefetchSectionPaints();
    369
    370            int x = (int) area.getX();
    371            int y = (int) area.getY();
    372            int width = ((int) area.getWidth()) / displayCols;
    373            int height = ((int) area.getHeight()) / displayRows;
    374            int row = 0;
    375            int column = 0;
    376            int diff = (displayRows * displayCols) - pieCount;
    377            int xoffset = 0;
    378            Rectangle rect = new Rectangle();
    379
    380            for (int pieIndex = 0; pieIndex < pieCount; pieIndex++) {
    381                rect.setBounds(x + xoffset + (width * column), y + (height * row),
    382                        width, height);
    383
    384                String title = null;
    385                if (this.dataExtractOrder == TableOrder.BY_ROW) {
    386                    title = this.dataset.getRowKey(pieIndex).toString();
    387                }
    388                else {
    389                    title = this.dataset.getColumnKey(pieIndex).toString();
    390                }
    391                this.pieChart.setTitle(title);
    392
    393                PieDataset piedataset = null;
    394                PieDataset dd = new CategoryToPieDataset(this.dataset,
    395                        this.dataExtractOrder, pieIndex);
    396                if (this.limit > 0.0) {
    397                    piedataset = DatasetUtilities.createConsolidatedPieDataset(
    398                            dd, this.aggregatedItemsKey, this.limit);
    399                }
    400                else {
    401                    piedataset = dd;
    402                }
    403                PiePlot piePlot = (PiePlot) this.pieChart.getPlot();
    404                piePlot.setDataset(piedataset);
    405                piePlot.setPieIndex(pieIndex);
    406
    407                // update the section colors to match the global colors...
    408                for (int i = 0; i < piedataset.getItemCount(); i++) {
    409                    Comparable key = piedataset.getKey(i);
    410                    Paint p;
    411                    if (key.equals(this.aggregatedItemsKey)) {
    412                        p = this.aggregatedItemsPaint;
    413                    }
    414                    else {
    415                        p = (Paint) this.sectionPaints.get(key);
    416                    }
    417                    piePlot.setSectionPaint(key, p);
    418                }
    419
    420                ChartRenderingInfo subinfo = null;
    421                if (info != null) {
    422                    subinfo = new ChartRenderingInfo();
    423                }
    424                this.pieChart.draw(g2, rect, subinfo);
    425                if (info != null) {
    426                    info.getOwner().getEntityCollection().addAll(
    427                            subinfo.getEntityCollection());
    428                    info.addSubplotInfo(subinfo.getPlotInfo());
    429                }
    430
    431                ++column;
    432                if (column == displayCols) {
    433                    column = 0;
    434                    ++row;
    435
    436                    if (row == displayRows - 1 && diff != 0) {
    437                        xoffset = (diff * width) / 2;
    438                    }
    439                }
    440            }
    441
    442        }
    443
    444        /**
    445         * For each key in the dataset, check the <code>sectionPaints</code>
    446         * cache to see if a paint is associated with that key and, if not,
    447         * fetch one from the drawing supplier.  These colors are cached so that
    448         * the legend and all the subplots use consistent colors.
    449         */
    450        private void prefetchSectionPaints() {
    451
    452            // pre-fetch the colors for each key...this is because the subplots
    453            // may not display every key, but we need the coloring to be
    454            // consistent...
    455
    456            PiePlot piePlot = (PiePlot) getPieChart().getPlot();
    457
    458            if (this.dataExtractOrder == TableOrder.BY_ROW) {
    459                // column keys provide potential keys for individual pies
    460                for (int c = 0; c < this.dataset.getColumnCount(); c++) {
    461                    Comparable key = this.dataset.getColumnKey(c);
    462                    Paint p = piePlot.getSectionPaint(key);
    463                    if (p == null) {
    464                        p = (Paint) this.sectionPaints.get(key);
    465                        if (p == null) {
    466                            p = getDrawingSupplier().getNextPaint();
    467                        }
    468                    }
    469                    this.sectionPaints.put(key, p);
    470                }
    471            }
    472            else {
    473                // row keys provide potential keys for individual pies
    474                for (int r = 0; r < this.dataset.getRowCount(); r++) {
    475                    Comparable key = this.dataset.getRowKey(r);
    476                    Paint p = piePlot.getSectionPaint(key);
    477                    if (p == null) {
    478                        p = (Paint) this.sectionPaints.get(key);
    479                        if (p == null) {
    480                            p = getDrawingSupplier().getNextPaint();
    481                        }
    482                    }
    483                    this.sectionPaints.put(key, p);
    484                }
    485            }
    486
    487        }
    488
    489        /**
    490         * Returns a collection of legend items for the pie chart.
    491         *
    492         * @return The legend items.
    493         */
    494        public LegendItemCollection getLegendItems() {
    495
    496            LegendItemCollection result = new LegendItemCollection();
    497
    498            if (this.dataset != null) {
    499                List keys = null;
    500
    501                prefetchSectionPaints();
    502                if (this.dataExtractOrder == TableOrder.BY_ROW) {
    503                    keys = this.dataset.getColumnKeys();
    504                }
    505                else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
    506                    keys = this.dataset.getRowKeys();
    507                }
    508
    509                if (keys != null) {
    510                    int section = 0;
    511                    Iterator iterator = keys.iterator();
    512                    while (iterator.hasNext()) {
    513                        Comparable key = (Comparable) iterator.next();
    514                        String label = key.toString();
    515                        String description = label;
    516                        Paint paint = (Paint) this.sectionPaints.get(key);
    517                        LegendItem item = new LegendItem(label, description,
    518                                null, null, Plot.DEFAULT_LEGEND_ITEM_CIRCLE,
    519                                paint, Plot.DEFAULT_OUTLINE_STROKE, paint);
    520
    521                        result.add(item);
    522                        section++;
    523                    }
    524                }
    525                if (this.limit > 0.0) {
    526                    result.add(new LegendItem(this.aggregatedItemsKey.toString(),
    527                            this.aggregatedItemsKey.toString(), null, null,
    528                            Plot.DEFAULT_LEGEND_ITEM_CIRCLE,
    529                            this.aggregatedItemsPaint,
    530                            Plot.DEFAULT_OUTLINE_STROKE,
    531                            this.aggregatedItemsPaint));
    532                }
    533            }
    534            return result;
    535        }
    536
    537        /**
    538         * Tests this plot for equality with an arbitrary object.  Note that the
    539         * plot's dataset is not considered in the equality test.
    540         *
    541         * @param obj  the object (<code>null</code> permitted).
    542         *
    543         * @return <code>true</code> if this plot is equal to <code>obj</code>, and
    544         *     <code>false</code> otherwise.
    545         */
    546        public boolean equals(Object obj) {
    547            if (obj == this) {
    548                return true;
    549            }
    550            if (!(obj instanceof MultiplePiePlot)) {
    551                return false;
    552            }
    553            MultiplePiePlot that = (MultiplePiePlot) obj;
    554            if (this.dataExtractOrder != that.dataExtractOrder) {
    555                return false;
    556            }
    557            if (this.limit != that.limit) {
    558                return false;
    559            }
    560            if (!this.aggregatedItemsKey.equals(that.aggregatedItemsKey)) {
    561                return false;
    562            }
    563            if (!PaintUtilities.equal(this.aggregatedItemsPaint,
    564                    that.aggregatedItemsPaint)) {
    565                return false;
    566            }
    567            if (!ObjectUtilities.equal(this.pieChart, that.pieChart)) {
    568                return false;
    569            }
    570            if (!super.equals(obj)) {
    571                return false;
    572            }
    573            return true;
    574        }
    575
    576        /**
    577         * Provides serialization support.
    578         *
    579         * @param stream  the output stream.
    580         *
    581         * @throws IOException  if there is an I/O error.
    582         */
    583        private void writeObject(ObjectOutputStream stream) throws IOException {
    584            stream.defaultWriteObject();
    585            SerialUtilities.writePaint(this.aggregatedItemsPaint, stream);
    586        }
    587
    588        /**
    589         * Provides serialization support.
    590         *
    591         * @param stream  the input stream.
    592         *
    593         * @throws IOException  if there is an I/O error.
    594         * @throws ClassNotFoundException  if there is a classpath problem.
    595         */
    596        private void readObject(ObjectInputStream stream)
    597            throws IOException, ClassNotFoundException {
    598            stream.defaultReadObject();
    599            this.aggregatedItemsPaint = SerialUtilities.readPaint(stream);
    600            this.sectionPaints = new HashMap();
    601        }
    602
    603
    604    }