001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2005, 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, 2005, 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.4 2005/11/28 12:06:35 mungady Exp $
036     *
037     * Changes (from 21-Jun-2001)
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     *
045     */
046    
047    package org.jfree.chart.plot;
048    
049    import java.awt.Font;
050    import java.awt.Graphics2D;
051    import java.awt.Paint;
052    import java.awt.Rectangle;
053    import java.awt.Stroke;
054    import java.awt.geom.Point2D;
055    import java.awt.geom.Rectangle2D;
056    import java.io.Serializable;
057    import java.util.Iterator;
058    import java.util.List;
059    
060    import org.jfree.chart.ChartRenderingInfo;
061    import org.jfree.chart.JFreeChart;
062    import org.jfree.chart.LegendItem;
063    import org.jfree.chart.LegendItemCollection;
064    import org.jfree.chart.event.PlotChangeEvent;
065    import org.jfree.chart.title.TextTitle;
066    import org.jfree.data.category.CategoryDataset;
067    import org.jfree.data.category.CategoryToPieDataset;
068    import org.jfree.data.general.DatasetChangeEvent;
069    import org.jfree.data.general.DatasetUtilities;
070    import org.jfree.data.general.PieDataset;
071    import org.jfree.ui.RectangleEdge;
072    import org.jfree.ui.RectangleInsets;
073    import org.jfree.util.ObjectUtilities;
074    import org.jfree.util.TableOrder;
075    
076    /**
077     * A plot that displays multiple pie plots using data from a 
078     * {@link CategoryDataset}.
079     */
080    public class MultiplePiePlot extends Plot implements Cloneable, Serializable {
081        
082        /** For serialization. */
083        private static final long serialVersionUID = -355377800470807389L;
084        
085        /** The chart object that draws the individual pie charts. */
086        private JFreeChart pieChart;
087        
088        /** The dataset. */
089        private CategoryDataset dataset;
090        
091        /** The data extract order (by row or by column). */
092        private TableOrder dataExtractOrder;
093        
094        /** The pie section limit percentage. */
095        private double limit = 0.0;
096        
097        /**
098         * Creates a new plot with no data.
099         */
100        public MultiplePiePlot() {
101            this(null);
102        }
103        
104        /**
105         * Creates a new plot.
106         * 
107         * @param dataset  the dataset (<code>null</code> permitted).
108         */
109        public MultiplePiePlot(CategoryDataset dataset) {
110            super();
111            this.dataset = dataset;
112            PiePlot piePlot = new PiePlot(null);
113            this.pieChart = new JFreeChart(piePlot);
114            this.pieChart.removeLegend();
115            this.dataExtractOrder = TableOrder.BY_COLUMN;
116            this.pieChart.setBackgroundPaint(null);
117            TextTitle seriesTitle = new TextTitle(
118                "Series Title", new Font("SansSerif", Font.BOLD, 12)
119            );
120            seriesTitle.setPosition(RectangleEdge.BOTTOM);
121            this.pieChart.setTitle(seriesTitle);
122        }
123        
124        /**
125         * Returns the dataset used by the plot.
126         * 
127         * @return The dataset (possibly <code>null</code>).
128         */
129        public CategoryDataset getDataset() {
130            return this.dataset;   
131        }
132        
133        /**
134         * Sets the dataset used by the plot and sends a {@link PlotChangeEvent}
135         * to all registered listeners.
136         * 
137         * @param dataset  the dataset (<code>null</code> permitted).
138         */
139        public void setDataset(CategoryDataset dataset) {
140            // if there is an existing dataset, remove the plot from the list of 
141            // change listeners...
142            if (this.dataset != null) {
143                this.dataset.removeChangeListener(this);
144            }
145    
146            // set the new dataset, and register the chart as a change listener...
147            this.dataset = dataset;
148            if (dataset != null) {
149                setDatasetGroup(dataset.getGroup());
150                dataset.addChangeListener(this);
151            }
152    
153            // send a dataset change event to self to trigger plot change event
154            datasetChanged(new DatasetChangeEvent(this, dataset));
155        }
156        
157        /**
158         * Returns the pie chart that is used to draw the individual pie plots.
159         * 
160         * @return The pie chart.
161         */
162        public JFreeChart getPieChart() {
163            return this.pieChart;
164        }
165        
166        /**
167         * Sets the chart that is used to draw the individual pie plots.
168         * 
169         * @param pieChart  the pie chart.
170         */
171        public void setPieChart(JFreeChart pieChart) {
172            this.pieChart = pieChart;
173            notifyListeners(new PlotChangeEvent(this));
174        }
175        
176        /**
177         * Returns the data extract order (by row or by column).
178         * 
179         * @return The data extract order (never <code>null</code>).
180         */
181        public TableOrder getDataExtractOrder() {
182            return this.dataExtractOrder;
183        }
184        
185        /**
186         * Sets the data extract order (by row or by column) and sends a 
187         * {@link PlotChangeEvent} to all registered listeners.
188         * 
189         * @param order  the order (<code>null</code> not permitted).
190         */
191        public void setDataExtractOrder(TableOrder order) {
192            if (order == null) {
193                throw new IllegalArgumentException("Null 'order' argument");
194            }
195            this.dataExtractOrder = order;
196            notifyListeners(new PlotChangeEvent(this));
197        }
198        
199        /**
200         * Returns the limit (as a percentage) below which small pie sections are 
201         * aggregated.
202         * 
203         * @return The limit percentage.
204         */
205        public double getLimit() {
206            return this.limit;
207        }
208        
209        /**
210         * Sets the limit below which pie sections are aggregated.  
211         * Set this to 0.0 if you don't want any aggregation to occur.
212         * 
213         * @param limit  the limit percent.
214         */
215        public void setLimit(double limit) {
216            this.limit = limit;
217            notifyListeners(new PlotChangeEvent(this));
218        }
219        
220        /**
221         * Returns a short string describing the type of plot.
222         *
223         * @return The plot type.
224         */
225        public String getPlotType() {
226            return "Multiple Pie Plot";  
227             // TODO: need to fetch this from localised resources
228        }
229    
230        /**
231         * Draws the plot on a Java 2D graphics device (such as the screen or a 
232         * printer).
233         *
234         * @param g2  the graphics device.
235         * @param area  the area within which the plot should be drawn.
236         * @param anchor  the anchor point (<code>null</code> permitted).
237         * @param parentState  the state from the parent plot, if there is one.
238         * @param info  collects info about the drawing.
239         */
240        public void draw(Graphics2D g2, 
241                         Rectangle2D area,
242                         Point2D anchor,
243                         PlotState parentState,
244                         PlotRenderingInfo info) {
245            
246           
247            // adjust the drawing area for the plot insets (if any)...
248            RectangleInsets insets = getInsets();
249            insets.trim(area);
250            drawBackground(g2, area);
251            drawOutline(g2, area);
252            
253            // check that there is some data to display...
254            if (DatasetUtilities.isEmptyOrNull(this.dataset)) {
255                drawNoDataMessage(g2, area);
256                return;
257            }
258    
259            int pieCount = 0;
260            if (this.dataExtractOrder == TableOrder.BY_ROW) {
261                pieCount = this.dataset.getRowCount();
262            }
263            else {
264                pieCount = this.dataset.getColumnCount();
265            }
266    
267            // the columns variable is always >= rows
268            int displayCols = (int) Math.ceil(Math.sqrt(pieCount));
269            int displayRows 
270                = (int) Math.ceil((double) pieCount / (double) displayCols);
271    
272            // swap rows and columns to match plotArea shape
273            if (displayCols > displayRows && area.getWidth() < area.getHeight()) {
274                int temp = displayCols;
275                displayCols = displayRows;
276                displayRows = temp;
277            }
278    
279            // int fontHeight 
280            //     = g2.getFontMetrics(this.plotLabelFont).getHeight() * 2;
281            int x = (int) area.getX();
282            int y = (int) area.getY();
283            int width = ((int) area.getWidth()) / displayCols;
284            int height = ((int) area.getHeight()) / displayRows;
285            int row = 0;
286            int column = 0;
287            int diff = (displayRows * displayCols) - pieCount;
288            int xoffset = 0;
289            Rectangle rect = new Rectangle();
290    
291            for (int pieIndex = 0; pieIndex < pieCount; pieIndex++) {
292                rect.setBounds(
293                    x + xoffset + (width * column), y + (height * row), 
294                    width, height
295                );
296    
297                String title = null;
298                if (this.dataExtractOrder == TableOrder.BY_ROW) {
299                    title = this.dataset.getRowKey(pieIndex).toString();
300                }
301                else {
302                    title = this.dataset.getColumnKey(pieIndex).toString();
303                }
304                this.pieChart.setTitle(title);
305                
306                PieDataset piedataset = null;
307                PieDataset dd = new CategoryToPieDataset(
308                    this.dataset, this.dataExtractOrder, pieIndex
309                );
310                if (this.limit > 0.0) {
311                    piedataset = DatasetUtilities.createConsolidatedPieDataset(
312                        dd, "Other", this.limit
313                    );
314                }
315                else {
316                    piedataset = dd;
317                }
318                PiePlot piePlot = (PiePlot) this.pieChart.getPlot();
319                piePlot.setDataset(piedataset);
320                piePlot.setPieIndex(pieIndex);
321                ChartRenderingInfo subinfo = null;
322                if (info != null) {
323                    subinfo = new ChartRenderingInfo();
324                }
325                this.pieChart.draw(g2, rect, subinfo);
326                if (info != null) {
327                    info.getOwner().getEntityCollection().addAll(
328                        subinfo.getEntityCollection()
329                    );
330                    info.addSubplotInfo(subinfo.getPlotInfo());
331                }
332                
333                ++column;
334                if (column == displayCols) {
335                    column = 0;
336                    ++row;
337    
338                    if (row == displayRows - 1 && diff != 0) {
339                        xoffset = (diff * width) / 2;
340                    }
341                }
342            }
343    
344        }
345        
346        /**
347         * Returns a collection of legend items for the pie chart.
348         *
349         * @return The legend items.
350         */
351        public LegendItemCollection getLegendItems() {
352    
353            LegendItemCollection result = new LegendItemCollection();
354    
355            if (this.dataset != null) {
356                List keys = null;
357          
358                if (this.dataExtractOrder == TableOrder.BY_ROW) {
359                    keys = this.dataset.getColumnKeys();
360                }
361                else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
362                    keys = this.dataset.getRowKeys();
363                }
364    
365                if (keys != null) {
366                    int section = 0;
367                    Iterator iterator = keys.iterator();
368                    while (iterator.hasNext()) {
369                        String label = iterator.next().toString();
370                        String description = label;
371                        PiePlot plot = (PiePlot) this.pieChart.getPlot();
372                        Paint paint = plot.getSectionPaint(section);
373                        Paint outlinePaint = plot.getSectionOutlinePaint(section);
374                        Stroke outlineStroke 
375                            = plot.getSectionOutlineStroke(section);
376                        LegendItem item = new LegendItem(label, description, 
377                                null, null, Plot.DEFAULT_LEGEND_ITEM_CIRCLE, 
378                                paint, outlineStroke, outlinePaint);
379    
380                        result.add(item);
381                        section++;
382                    }
383                }
384            }
385            return result;
386        }
387        
388        /**
389         * Tests this plot for equality with an arbitrary object.  Note that the 
390         * plot's dataset is not considered in the equality test.
391         * 
392         * @param obj  the object (<code>null</code> permitted).
393         */
394        public boolean equals(Object obj) {
395            if (obj == this) {
396                return true;   
397            }
398            if (!(obj instanceof MultiplePiePlot)) {
399                return false;   
400            }
401            MultiplePiePlot that = (MultiplePiePlot) obj;
402            if (this.dataExtractOrder != that.dataExtractOrder) {
403                return false;   
404            }
405            if (this.limit != that.limit) {
406                return false;   
407            }
408            if (!ObjectUtilities.equal(this.pieChart, that.pieChart)) {
409                return false;   
410            }
411            if (!super.equals(obj)) {
412                return false;   
413            }
414            return true;
415        }
416        
417    }