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