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