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