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