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     * 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     *
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 (never <code>null</code>).
194         * 
195         * @see #setPieChart(JFreeChart)
196         */
197        public JFreeChart getPieChart() {
198            return this.pieChart;
199        }
200        
201        /**
202         * Sets the chart that is used to draw the individual pie plots.  The
203         * chart's plot must be an instance of {@link PiePlot}.
204         * 
205         * @param pieChart  the pie chart (<code>null</code> not permitted).
206         *
207         * @see #getPieChart()
208         */
209        public void setPieChart(JFreeChart pieChart) {
210            if (pieChart == null) {
211                throw new IllegalArgumentException("Null 'pieChart' argument.");
212            }
213            if (!(pieChart.getPlot() instanceof PiePlot)) {
214                throw new IllegalArgumentException("The 'pieChart' argument must "
215                        + "be a chart based on a PiePlot.");
216            }
217            this.pieChart = pieChart;
218            notifyListeners(new PlotChangeEvent(this));
219        }
220        
221        /**
222         * Returns the data extract order (by row or by column).
223         * 
224         * @return The data extract order (never <code>null</code>).
225         */
226        public TableOrder getDataExtractOrder() {
227            return this.dataExtractOrder;
228        }
229        
230        /**
231         * Sets the data extract order (by row or by column) and sends a 
232         * {@link PlotChangeEvent} to all registered listeners.
233         * 
234         * @param order  the order (<code>null</code> not permitted).
235         */
236        public void setDataExtractOrder(TableOrder order) {
237            if (order == null) {
238                throw new IllegalArgumentException("Null 'order' argument");
239            }
240            this.dataExtractOrder = order;
241            notifyListeners(new PlotChangeEvent(this));
242        }
243        
244        /**
245         * Returns the limit (as a percentage) below which small pie sections are 
246         * aggregated.
247         * 
248         * @return The limit percentage.
249         */
250        public double getLimit() {
251            return this.limit;
252        }
253        
254        /**
255         * Sets the limit below which pie sections are aggregated.  
256         * Set this to 0.0 if you don't want any aggregation to occur.
257         * 
258         * @param limit  the limit percent.
259         */
260        public void setLimit(double limit) {
261            this.limit = limit;
262            notifyListeners(new PlotChangeEvent(this));
263        }
264        
265        /**
266         * Returns the key for aggregated items in the pie plots, if there are any.
267         * The default value is "Other".
268         * 
269         * @return The aggregated items key.
270         * 
271         * @since 1.0.2
272         */
273        public Comparable getAggregatedItemsKey() {
274            return this.aggregatedItemsKey;
275        }
276        
277        /**
278         * Sets the key for aggregated items in the pie plots.  You must ensure 
279         * that this doesn't clash with any keys in the dataset.
280         * 
281         * @param key  the key (<code>null</code> not permitted).
282         * 
283         * @since 1.0.2
284         */
285        public void setAggregatedItemsKey(Comparable key) {
286            if (key == null) {
287                throw new IllegalArgumentException("Null 'key' argument.");
288            }
289            this.aggregatedItemsKey = key;
290            notifyListeners(new PlotChangeEvent(this));
291        }
292        
293        /**
294         * Returns the paint used to draw the pie section representing the 
295         * aggregated items.  The default value is <code>Color.lightGray</code>.
296         * 
297         * @return The paint.
298         * 
299         * @since 1.0.2
300         */
301        public Paint getAggregatedItemsPaint() {
302            return this.aggregatedItemsPaint;
303        }
304        
305        /**
306         * Sets the paint used to draw the pie section representing the aggregated
307         * items and sends a {@link PlotChangeEvent} to all registered listeners.
308         * 
309         * @param paint  the paint (<code>null</code> not permitted).
310         * 
311         * @since 1.0.2
312         */
313        public void setAggregatedItemsPaint(Paint paint) {
314            if (paint == null) {
315                throw new IllegalArgumentException("Null 'paint' argument.");
316            }
317            this.aggregatedItemsPaint = paint;
318            notifyListeners(new PlotChangeEvent(this));
319        }
320        
321        /**
322         * Returns a short string describing the type of plot.
323         *
324         * @return The plot type.
325         */
326        public String getPlotType() {
327            return "Multiple Pie Plot";  
328             // TODO: need to fetch this from localised resources
329        }
330    
331        /**
332         * Draws the plot on a Java 2D graphics device (such as the screen or a 
333         * printer).
334         *
335         * @param g2  the graphics device.
336         * @param area  the area within which the plot should be drawn.
337         * @param anchor  the anchor point (<code>null</code> permitted).
338         * @param parentState  the state from the parent plot, if there is one.
339         * @param info  collects info about the drawing.
340         */
341        public void draw(Graphics2D g2, 
342                         Rectangle2D area,
343                         Point2D anchor,
344                         PlotState parentState,
345                         PlotRenderingInfo info) {
346            
347           
348            // adjust the drawing area for the plot insets (if any)...
349            RectangleInsets insets = getInsets();
350            insets.trim(area);
351            drawBackground(g2, area);
352            drawOutline(g2, area);
353            
354            // check that there is some data to display...
355            if (DatasetUtilities.isEmptyOrNull(this.dataset)) {
356                drawNoDataMessage(g2, area);
357                return;
358            }
359    
360            int pieCount = 0;
361            if (this.dataExtractOrder == TableOrder.BY_ROW) {
362                pieCount = this.dataset.getRowCount();
363            }
364            else {
365                pieCount = this.dataset.getColumnCount();
366            }
367    
368            // the columns variable is always >= rows
369            int displayCols = (int) Math.ceil(Math.sqrt(pieCount));
370            int displayRows 
371                = (int) Math.ceil((double) pieCount / (double) displayCols);
372    
373            // swap rows and columns to match plotArea shape
374            if (displayCols > displayRows && area.getWidth() < area.getHeight()) {
375                int temp = displayCols;
376                displayCols = displayRows;
377                displayRows = temp;
378            }
379    
380            prefetchSectionPaints();
381            
382            int x = (int) area.getX();
383            int y = (int) area.getY();
384            int width = ((int) area.getWidth()) / displayCols;
385            int height = ((int) area.getHeight()) / displayRows;
386            int row = 0;
387            int column = 0;
388            int diff = (displayRows * displayCols) - pieCount;
389            int xoffset = 0;
390            Rectangle rect = new Rectangle();
391    
392            for (int pieIndex = 0; pieIndex < pieCount; pieIndex++) {
393                rect.setBounds(x + xoffset + (width * column), y + (height * row), 
394                        width, height);
395    
396                String title = null;
397                if (this.dataExtractOrder == TableOrder.BY_ROW) {
398                    title = this.dataset.getRowKey(pieIndex).toString();
399                }
400                else {
401                    title = this.dataset.getColumnKey(pieIndex).toString();
402                }
403                this.pieChart.setTitle(title);
404                
405                PieDataset piedataset = null;
406                PieDataset dd = new CategoryToPieDataset(this.dataset, 
407                        this.dataExtractOrder, pieIndex);
408                if (this.limit > 0.0) {
409                    piedataset = DatasetUtilities.createConsolidatedPieDataset(
410                            dd, this.aggregatedItemsKey, this.limit);
411                }
412                else {
413                    piedataset = dd;
414                }
415                PiePlot piePlot = (PiePlot) this.pieChart.getPlot();
416                piePlot.setDataset(piedataset);
417                piePlot.setPieIndex(pieIndex);
418                
419                // update the section colors to match the global colors...
420                for (int i = 0; i < piedataset.getItemCount(); i++) {
421                    Comparable key = piedataset.getKey(i);
422                    Paint p;
423                    if (key.equals(this.aggregatedItemsKey)) {
424                        p = this.aggregatedItemsPaint;
425                    }
426                    else {
427                        p = (Paint) this.sectionPaints.get(key);
428                    }
429                    piePlot.setSectionPaint(key, p);
430                }
431                
432                ChartRenderingInfo subinfo = null;
433                if (info != null) {
434                    subinfo = new ChartRenderingInfo();
435                }
436                this.pieChart.draw(g2, rect, subinfo);
437                if (info != null) {
438                    info.getOwner().getEntityCollection().addAll(
439                            subinfo.getEntityCollection());
440                    info.addSubplotInfo(subinfo.getPlotInfo());
441                }
442                
443                ++column;
444                if (column == displayCols) {
445                    column = 0;
446                    ++row;
447    
448                    if (row == displayRows - 1 && diff != 0) {
449                        xoffset = (diff * width) / 2;
450                    }
451                }
452            }
453    
454        }
455        
456        /**
457         * For each key in the dataset, check the <code>sectionPaints</code>
458         * cache to see if a paint is associated with that key and, if not, 
459         * fetch one from the drawing supplier.  These colors are cached so that
460         * the legend and all the subplots use consistent colors.
461         */
462        private void prefetchSectionPaints() {
463            
464            // pre-fetch the colors for each key...this is because the subplots
465            // may not display every key, but we need the coloring to be
466            // consistent...
467            
468            PiePlot piePlot = (PiePlot) getPieChart().getPlot();
469            
470            if (this.dataExtractOrder == TableOrder.BY_ROW) {
471                // column keys provide potential keys for individual pies
472                for (int c = 0; c < this.dataset.getColumnCount(); c++) {
473                    Comparable key = this.dataset.getColumnKey(c);
474                    Paint p = piePlot.getSectionPaint(key); 
475                    if (p == null) {
476                        p = (Paint) this.sectionPaints.get(key);
477                        if (p == null) {
478                            p = getDrawingSupplier().getNextPaint();
479                        }
480                    }
481                    this.sectionPaints.put(key, p);
482                }
483            }
484            else {
485                // row keys provide potential keys for individual pies            
486                for (int r = 0; r < this.dataset.getRowCount(); r++) {
487                    Comparable key = this.dataset.getRowKey(r);
488                    Paint p = piePlot.getSectionPaint(key); 
489                    if (p == null) {
490                        p = (Paint) this.sectionPaints.get(key);
491                        if (p == null) {
492                            p = getDrawingSupplier().getNextPaint();
493                        }
494                    }
495                    this.sectionPaints.put(key, p);
496                }
497            }
498            
499        }
500        
501        /**
502         * Returns a collection of legend items for the pie chart.
503         *
504         * @return The legend items.
505         */
506        public LegendItemCollection getLegendItems() {
507    
508            LegendItemCollection result = new LegendItemCollection();
509            
510            if (this.dataset != null) {
511                List keys = null;
512          
513                prefetchSectionPaints();
514                if (this.dataExtractOrder == TableOrder.BY_ROW) {
515                    keys = this.dataset.getColumnKeys();
516                }
517                else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
518                    keys = this.dataset.getRowKeys();
519                }
520    
521                if (keys != null) {
522                    int section = 0;
523                    Iterator iterator = keys.iterator();
524                    while (iterator.hasNext()) {
525                        Comparable key = (Comparable) iterator.next();
526                        String label = key.toString();
527                        String description = label;
528                        Paint paint = (Paint) this.sectionPaints.get(key);
529                        LegendItem item = new LegendItem(label, description, 
530                                null, null, Plot.DEFAULT_LEGEND_ITEM_CIRCLE, 
531                                paint, Plot.DEFAULT_OUTLINE_STROKE, paint);
532                        item.setDataset(getDataset());
533                        result.add(item);
534                        section++;
535                    }
536                }
537                if (this.limit > 0.0) {
538                    result.add(new LegendItem(this.aggregatedItemsKey.toString(), 
539                            this.aggregatedItemsKey.toString(), null, null, 
540                            Plot.DEFAULT_LEGEND_ITEM_CIRCLE, 
541                            this.aggregatedItemsPaint,
542                            Plot.DEFAULT_OUTLINE_STROKE, 
543                            this.aggregatedItemsPaint));
544                }
545            }
546            return result;
547        }
548        
549        /**
550         * Tests this plot for equality with an arbitrary object.  Note that the 
551         * plot's dataset is not considered in the equality test.
552         * 
553         * @param obj  the object (<code>null</code> permitted).
554         * 
555         * @return <code>true</code> if this plot is equal to <code>obj</code>, and
556         *     <code>false</code> otherwise.
557         */
558        public boolean equals(Object obj) {
559            if (obj == this) {
560                return true;   
561            }
562            if (!(obj instanceof MultiplePiePlot)) {
563                return false;   
564            }
565            MultiplePiePlot that = (MultiplePiePlot) obj;
566            if (this.dataExtractOrder != that.dataExtractOrder) {
567                return false;   
568            }
569            if (this.limit != that.limit) {
570                return false;   
571            }
572            if (!this.aggregatedItemsKey.equals(that.aggregatedItemsKey)) {
573                return false;
574            }
575            if (!PaintUtilities.equal(this.aggregatedItemsPaint, 
576                    that.aggregatedItemsPaint)) {
577                return false;
578            }
579            if (!ObjectUtilities.equal(this.pieChart, that.pieChart)) {
580                return false;   
581            }
582            if (!super.equals(obj)) {
583                return false;   
584            }
585            return true;
586        }
587        
588        /**
589         * Provides serialization support.
590         *
591         * @param stream  the output stream.
592         *
593         * @throws IOException  if there is an I/O error.
594         */
595        private void writeObject(ObjectOutputStream stream) throws IOException {
596            stream.defaultWriteObject();
597            SerialUtilities.writePaint(this.aggregatedItemsPaint, stream);
598        }
599    
600        /**
601         * Provides serialization support.
602         *
603         * @param stream  the input stream.
604         *
605         * @throws IOException  if there is an I/O error.
606         * @throws ClassNotFoundException  if there is a classpath problem.
607         */
608        private void readObject(ObjectInputStream stream) 
609            throws IOException, ClassNotFoundException {
610            stream.defaultReadObject();
611            this.aggregatedItemsPaint = SerialUtilities.readPaint(stream);
612            this.sectionPaints = new HashMap();
613        }
614    
615        
616    }