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