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}