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