    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
    058package org.jfree.chart.plot;
    059
    060import java.awt.Color;
    061import java.awt.Font;
    062import java.awt.Graphics2D;
    063import java.awt.Paint;
    064import java.awt.Rectangle;
    065import java.awt.Shape;
    066import java.awt.geom.Ellipse2D;
    067import java.awt.geom.Point2D;
    068import java.awt.geom.Rectangle2D;
    069import java.io.IOException;
    070import java.io.ObjectInputStream;
    071import java.io.ObjectOutputStream;
    072import java.io.Serializable;
    073import java.util.HashMap;
    074import java.util.Iterator;
    075import java.util.List;
    076import java.util.Map;
    077
    078import org.jfree.chart.ChartRenderingInfo;
    079import org.jfree.chart.JFreeChart;
    080import org.jfree.chart.LegendItem;
    081import org.jfree.chart.LegendItemCollection;
    082import org.jfree.chart.event.PlotChangeEvent;
    083import org.jfree.chart.title.TextTitle;
    084import org.jfree.data.category.CategoryDataset;
    085import org.jfree.data.category.CategoryToPieDataset;
    086import org.jfree.data.general.DatasetChangeEvent;
    087import org.jfree.data.general.DatasetUtilities;
    088import org.jfree.data.general.PieDataset;
    089import org.jfree.io.SerialUtilities;
    090import org.jfree.ui.RectangleEdge;
    091import org.jfree.ui.RectangleInsets;
    092import org.jfree.util.ObjectUtilities;
    093import org.jfree.util.PaintUtilities;
    094import org.jfree.util.ShapeUtilities;
    095import org.jfree.util.TableOrder;
    096
    097/**
    098 * A plot that displays multiple pie plots using data from a
    099 * {@link CategoryDataset}.
    100 */
    101public 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}