001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2021, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * --------------------
028 * MultiplePiePlot.java
029 * --------------------
030 * (C) Copyright 2004-2021, by Object Refinery Limited.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limited);
033 * Contributor(s):   Brian Cabana (patch 1943021);
034 *
035 */
036
037package org.jfree.chart.plot;
038
039import java.awt.Color;
040import java.awt.Font;
041import java.awt.Graphics2D;
042import java.awt.Paint;
043import java.awt.Rectangle;
044import java.awt.Shape;
045import java.awt.geom.Ellipse2D;
046import java.awt.geom.Point2D;
047import java.awt.geom.Rectangle2D;
048import java.io.IOException;
049import java.io.ObjectInputStream;
050import java.io.ObjectOutputStream;
051import java.io.Serializable;
052import java.util.HashMap;
053import java.util.Iterator;
054import java.util.List;
055import java.util.Map;
056import java.util.Objects;
057
058import org.jfree.chart.ChartRenderingInfo;
059import org.jfree.chart.JFreeChart;
060import org.jfree.chart.LegendItem;
061import org.jfree.chart.LegendItemCollection;
062import org.jfree.chart.event.PlotChangeEvent;
063import org.jfree.chart.title.TextTitle;
064import org.jfree.chart.ui.RectangleEdge;
065import org.jfree.chart.ui.RectangleInsets;
066import org.jfree.chart.util.PaintUtils;
067import org.jfree.chart.util.Args;
068import org.jfree.chart.util.SerialUtils;
069import org.jfree.chart.util.ShapeUtils;
070import org.jfree.chart.util.TableOrder;
071import org.jfree.data.category.CategoryDataset;
072import org.jfree.data.category.CategoryToPieDataset;
073import org.jfree.data.general.DatasetChangeEvent;
074import org.jfree.data.general.DatasetUtils;
075import org.jfree.data.general.PieDataset;
076
077/**
078 * A plot that displays multiple pie plots using data from a
079 * {@link CategoryDataset}.
080 */
081public class MultiplePiePlot extends Plot implements Cloneable, Serializable {
082
083    /** For serialization. */
084    private static final long serialVersionUID = -355377800470807389L;
085
086    /** The chart object that draws the individual pie charts. */
087    private JFreeChart pieChart;
088
089    /** The dataset. */
090    private CategoryDataset dataset;
091
092    /** The data extract order (by row or by column). */
093    private TableOrder dataExtractOrder;
094
095    /** The pie section limit percentage. */
096    private double limit = 0.0;
097
098    /**
099     * The key for the aggregated items.
100     */
101    private Comparable aggregatedItemsKey;
102
103    /**
104     * The paint for the aggregated items.
105     */
106    private transient Paint aggregatedItemsPaint;
107
108    /**
109     * The colors to use for each section.
110     */
111    private transient Map sectionPaints;
112
113    /**
114     * The legend item shape (never null).
115     */
116    private transient Shape legendItemShape;
117
118    /**
119     * Creates a new plot with no data.
120     */
121    public MultiplePiePlot() {
122        this(null);
123    }
124
125    /**
126     * Creates a new plot.
127     *
128     * @param dataset  the dataset ({@code null} permitted).
129     */
130    public MultiplePiePlot(CategoryDataset dataset) {
131        super();
132        setDataset(dataset);
133        PiePlot piePlot = new PiePlot(null);
134        piePlot.setIgnoreNullValues(true);
135        this.pieChart = new JFreeChart(piePlot);
136        this.pieChart.removeLegend();
137        this.dataExtractOrder = TableOrder.BY_COLUMN;
138        this.pieChart.setBackgroundPaint(null);
139        TextTitle seriesTitle = new TextTitle("Series Title",
140                new Font("SansSerif", Font.BOLD, 12));
141        seriesTitle.setPosition(RectangleEdge.BOTTOM);
142        this.pieChart.setTitle(seriesTitle);
143        this.aggregatedItemsKey = "Other";
144        this.aggregatedItemsPaint = Color.LIGHT_GRAY;
145        this.sectionPaints = new HashMap();
146        this.legendItemShape = new Ellipse2D.Double(-4.0, -4.0, 8.0, 8.0);
147    }
148
149    /**
150     * Returns the dataset used by the plot.
151     *
152     * @return The dataset (possibly {@code null}).
153     */
154    public CategoryDataset getDataset() {
155        return this.dataset;
156    }
157
158    /**
159     * Sets the dataset used by the plot and sends a {@link PlotChangeEvent}
160     * to all registered listeners.
161     *
162     * @param dataset  the dataset ({@code null} permitted).
163     */
164    public void setDataset(CategoryDataset dataset) {
165        // if there is an existing dataset, remove the plot from the list of
166        // change listeners...
167        if (this.dataset != null) {
168            this.dataset.removeChangeListener(this);
169        }
170
171        // set the new dataset, and register the chart as a change listener...
172        this.dataset = dataset;
173        if (dataset != null) {
174            setDatasetGroup(dataset.getGroup());
175            dataset.addChangeListener(this);
176        }
177
178        // send a dataset change event to self to trigger plot change event
179        datasetChanged(new DatasetChangeEvent(this, dataset));
180    }
181
182    /**
183     * Returns the pie chart that is used to draw the individual pie plots.
184     * Note that there are some attributes on this chart instance that will
185     * be ignored at rendering time (for example, legend item settings).
186     *
187     * @return The pie chart (never {@code null}).
188     *
189     * @see #setPieChart(JFreeChart)
190     */
191    public JFreeChart getPieChart() {
192        return this.pieChart;
193    }
194
195    /**
196     * Sets the chart that is used to draw the individual pie plots.  The
197     * chart's plot must be an instance of {@link PiePlot}.
198     *
199     * @param pieChart  the pie chart ({@code null} not permitted).
200     *
201     * @see #getPieChart()
202     */
203    public void setPieChart(JFreeChart pieChart) {
204        Args.nullNotPermitted(pieChart, "pieChart");
205        if (!(pieChart.getPlot() instanceof PiePlot)) {
206            throw new IllegalArgumentException("The 'pieChart' argument must "
207                    + "be a chart based on a PiePlot.");
208        }
209        this.pieChart = pieChart;
210        fireChangeEvent();
211    }
212
213    /**
214     * Returns the data extract order (by row or by column).
215     *
216     * @return The data extract order (never {@code null}).
217     */
218    public TableOrder getDataExtractOrder() {
219        return this.dataExtractOrder;
220    }
221
222    /**
223     * Sets the data extract order (by row or by column) and sends a
224     * {@link PlotChangeEvent} to all registered listeners.
225     *
226     * @param order  the order ({@code null} not permitted).
227     */
228    public void setDataExtractOrder(TableOrder order) {
229        Args.nullNotPermitted(order, "order");
230        this.dataExtractOrder = order;
231        fireChangeEvent();
232    }
233
234    /**
235     * Returns the limit (as a percentage) below which small pie sections are
236     * aggregated.
237     *
238     * @return The limit percentage.
239     */
240    public double getLimit() {
241        return this.limit;
242    }
243
244    /**
245     * Sets the limit below which pie sections are aggregated.
246     * Set this to 0.0 if you don't want any aggregation to occur.
247     *
248     * @param limit  the limit percent.
249     */
250    public void setLimit(double limit) {
251        this.limit = limit;
252        fireChangeEvent();
253    }
254
255    /**
256     * Returns the key for aggregated items in the pie plots, if there are any.
257     * The default value is "Other".
258     *
259     * @return The aggregated items key.
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} not permitted).
270     */
271    public void setAggregatedItemsKey(Comparable key) {
272        Args.nullNotPermitted(key, "key");
273        this.aggregatedItemsKey = key;
274        fireChangeEvent();
275    }
276
277    /**
278     * Returns the paint used to draw the pie section representing the
279     * aggregated items.  The default value is {code Color.LIGHT_GRAY}.
280     *
281     * @return The paint.
282     */
283    public Paint getAggregatedItemsPaint() {
284        return this.aggregatedItemsPaint;
285    }
286
287    /**
288     * Sets the paint used to draw the pie section representing the aggregated
289     * items and sends a {@link PlotChangeEvent} to all registered listeners.
290     *
291     * @param paint  the paint ({@code null} not permitted).
292     */
293    public void setAggregatedItemsPaint(Paint paint) {
294        Args.nullNotPermitted(paint, "paint");
295        this.aggregatedItemsPaint = paint;
296        fireChangeEvent();
297    }
298
299    /**
300     * Returns a short string describing the type of plot.
301     *
302     * @return The plot type.
303     */
304    @Override
305    public String getPlotType() {
306        return "Multiple Pie Plot";
307         // TODO: need to fetch this from localised resources
308    }
309
310    /**
311     * Returns the shape used for legend items.
312     *
313     * @return The shape (never {@code null}).
314     *
315     * @see #setLegendItemShape(Shape)
316     */
317    public Shape getLegendItemShape() {
318        return this.legendItemShape;
319    }
320
321    /**
322     * Sets the shape used for legend items and sends a {@link PlotChangeEvent}
323     * to all registered listeners.
324     *
325     * @param shape  the shape ({@code null} not permitted).
326     *
327     * @see #getLegendItemShape()
328     */
329    public void setLegendItemShape(Shape shape) {
330        Args.nullNotPermitted(shape, "shape");
331        this.legendItemShape = shape;
332        fireChangeEvent();
333    }
334
335    /**
336     * Draws the plot on a Java 2D graphics device (such as the screen or a
337     * printer).
338     *
339     * @param g2  the graphics device.
340     * @param area  the area within which the plot should be drawn.
341     * @param anchor  the anchor point ({@code null} permitted).
342     * @param parentState  the state from the parent plot, if there is one.
343     * @param info  collects info about the drawing.
344     */
345    @Override
346    public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor,
347            PlotState parentState, PlotRenderingInfo info) {
348
349        // adjust the drawing area for the plot insets (if any)...
350        RectangleInsets insets = getInsets();
351        insets.trim(area);
352        drawBackground(g2, area);
353        drawOutline(g2, area);
354
355        // check that there is some data to display...
356        if (DatasetUtils.isEmptyOrNull(this.dataset)) {
357            drawNoDataMessage(g2, area);
358            return;
359        }
360
361        int pieCount;
362        if (this.dataExtractOrder == TableOrder.BY_ROW) {
363            pieCount = this.dataset.getRowCount();
364        }
365        else {
366            pieCount = this.dataset.getColumnCount();
367        }
368
369        // the columns variable is always >= rows
370        int displayCols = (int) Math.ceil(Math.sqrt(pieCount));
371        int displayRows
372            = (int) Math.ceil((double) pieCount / (double) displayCols);
373
374        // swap rows and columns to match plotArea shape
375        if (displayCols > displayRows && area.getWidth() < area.getHeight()) {
376            int temp = displayCols;
377            displayCols = displayRows;
378            displayRows = temp;
379        }
380
381        prefetchSectionPaints();
382
383        int x = (int) area.getX();
384        int y = (int) area.getY();
385        int width = ((int) area.getWidth()) / displayCols;
386        int height = ((int) area.getHeight()) / displayRows;
387        int row = 0;
388        int column = 0;
389        int diff = (displayRows * displayCols) - pieCount;
390        int xoffset = 0;
391        Rectangle rect = new Rectangle();
392
393        for (int pieIndex = 0; pieIndex < pieCount; pieIndex++) {
394            rect.setBounds(x + xoffset + (width * column), y + (height * row),
395                    width, height);
396
397            String title;
398            if (this.dataExtractOrder == TableOrder.BY_ROW) {
399                title = this.dataset.getRowKey(pieIndex).toString();
400            }
401            else {
402                title = this.dataset.getColumnKey(pieIndex).toString();
403            }
404            this.pieChart.setTitle(title);
405
406            PieDataset piedataset;
407            PieDataset dd = new CategoryToPieDataset(this.dataset,
408                    this.dataExtractOrder, pieIndex);
409            if (this.limit > 0.0) {
410                piedataset = DatasetUtils.createConsolidatedPieDataset(
411                        dd, this.aggregatedItemsKey, this.limit);
412            }
413            else {
414                piedataset = dd;
415            }
416            PiePlot piePlot = (PiePlot) this.pieChart.getPlot();
417            piePlot.setDataset(piedataset);
418            piePlot.setPieIndex(pieIndex);
419
420            // update the section colors to match the global colors...
421            for (int i = 0; i < piedataset.getItemCount(); i++) {
422                Comparable key = piedataset.getKey(i);
423                Paint p;
424                if (key.equals(this.aggregatedItemsKey)) {
425                    p = this.aggregatedItemsPaint;
426                }
427                else {
428                    p = (Paint) this.sectionPaints.get(key);
429                }
430                piePlot.setSectionPaint(key, p);
431            }
432
433            ChartRenderingInfo subinfo = null;
434            if (info != null) {
435                subinfo = new ChartRenderingInfo();
436            }
437            this.pieChart.draw(g2, rect, subinfo);
438            if (info != null) {
439                assert subinfo != 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}
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    @Override
509    public LegendItemCollection getLegendItems() {
510
511        LegendItemCollection result = new LegendItemCollection();
512        if (this.dataset == null) {
513            return result;
514        }
515
516        List keys = null;
517        prefetchSectionPaints();
518        if (this.dataExtractOrder == TableOrder.BY_ROW) {
519            keys = this.dataset.getColumnKeys();
520        }
521        else if (this.dataExtractOrder == TableOrder.BY_COLUMN) {
522            keys = this.dataset.getRowKeys();
523        }
524        if (keys == null) {
525            return result;
526        }
527        int section = 0;
528        Iterator iterator = keys.iterator();
529        while (iterator.hasNext()) {
530            Comparable key = (Comparable) iterator.next();
531            String label = key.toString();  // TODO: use a generator here
532            String description = label;
533            Paint paint = (Paint) this.sectionPaints.get(key);
534            LegendItem item = new LegendItem(label, description, null,
535                    null, getLegendItemShape(), paint,
536                    Plot.DEFAULT_OUTLINE_STROKE, paint);
537            item.setSeriesKey(key);
538            item.setSeriesIndex(section);
539            item.setDataset(getDataset());
540            result.add(item);
541            section++;
542        }
543        if (this.limit > 0.0) {
544            LegendItem a = new LegendItem(this.aggregatedItemsKey.toString(),
545                    this.aggregatedItemsKey.toString(), null, null,
546                    getLegendItemShape(), this.aggregatedItemsPaint,
547                    Plot.DEFAULT_OUTLINE_STROKE, this.aggregatedItemsPaint);
548            result.add(a);
549        }
550        return result;
551    }
552
553    /**
554     * Tests this plot for equality with an arbitrary object.  Note that the
555     * plot's dataset is not considered in the equality test.
556     *
557     * @param obj  the object ({@code null} permitted).
558     *
559     * @return {@code true} if this plot is equal to {@code obj}, and
560     *     {@code false} otherwise.
561     */
562    @Override
563    public boolean equals(Object obj) {
564        if (obj == this) {
565            return true;
566        }
567        if (!(obj instanceof MultiplePiePlot)) {
568            return false;
569        }
570        MultiplePiePlot that = (MultiplePiePlot) obj;
571        if (this.dataExtractOrder != that.dataExtractOrder) {
572            return false;
573        }
574        if (this.limit != that.limit) {
575            return false;
576        }
577        if (!this.aggregatedItemsKey.equals(that.aggregatedItemsKey)) {
578            return false;
579        }
580        if (!PaintUtils.equal(this.aggregatedItemsPaint,
581                that.aggregatedItemsPaint)) {
582            return false;
583        }
584        if (!Objects.equals(this.pieChart, that.pieChart)) {
585            return false;
586        }
587        if (!ShapeUtils.equal(this.legendItemShape, that.legendItemShape)) {
588            return false;
589        }
590        if (!super.equals(obj)) {
591            return false;
592        }
593        return true;
594    }
595
596    /**
597     * Returns a clone of the plot.
598     *
599     * @return A clone.
600     *
601     * @throws CloneNotSupportedException if some component of the plot does
602     *         not support cloning.
603     */
604    @Override
605    public Object clone() throws CloneNotSupportedException {
606        MultiplePiePlot clone = (MultiplePiePlot) super.clone();
607        clone.pieChart = (JFreeChart) this.pieChart.clone();
608        clone.sectionPaints = new HashMap(this.sectionPaints);
609        clone.legendItemShape = ShapeUtils.clone(this.legendItemShape);
610        return clone;
611    }
612
613    /**
614     * Provides serialization support.
615     *
616     * @param stream  the output stream.
617     *
618     * @throws IOException  if there is an I/O error.
619     */
620    private void writeObject(ObjectOutputStream stream) throws IOException {
621        stream.defaultWriteObject();
622        SerialUtils.writePaint(this.aggregatedItemsPaint, stream);
623        SerialUtils.writeShape(this.legendItemShape, stream);
624    }
625
626    /**
627     * Provides serialization support.
628     *
629     * @param stream  the input stream.
630     *
631     * @throws IOException  if there is an I/O error.
632     * @throws ClassNotFoundException  if there is a classpath problem.
633     */
634    private void readObject(ObjectInputStream stream)
635        throws IOException, ClassNotFoundException {
636        stream.defaultReadObject();
637        this.aggregatedItemsPaint = SerialUtils.readPaint(stream);
638        this.legendItemShape = SerialUtils.readShape(stream);
639        this.sectionPaints = new HashMap();
640    }
641
642}