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