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