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