    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     * 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     *
    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 (never <code>null</code>).
    194         *
    195         * @see #setPieChart(JFreeChart)
    196         */
    197        public JFreeChart getPieChart() {
    198            return this.pieChart;
    199        }
    200
    201        /**
    202         * Sets the chart that is used to draw the individual pie plots.  The
    203         * chart's plot must be an instance of {@link PiePlot}.
    204         *
    205         * @param pieChart  the pie chart (<code>null</code> not permitted).
    206         *
    207         * @see #getPieChart()
    208         */
    209        public void setPieChart(JFreeChart pieChart) {
    210            if (pieChart == null) {
    211                throw new IllegalArgumentException("Null 'pieChart' argument.");
    212            }
    213            if (!(pieChart.getPlot() instanceof PiePlot)) {
    214                throw new IllegalArgumentException("The 'pieChart' argument must "
    215                        + "be a chart based on a PiePlot.");
    216            }
    217            this.pieChart = pieChart;
    218            notifyListeners(new PlotChangeEvent(this));
    219        }
    220
    221        /**
    222         * Returns the data extract order (by row or by column).
    223         *
    224         * @return The data extract order (never <code>null</code>).
    225         */
    226        public TableOrder getDataExtractOrder() {
    227            return this.dataExtractOrder;
    228        }
    229
    230        /**
    231         * Sets the data extract order (by row or by column) and sends a
    232         * {@link PlotChangeEvent} to all registered listeners.
    233         *
    234         * @param order  the order (<code>null</code> not permitted).
    235         */
    236        public void setDataExtractOrder(TableOrder order) {
    237            if (order == null) {
    238                throw new IllegalArgumentException("Null 'order' argument");
    239            }
    240            this.dataExtractOrder = order;
    241            notifyListeners(new PlotChangeEvent(this));
    242        }
    243
    244        /**
    245         * Returns the limit (as a percentage) below which small pie sections are
    246         * aggregated.
    247         *
    248         * @return The limit percentage.
    249         */
    250        public double getLimit() {
    251            return this.limit;
    252        }
    253
    254        /**
    255         * Sets the limit below which pie sections are aggregated.
    256         * Set this to 0.0 if you don't want any aggregation to occur.
    257         *
    258         * @param limit  the limit percent.
    259         */
    260        public void setLimit(double limit) {
    261            this.limit = limit;
    262            notifyListeners(new PlotChangeEvent(this));
    263        }
    264
    265        /**
    266         * Returns the key for aggregated items in the pie plots, if there are any.
    267         * The default value is "Other".
    268         *
    269         * @return The aggregated items key.
    270         *
    271         * @since 1.0.2
    272         */
    273        public Comparable getAggregatedItemsKey() {
    274            return this.aggregatedItemsKey;
    275        }
    276
    277        /**
    278         * Sets the key for aggregated items in the pie plots.  You must ensure
    279         * that this doesn't clash with any keys in the dataset.
    280         *
    281         * @param key  the key (<code>null</code> not permitted).
    282         *
    283         * @since 1.0.2
    284         */
    285        public void setAggregatedItemsKey(Comparable key) {
    286            if (key == null) {
    287                throw new IllegalArgumentException("Null 'key' argument.");
    288            }
    289            this.aggregatedItemsKey = key;
    290            notifyListeners(new PlotChangeEvent(this));
    291        }
    292
    293        /**
    294         * Returns the paint used to draw the pie section representing the
    295         * aggregated items.  The default value is <code>Color.lightGray</code>.
    296         *
    297         * @return The paint.
    298         *
    299         * @since 1.0.2
    300         */
    301        public Paint getAggregatedItemsPaint() {
    302            return this.aggregatedItemsPaint;
    303        }
    304
    305        /**
    306         * Sets the paint used to draw the pie section representing the aggregated
    307         * items and sends a {@link PlotChangeEvent} to all registered listeners.
    308         *
    309         * @param paint  the paint (<code>null</code> not permitted).
    310         *
    311         * @since 1.0.2
    312         */
    313        public void setAggregatedItemsPaint(Paint paint) {
    314            if (paint == null) {
    315                throw new IllegalArgumentException("Null 'paint' argument.");
    316            }
    317            this.aggregatedItemsPaint = paint;
    318            notifyListeners(new PlotChangeEvent(this));
    319        }
    320
    321        /**
    322         * Returns a short string describing the type of plot.
    323         *
    324         * @return The plot type.
    325         */
    326        public String getPlotType() {
    327            return "Multiple Pie Plot";
    328             // TODO: need to fetch this from localised resources
    329        }
    330
    331        /**
    332         * Draws the plot on a Java 2D graphics device (such as the screen or a
    333         * printer).
    334         *
    335         * @param g2  the graphics device.
    336         * @param area  the area within which the plot should be drawn.
    337         * @param anchor  the anchor point (<code>null</code> permitted).
    338         * @param parentState  the state from the parent plot, if there is one.
    339         * @param info  collects info about the drawing.
    340         */
    341        public void draw(Graphics2D g2,
    342                         Rectangle2D area,
    343                         Point2D anchor,
    344                         PlotState parentState,
    345                         PlotRenderingInfo info) {
    346
    347
    348            // adjust the drawing area for the plot insets (if any)...
    349            RectangleInsets insets = getInsets();
    350            insets.trim(area);
    351            drawBackground(g2, area);
    352            drawOutline(g2, area);
    353
    354            // check that there is some data to display...
    355            if (DatasetUtilities.isEmptyOrNull(this.dataset)) {
    356                drawNoDataMessage(g2, area);
    357                return;
    358            }
    359
    360            int pieCount = 0;
    361            if (this.dataExtractOrder == TableOrder.BY_ROW) {
    362                pieCount = this.dataset.getRowCount();
    363            }
    364            else {
    365                pieCount = this.dataset.getColumnCount();
    366            }
    367
    368            // the columns variable is always >= rows
    369            int displayCols = (int) Math.ceil(Math.sqrt(pieCount));
    370            int displayRows
    371                = (int) Math.ceil((double) pieCount / (double) displayCols);
    372
    373            // swap rows and columns to match plotArea shape
    374            if (displayCols > displayRows && area.getWidth() < area.getHeight()) {
    375                int temp = displayCols;
    376                displayCols = displayRows;
    377                displayRows = temp;
    378            }
    379
    380            prefetchSectionPaints();
    381
    382            int x = (int) area.getX();
    383            int y = (int) area.getY();
    384            int width = ((int) area.getWidth()) / displayCols;
    385            int height = ((int) area.getHeight()) / displayRows;
    386            int row = 0;
    387            int column = 0;
    388            int diff = (displayRows * displayCols) - pieCount;
    389            int xoffset = 0;
    390            Rectangle rect = new Rectangle();
    391
    392            for (int pieIndex = 0; pieIndex < pieCount; pieIndex++) {
    393                rect.setBounds(x + xoffset + (width * column), y + (height * row),
    394                        width, height);
    395
    396                String title = null;
    397                if (this.dataExtractOrder == TableOrder.BY_ROW) {
    398                    title = this.dataset.getRowKey(pieIndex).toString();
    399                }
    400                else {
    401                    title = this.dataset.getColumnKey(pieIndex).toString();
    402                }
    403                this.pieChart.setTitle(title);
    404
    405                PieDataset piedataset = null;
    406                PieDataset dd = new CategoryToPieDataset(this.dataset,
    407                        this.dataExtractOrder, pieIndex);
    408                if (this.limit > 0.0) {
    409                    piedataset = DatasetUtilities.createConsolidatedPieDataset(
    410                            dd, this.aggregatedItemsKey, this.limit);
    411                }
    412                else {
    413                    piedataset = dd;
    414                }
    415                PiePlot piePlot = (PiePlot) this.pieChart.getPlot();
    416                piePlot.setDataset(piedataset);
    417                piePlot.setPieIndex(pieIndex);
    418
    419                // update the section colors to match the global colors...
    420                for (int i = 0; i < piedataset.getItemCount(); i++) {
    421                    Comparable key = piedataset.getKey(i);
    422                    Paint p;
    423                    if (key.equals(this.aggregatedItemsKey)) {
    424                        p = this.aggregatedItemsPaint;
    425                    }
    426                    else {
    427                        p = (Paint) this.sectionPaints.get(key);
    428                    }
    429                    piePlot.setSectionPaint(key, p);
    430                }
    431
    432                ChartRenderingInfo subinfo = null;
    433                if (info != null) {
    434                    subinfo = new ChartRenderingInfo();
    435                }
    436                this.pieChart.draw(g2, rect, subinfo);
    437                if (info != null) {
    438                    info.getOwner().getEntityCollection().addAll(
    439                            subinfo.getEntityCollection());
    440                    info.addSubplotInfo(subinfo.getPlotInfo());
    441                }
    442
    443                ++column;
    444                if (column == displayCols) {
    445                    column = 0;
    446                    ++row;
    447
    448                    if (row == displayRows - 1 && diff != 0) {
    449                        xoffset = (diff * width) / 2;
    450                    }
    451                }
    452            }
    453
    454        }
    455
    456        /**
    457         * For each key in the dataset, check the <code>sectionPaints</code>
    458         * cache to see if a paint is associated with that key and, if not,
    459         * fetch one from the drawing supplier.  These colors are cached so that
    460         * the legend and all the subplots use consistent colors.
    461         */
    462        private void prefetchSectionPaints() {
    463
    464            // pre-fetch the colors for each key...this is because the subplots
    465            // may not display every key, but we need the coloring to be
    466            // consistent...
    467
    468            PiePlot piePlot = (PiePlot) getPieChart().getPlot();
    469
    470            if (this.dataExtractOrder == TableOrder.BY_ROW) {
    471                // column keys provide potential keys for individual pies
    472                for (int c = 0; c < this.dataset.getColumnCount(); c++) {
    473                    Comparable key = this.dataset.getColumnKey(c);
    474                    Paint p = piePlot.getSectionPaint(key);
    475                    if (p == null) {
    476                        p = (Paint) this.sectionPaints.get(key);
    477                        if (p == null) {
    478                            p = getDrawingSupplier().getNextPaint();
    479                        }
    480                    }
    481                    this.sectionPaints.put(key, p);
    482                }
    483            }
    484            else {
    485                // row keys provide potential keys for individual pies
    486                for (int r = 0; r < this.dataset.getRowCount(); r++) {
    487                    Comparable key = this.dataset.getRowKey(r);
    488                    Paint p = piePlot.getSectionPaint(key);
    489                    if (p == null) {
    490                        p = (Paint) this.sectionPaints.get(key);
    491                        if (p == null) {
    492                            p = getDrawingSupplier().getNextPaint();
    493                        }
    494                    }
    495                    this.sectionPaints.put(key, p);
    496                }
    497            }
    498
    499        }
    500
    501        /**
    502         * Returns a collection of legend items for the pie chart.
    503         *
    504         * @return The legend items.
    505         */
    506        public LegendItemCollection getLegendItems() {
    507
    508            LegendItemCollection result = new LegendItemCollection();
    509
    510            if (this.dataset != null) {
    511                List keys = null;
    512
    513                prefetchSectionPaints();
    514                if (this.dataExtractOrder == TableOrder.BY_ROW) {
    515                    keys = this.dataset.getColumnKeys();
    516                }
    517                else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
    518                    keys = this.dataset.getRowKeys();
    519                }
    520
    521                if (keys != null) {
    522                    int section = 0;
    523                    Iterator iterator = keys.iterator();
    524                    while (iterator.hasNext()) {
    525                        Comparable key = (Comparable) iterator.next();
    526                        String label = key.toString();
    527                        String description = label;
    528                        Paint paint = (Paint) this.sectionPaints.get(key);
    529                        LegendItem item = new LegendItem(label, description,
    530                                null, null, Plot.DEFAULT_LEGEND_ITEM_CIRCLE,
    531                                paint, Plot.DEFAULT_OUTLINE_STROKE, paint);
    532                        item.setDataset(getDataset());
    533                        result.add(item);
    534                        section++;
    535                    }
    536                }
    537                if (this.limit > 0.0) {
    538                    result.add(new LegendItem(this.aggregatedItemsKey.toString(),
    539                            this.aggregatedItemsKey.toString(), null, null,
    540                            Plot.DEFAULT_LEGEND_ITEM_CIRCLE,
    541                            this.aggregatedItemsPaint,
    542                            Plot.DEFAULT_OUTLINE_STROKE,
    543                            this.aggregatedItemsPaint));
    544                }
    545            }
    546            return result;
    547        }
    548
    549        /**
    550         * Tests this plot for equality with an arbitrary object.  Note that the
    551         * plot's dataset is not considered in the equality test.
    552         *
    553         * @param obj  the object (<code>null</code> permitted).
    554         *
    555         * @return <code>true</code> if this plot is equal to <code>obj</code>, and
    556         *     <code>false</code> otherwise.
    557         */
    558        public boolean equals(Object obj) {
    559            if (obj == this) {
    560                return true;
    561            }
    562            if (!(obj instanceof MultiplePiePlot)) {
    563                return false;
    564            }
    565            MultiplePiePlot that = (MultiplePiePlot) obj;
    566            if (this.dataExtractOrder != that.dataExtractOrder) {
    567                return false;
    568            }
    569            if (this.limit != that.limit) {
    570                return false;
    571            }
    572            if (!this.aggregatedItemsKey.equals(that.aggregatedItemsKey)) {
    573                return false;
    574            }
    575            if (!PaintUtilities.equal(this.aggregatedItemsPaint,
    576                    that.aggregatedItemsPaint)) {
    577                return false;
    578            }
    579            if (!ObjectUtilities.equal(this.pieChart, that.pieChart)) {
    580                return false;
    581            }
    582            if (!super.equals(obj)) {
    583                return false;
    584            }
    585            return true;
    586        }
    587
    588        /**
    589         * Provides serialization support.
    590         *
    591         * @param stream  the output stream.
    592         *
    593         * @throws IOException  if there is an I/O error.
    594         */
    595        private void writeObject(ObjectOutputStream stream) throws IOException {
    596            stream.defaultWriteObject();
    597            SerialUtilities.writePaint(this.aggregatedItemsPaint, stream);
    598        }
    599
    600        /**
    601         * Provides serialization support.
    602         *
    603         * @param stream  the input stream.
    604         *
    605         * @throws IOException  if there is an I/O error.
    606         * @throws ClassNotFoundException  if there is a classpath problem.
    607         */
    608        private void readObject(ObjectInputStream stream)
    609            throws IOException, ClassNotFoundException {
    610            stream.defaultReadObject();
    611            this.aggregatedItemsPaint = SerialUtilities.readPaint(stream);
    612            this.sectionPaints = new HashMap();
    613        }
    614
    615
    616    }