/* =========================================================== * JFreeChart : a free chart library for the Java(tm) platform * =========================================================== * * (C) Copyright 2000-2014, by Object Refinery Limited and Contributors. * * Project Info: http://www.jfree.org/jfreechart/index.html * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA. * * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners.] * * -------------------- * MultiplePiePlot.java * -------------------- * (C) Copyright 2004-2014, by Object Refinery Limited. * * Original Author: David Gilbert (for Object Refinery Limited); * Contributor(s): Brian Cabana (patch 1943021); * * Changes * ------- * 29-Jan-2004 : Version 1 (DG); * 31-Mar-2004 : Added setPieIndex() call during drawing (DG); * 20-Apr-2005 : Small change for update to LegendItem constructors (DG); * 05-May-2005 : Updated draw() method parameters (DG); * 16-Jun-2005 : Added get/setDataset() and equals() methods (DG); * ------------- JFREECHART 1.0.x --------------------------------------------- * 06-Apr-2006 : Fixed bug 1190647 - legend and section colors not consistent * when aggregation limit is specified (DG); * 27-Sep-2006 : Updated draw() method for deprecated code (DG); * 17-Jan-2007 : Updated prefetchSectionPaints() to check settings in * underlying PiePlot (DG); * 17-May-2007 : Added argument check to setPieChart() (DG); * 18-May-2007 : Set dataset for LegendItem (DG); * 18-Apr-2008 : In the constructor, register the plot as a dataset listener - * see patch 1943021 from Brian Cabana (DG); * 30-Dec-2008 : Added legendItemShape field, and fixed cloning bug (DG); * 09-Jan-2009 : See ignoreNullValues to true for sub-chart (DG); * 01-Jun-2009 : Set series key in getLegendItems() (DG); * 17-Jun-2012 : Removed JCommon dependencies (DG); * 10-Mar-2014 : Removed LegendItemCollection (DG); * */ package org.jfree.chart.plot; import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; import java.awt.Paint; import java.awt.Rectangle; import java.awt.Shape; import java.awt.geom.Ellipse2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jfree.chart.ChartRenderingInfo; import org.jfree.chart.JFreeChart; import org.jfree.chart.LegendItem; import org.jfree.chart.ui.RectangleEdge; import org.jfree.chart.ui.RectangleInsets; import org.jfree.chart.util.ObjectUtils; import org.jfree.chart.util.PaintUtils; import org.jfree.chart.util.ShapeUtils; import org.jfree.chart.util.TableOrder; import org.jfree.chart.event.PlotChangeEvent; import org.jfree.chart.title.TextTitle; import org.jfree.chart.util.SerialUtils; import org.jfree.data.category.CategoryDataset; import org.jfree.data.category.CategoryToPieDataset; import org.jfree.data.general.DatasetChangeEvent; import org.jfree.data.general.DatasetUtilities; import org.jfree.data.general.PieDataset; /** * A plot that displays multiple pie plots using data from a * {@link CategoryDataset}. */ public class MultiplePiePlot extends Plot implements Cloneable, Serializable { /** For serialization. */ private static final long serialVersionUID = -355377800470807389L; /** The chart object that draws the individual pie charts. */ private JFreeChart pieChart; /** The dataset. */ private CategoryDataset dataset; /** The data extract order (by row or by column). */ private TableOrder dataExtractOrder; /** The pie section limit percentage. */ private double limit = 0.0; /** * The key for the aggregated items. * * @since 1.0.2 */ private Comparable aggregatedItemsKey; /** * The paint for the aggregated items. * * @since 1.0.2 */ private transient Paint aggregatedItemsPaint; /** * The colors to use for each section. * * @since 1.0.2 */ private transient Map<Comparable, Paint> sectionPaints; /** * The legend item shape (never null). * * @since 1.0.12 */ private transient Shape legendItemShape; /** * Creates a new plot with no data. */ public MultiplePiePlot() { this(null); } /** * Creates a new plot. * * @param dataset the dataset (<code>null</code> permitted). */ public MultiplePiePlot(CategoryDataset dataset) { super(); setDataset(dataset); PiePlot piePlot = new PiePlot(null); piePlot.setIgnoreNullValues(true); this.pieChart = new JFreeChart(piePlot); this.pieChart.removeLegend(); this.dataExtractOrder = TableOrder.BY_COLUMN; this.pieChart.setBackgroundPainter(null); TextTitle seriesTitle = new TextTitle("Series Title", new Font("SansSerif", Font.BOLD, 12)); seriesTitle.setPosition(RectangleEdge.BOTTOM); this.pieChart.setTitle(seriesTitle); this.aggregatedItemsKey = "Other"; this.aggregatedItemsPaint = Color.LIGHT_GRAY; this.sectionPaints = new HashMap<Comparable, Paint>(); this.legendItemShape = new Ellipse2D.Double(-4.0, -4.0, 8.0, 8.0); } /** * Returns the dataset used by the plot. * * @return The dataset (possibly <code>null</code>). */ public CategoryDataset getDataset() { return this.dataset; } /** * Sets the dataset used by the plot and sends a {@link PlotChangeEvent} * to all registered listeners. * * @param dataset the dataset (<code>null</code> permitted). */ public void setDataset(CategoryDataset dataset) { // if there is an existing dataset, remove the plot from the list of // change listeners... if (this.dataset != null) { this.dataset.removeChangeListener(this); } // set the new dataset, and register the chart as a change listener... this.dataset = dataset; if (dataset != null) { dataset.addChangeListener(this); } // send a dataset change event to self to trigger plot change event datasetChanged(new DatasetChangeEvent(this, dataset)); } /** * Returns the pie chart that is used to draw the individual pie plots. * Note that there are some attributes on this chart instance that will * be ignored at rendering time (for example, legend item settings). * * @return The pie chart (never <code>null</code>). * * @see #setPieChart(JFreeChart) */ public JFreeChart getPieChart() { return this.pieChart; } /** * Sets the chart that is used to draw the individual pie plots. The * chart's plot must be an instance of {@link PiePlot}. * * @param pieChart the pie chart (<code>null</code> not permitted). * * @see #getPieChart() */ public void setPieChart(JFreeChart pieChart) { if (pieChart == null) { throw new IllegalArgumentException("Null 'pieChart' argument."); } if (!(pieChart.getPlot() instanceof PiePlot)) { throw new IllegalArgumentException("The 'pieChart' argument must " + "be a chart based on a PiePlot."); } this.pieChart = pieChart; fireChangeEvent(); } /** * Returns the data extract order (by row or by column). * * @return The data extract order (never <code>null</code>). */ public TableOrder getDataExtractOrder() { return this.dataExtractOrder; } /** * Sets the data extract order (by row or by column) and sends a * {@link PlotChangeEvent} to all registered listeners. * * @param order the order (<code>null</code> not permitted). */ public void setDataExtractOrder(TableOrder order) { if (order == null) { throw new IllegalArgumentException("Null 'order' argument"); } this.dataExtractOrder = order; fireChangeEvent(); } /** * Returns the limit (as a percentage) below which small pie sections are * aggregated. * * @return The limit percentage. */ public double getLimit() { return this.limit; } /** * Sets the limit below which pie sections are aggregated. * Set this to 0.0 if you don't want any aggregation to occur. * * @param limit the limit percent. */ public void setLimit(double limit) { this.limit = limit; fireChangeEvent(); } /** * Returns the key for aggregated items in the pie plots, if there are any. * The default value is "Other". * * @return The aggregated items key. * * @since 1.0.2 */ public Comparable getAggregatedItemsKey() { return this.aggregatedItemsKey; } /** * Sets the key for aggregated items in the pie plots. You must ensure * that this doesn't clash with any keys in the dataset. * * @param key the key (<code>null</code> not permitted). * * @since 1.0.2 */ public void setAggregatedItemsKey(Comparable key) { if (key == null) { throw new IllegalArgumentException("Null 'key' argument."); } this.aggregatedItemsKey = key; fireChangeEvent(); } /** * Returns the paint used to draw the pie section representing the * aggregated items. The default value is <code>Color.LIGHT_GRAY</code>. * * @return The paint. * * @since 1.0.2 */ public Paint getAggregatedItemsPaint() { return this.aggregatedItemsPaint; } /** * Sets the paint used to draw the pie section representing the aggregated * items and sends a {@link PlotChangeEvent} to all registered listeners. * * @param paint the paint (<code>null</code> not permitted). * * @since 1.0.2 */ public void setAggregatedItemsPaint(Paint paint) { if (paint == null) { throw new IllegalArgumentException("Null 'paint' argument."); } this.aggregatedItemsPaint = paint; fireChangeEvent(); } /** * Returns a short string describing the type of plot. * * @return The plot type. */ @Override public String getPlotType() { return "Multiple Pie Plot"; // TODO: need to fetch this from localised resources } /** * Returns the shape used for legend items. * * @return The shape (never <code>null</code>). * * @see #setLegendItemShape(Shape) * * @since 1.0.12 */ public Shape getLegendItemShape() { return this.legendItemShape; } /** * Sets the shape used for legend items and sends a {@link PlotChangeEvent} * to all registered listeners. * * @param shape the shape (<code>null</code> not permitted). * * @see #getLegendItemShape() * * @since 1.0.12 */ public void setLegendItemShape(Shape shape) { if (shape == null) { throw new IllegalArgumentException("Null 'shape' argument."); } this.legendItemShape = shape; fireChangeEvent(); } /** * Draws the plot on a Java 2D graphics device (such as the screen or a * printer). * * @param g2 the graphics device. * @param area the area within which the plot should be drawn. * @param anchor the anchor point (<code>null</code> permitted). * @param parentState the state from the parent plot, if there is one. * @param info collects info about the drawing. */ @Override public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, PlotState parentState, PlotRenderingInfo info) { // adjust the drawing area for the plot insets (if any)... RectangleInsets insets = getInsets(); insets.trim(area); drawBackground(g2, area); drawOutline(g2, area); // check that there is some data to display... if (DatasetUtilities.isEmptyOrNull(this.dataset)) { drawNoDataMessage(g2, area); return; } int pieCount; if (this.dataExtractOrder == TableOrder.BY_ROW) { pieCount = this.dataset.getRowCount(); } else { pieCount = this.dataset.getColumnCount(); } // the columns variable is always >= rows int displayCols = (int) Math.ceil(Math.sqrt(pieCount)); int displayRows = (int) Math.ceil((double) pieCount / (double) displayCols); // swap rows and columns to match plotArea shape if (displayCols > displayRows && area.getWidth() < area.getHeight()) { int temp = displayCols; displayCols = displayRows; displayRows = temp; } prefetchSectionPaints(); int x = (int) area.getX(); int y = (int) area.getY(); int width = ((int) area.getWidth()) / displayCols; int height = ((int) area.getHeight()) / displayRows; int row = 0; int column = 0; int diff = (displayRows * displayCols) - pieCount; int xoffset = 0; Rectangle rect = new Rectangle(); for (int pieIndex = 0; pieIndex < pieCount; pieIndex++) { rect.setBounds(x + xoffset + (width * column), y + (height * row), width, height); String title; if (this.dataExtractOrder == TableOrder.BY_ROW) { title = this.dataset.getRowKey(pieIndex).toString(); } else { title = this.dataset.getColumnKey(pieIndex).toString(); } this.pieChart.setTitle(title); PieDataset piedataset; PieDataset dd = new CategoryToPieDataset(this.dataset, this.dataExtractOrder, pieIndex); if (this.limit > 0.0) { piedataset = DatasetUtilities.createConsolidatedPieDataset( dd, this.aggregatedItemsKey, this.limit); } else { piedataset = dd; } PiePlot piePlot = (PiePlot) this.pieChart.getPlot(); piePlot.setDataset(piedataset); piePlot.setPieIndex(pieIndex); // update the section colors to match the global colors... for (int i = 0; i < piedataset.getItemCount(); i++) { Comparable key = piedataset.getKey(i); Paint p; if (key.equals(this.aggregatedItemsKey)) { p = this.aggregatedItemsPaint; } else { p = this.sectionPaints.get(key); } piePlot.setSectionPaint(key, p); } ChartRenderingInfo subinfo = null; if (info != null) { subinfo = new ChartRenderingInfo(); } this.pieChart.draw(g2, rect, subinfo); if (info != null) { info.getOwner().getEntityCollection().addAll( subinfo.getEntityCollection()); info.addSubplotInfo(subinfo.getPlotInfo()); } ++column; if (column == displayCols) { column = 0; ++row; if (row == displayRows - 1 && diff != 0) { xoffset = (diff * width) / 2; } } } } /** * For each key in the dataset, check the <code>sectionPaints</code> * cache to see if a paint is associated with that key and, if not, * fetch one from the drawing supplier. These colors are cached so that * the legend and all the subplots use consistent colors. */ private void prefetchSectionPaints() { // pre-fetch the colors for each key...this is because the subplots // may not display every key, but we need the coloring to be // consistent... PiePlot piePlot = (PiePlot) getPieChart().getPlot(); if (this.dataExtractOrder == TableOrder.BY_ROW) { // column keys provide potential keys for individual pies for (int c = 0; c < this.dataset.getColumnCount(); c++) { Comparable key = this.dataset.getColumnKey(c); Paint p = piePlot.getSectionPaint(key); if (p == null) { p = this.sectionPaints.get(key); if (p == null) { p = getDrawingSupplier().getNextPaint(); } } this.sectionPaints.put(key, p); } } else { // row keys provide potential keys for individual pies for (int r = 0; r < this.dataset.getRowCount(); r++) { Comparable key = this.dataset.getRowKey(r); Paint p = piePlot.getSectionPaint(key); if (p == null) { p = this.sectionPaints.get(key); if (p == null) { p = getDrawingSupplier().getNextPaint(); } } this.sectionPaints.put(key, p); } } } /** * Returns a collection of legend items for the pie chart. * * @return The legend items. */ @Override public List<LegendItem> getLegendItems() { List<LegendItem> result = new ArrayList<LegendItem>(); if (this.dataset == null) { return result; } List<Comparable> keys = null; prefetchSectionPaints(); if (this.dataExtractOrder == TableOrder.BY_ROW) { keys = this.dataset.getColumnKeys(); } else if (this.dataExtractOrder == TableOrder.BY_COLUMN) { keys = this.dataset.getRowKeys(); } if (keys == null) { return result; } int section = 0; for (Comparable key : keys) { String label = key.toString(); // TODO: use a generator here String description = label; Paint paint = this.sectionPaints.get(key); LegendItem item = new LegendItem(label, description, null, null, getLegendItemShape(), paint, Plot.DEFAULT_OUTLINE_STROKE, paint); item.setSeriesKey(key); item.setSeriesIndex(section); item.setDataset(getDataset()); result.add(item); section++; } if (this.limit > 0.0) { LegendItem a = new LegendItem(this.aggregatedItemsKey.toString(), this.aggregatedItemsKey.toString(), null, null, getLegendItemShape(), this.aggregatedItemsPaint, Plot.DEFAULT_OUTLINE_STROKE, this.aggregatedItemsPaint); result.add(a); } return result; } /** * Tests this plot for equality with an arbitrary object. Note that the * plot's dataset is not considered in the equality test. * * @param obj the object (<code>null</code> permitted). * * @return <code>true</code> if this plot is equal to <code>obj</code>, and * <code>false</code> otherwise. */ @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (!(obj instanceof MultiplePiePlot)) { return false; } MultiplePiePlot that = (MultiplePiePlot) obj; if (this.dataExtractOrder != that.dataExtractOrder) { return false; } if (this.limit != that.limit) { return false; } if (!this.aggregatedItemsKey.equals(that.aggregatedItemsKey)) { return false; } if (!PaintUtils.equal(this.aggregatedItemsPaint, that.aggregatedItemsPaint)) { return false; } if (!ObjectUtils.equal(this.pieChart, that.pieChart)) { return false; } if (!ShapeUtils.equal(this.legendItemShape, that.legendItemShape)) { return false; } if (!super.equals(obj)) { return false; } return true; } /** * Returns a clone of the plot. * * @return A clone. * * @throws CloneNotSupportedException if some component of the plot does * not support cloning. */ @Override public Object clone() throws CloneNotSupportedException { MultiplePiePlot clone = (MultiplePiePlot) super.clone(); clone.pieChart = (JFreeChart) this.pieChart.clone(); clone.sectionPaints = new HashMap<Comparable, Paint>(this.sectionPaints); clone.legendItemShape = ShapeUtils.clone(this.legendItemShape); return clone; } /** * Provides serialization support. * * @param stream the output stream. * * @throws IOException if there is an I/O error. */ private void writeObject(ObjectOutputStream stream) throws IOException { stream.defaultWriteObject(); SerialUtils.writePaint(this.aggregatedItemsPaint, stream); SerialUtils.writeShape(this.legendItemShape, stream); } /** * Provides serialization support. * * @param stream the input stream. * * @throws IOException if there is an I/O error. * @throws ClassNotFoundException if there is a classpath problem. */ private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); this.aggregatedItemsPaint = SerialUtils.readPaint(stream); this.legendItemShape = SerialUtils.readShape(stream); this.sectionPaints = new HashMap<Comparable, Paint>(); } }