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