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