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