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