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