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