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