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