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