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 * RingPlot.java 029 * ------------- 030 * (C) Copyright 2004-2022, by David Gilbert. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): Christoph Beck (bug 2121818); 034 * 035 */ 036 037package org.jfree.chart.plot; 038 039import org.jfree.chart.api.RectangleInsets; 040import org.jfree.chart.api.Rotation; 041import org.jfree.chart.api.UnitType; 042import org.jfree.chart.entity.EntityCollection; 043import org.jfree.chart.entity.PieSectionEntity; 044import org.jfree.chart.internal.*; 045import org.jfree.chart.labels.PieToolTipGenerator; 046import org.jfree.chart.plot.pie.PiePlot; 047import org.jfree.chart.plot.pie.PiePlotState; 048import org.jfree.chart.text.TextAnchor; 049import org.jfree.chart.text.TextUtils; 050import org.jfree.chart.urls.PieURLGenerator; 051import org.jfree.data.general.PieDataset; 052 053import java.awt.*; 054import java.awt.geom.Arc2D; 055import java.awt.geom.GeneralPath; 056import java.awt.geom.Line2D; 057import java.awt.geom.Rectangle2D; 058import java.io.IOException; 059import java.io.ObjectInputStream; 060import java.io.ObjectOutputStream; 061import java.io.Serializable; 062import java.text.DecimalFormat; 063import java.text.Format; 064import java.util.Objects; 065 066/** 067 * A customised pie plot that leaves a hole in the middle. 068 */ 069public class RingPlot extends PiePlot implements Cloneable, Serializable { 070 071 /** For serialization. */ 072 private static final long serialVersionUID = 1556064784129676620L; 073 074 /** The center text mode. */ 075 private CenterTextMode centerTextMode = CenterTextMode.NONE; 076 077 /** 078 * Text to display in the middle of the chart (used for 079 * CenterTextMode.FIXED). 080 */ 081 private String centerText; 082 083 /** 084 * The formatter used when displaying the first data value from the 085 * dataset (CenterTextMode.VALUE). 086 */ 087 private Format centerTextFormatter = new DecimalFormat("0.00"); 088 089 /** The font used to display the center text. */ 090 private Font centerTextFont; 091 092 /** The color used to display the center text. */ 093 private Color centerTextColor; 094 095 /** 096 * A flag that controls whether or not separators are drawn between the 097 * sections of the chart. 098 */ 099 private boolean separatorsVisible; 100 101 /** The stroke used to draw separators. */ 102 private transient Stroke separatorStroke; 103 104 /** The paint used to draw separators. */ 105 private transient Paint separatorPaint; 106 107 /** 108 * The length of the inner separator extension (as a proportion of the 109 * depth of the sections). 110 */ 111 private double innerSeparatorExtension; 112 113 /** 114 * The length of the outer separator extension (as a proportion of the 115 * depth of the sections). 116 */ 117 private double outerSeparatorExtension; 118 119 /** 120 * The depth of the section as a proportion of the diameter. 121 */ 122 private double sectionDepth; 123 124 /** 125 * Creates a new plot with a {@code null} dataset. 126 */ 127 public RingPlot() { 128 this(null); 129 } 130 131 /** 132 * Creates a new plot for the specified dataset. 133 * 134 * @param dataset the dataset ({@code null} permitted). 135 */ 136 public RingPlot(PieDataset dataset) { 137 super(dataset); 138 this.centerTextMode = CenterTextMode.NONE; 139 this.centerText = null; 140 this.centerTextFormatter = new DecimalFormat("0.00"); 141 this.centerTextFont = DEFAULT_LABEL_FONT; 142 this.centerTextColor = Color.BLACK; 143 this.separatorsVisible = true; 144 this.separatorStroke = new BasicStroke(0.5f); 145 this.separatorPaint = Color.GRAY; 146 this.innerSeparatorExtension = 0.20; // 20% 147 this.outerSeparatorExtension = 0.20; // 20% 148 this.sectionDepth = 0.20; // 20% 149 } 150 151 /** 152 * Returns the mode for displaying text in the center of the plot. The 153 * default value is {@link CenterTextMode#NONE} therefore no text 154 * will be displayed by default. 155 * 156 * @return The mode (never {@code null}). 157 */ 158 public CenterTextMode getCenterTextMode() { 159 return this.centerTextMode; 160 } 161 162 /** 163 * Sets the mode for displaying text in the center of the plot and sends 164 * a change event to all registered listeners. For 165 * {@link CenterTextMode#FIXED}, the display text will come from the 166 * {@code centerText} attribute (see {@link #getCenterText()}). 167 * For {@link CenterTextMode#VALUE}, the center text will be the value from 168 * the first section in the dataset. 169 * 170 * @param mode the mode ({@code null} not permitted). 171 */ 172 public void setCenterTextMode(CenterTextMode mode) { 173 Args.nullNotPermitted(mode, "mode"); 174 this.centerTextMode = mode; 175 fireChangeEvent(); 176 } 177 178 /** 179 * Returns the text to display in the center of the plot when the mode 180 * is {@link CenterTextMode#FIXED}. 181 * 182 * @return The text (possibly {@code null}). 183 */ 184 public String getCenterText() { 185 return this.centerText; 186 } 187 188 /** 189 * Sets the text to display in the center of the plot and sends a 190 * change event to all registered listeners. If the text is set to 191 * {@code null}, no text will be displayed. 192 * 193 * @param text the text ({@code null} permitted). 194 */ 195 public void setCenterText(String text) { 196 this.centerText = text; 197 fireChangeEvent(); 198 } 199 200 /** 201 * Returns the formatter used to format the center text value for the mode 202 * {@link CenterTextMode#VALUE}. The default value is 203 * {@code DecimalFormat("0.00")}. 204 * 205 * @return The formatter (never {@code null}). 206 */ 207 public Format getCenterTextFormatter() { 208 return this.centerTextFormatter; 209 } 210 211 /** 212 * Sets the formatter used to format the center text value and sends a 213 * change event to all registered listeners. 214 * 215 * @param formatter the formatter ({@code null} not permitted). 216 */ 217 public void setCenterTextFormatter(Format formatter) { 218 Args.nullNotPermitted(formatter, "formatter"); 219 this.centerTextFormatter = formatter; 220 } 221 222 /** 223 * Returns the font used to display the center text. The default value 224 * is {@link PiePlot#DEFAULT_LABEL_FONT}. 225 * 226 * @return The font (never {@code null}). 227 */ 228 public Font getCenterTextFont() { 229 return this.centerTextFont; 230 } 231 232 /** 233 * Sets the font used to display the center text and sends a change event 234 * to all registered listeners. 235 * 236 * @param font the font ({@code null} not permitted). 237 */ 238 public void setCenterTextFont(Font font) { 239 Args.nullNotPermitted(font, "font"); 240 this.centerTextFont = font; 241 fireChangeEvent(); 242 } 243 244 /** 245 * Returns the color for the center text. The default value is 246 * {@code Color.BLACK}. 247 * 248 * @return The color (never {@code null}). 249 */ 250 public Color getCenterTextColor() { 251 return this.centerTextColor; 252 } 253 254 /** 255 * Sets the color for the center text and sends a change event to all 256 * registered listeners. 257 * 258 * @param color the color ({@code null} not permitted). 259 */ 260 public void setCenterTextColor(Color color) { 261 Args.nullNotPermitted(color, "color"); 262 this.centerTextColor = color; 263 fireChangeEvent(); 264 } 265 266 /** 267 * Returns a flag that indicates whether or not separators are drawn between 268 * the sections in the chart. 269 * 270 * @return A boolean. 271 * 272 * @see #setSeparatorsVisible(boolean) 273 */ 274 public boolean getSeparatorsVisible() { 275 return this.separatorsVisible; 276 } 277 278 /** 279 * Sets the flag that controls whether or not separators are drawn between 280 * the sections in the chart, and sends a change event to all registered 281 * listeners. 282 * 283 * @param visible the flag. 284 * 285 * @see #getSeparatorsVisible() 286 */ 287 public void setSeparatorsVisible(boolean visible) { 288 this.separatorsVisible = visible; 289 fireChangeEvent(); 290 } 291 292 /** 293 * Returns the separator stroke. 294 * 295 * @return The stroke (never {@code null}). 296 * 297 * @see #setSeparatorStroke(Stroke) 298 */ 299 public Stroke getSeparatorStroke() { 300 return this.separatorStroke; 301 } 302 303 /** 304 * Sets the stroke used to draw the separator between sections and sends 305 * a change event to all registered listeners. 306 * 307 * @param stroke the stroke ({@code null} not permitted). 308 * 309 * @see #getSeparatorStroke() 310 */ 311 public void setSeparatorStroke(Stroke stroke) { 312 Args.nullNotPermitted(stroke, "stroke"); 313 this.separatorStroke = stroke; 314 fireChangeEvent(); 315 } 316 317 /** 318 * Returns the separator paint. 319 * 320 * @return The paint (never {@code null}). 321 * 322 * @see #setSeparatorPaint(Paint) 323 */ 324 public Paint getSeparatorPaint() { 325 return this.separatorPaint; 326 } 327 328 /** 329 * Sets the paint used to draw the separator between sections and sends a 330 * change event to all registered listeners. 331 * 332 * @param paint the paint ({@code null} not permitted). 333 * 334 * @see #getSeparatorPaint() 335 */ 336 public void setSeparatorPaint(Paint paint) { 337 Args.nullNotPermitted(paint, "paint"); 338 this.separatorPaint = paint; 339 fireChangeEvent(); 340 } 341 342 /** 343 * Returns the length of the inner extension of the separator line that 344 * is drawn between sections, expressed as a proportion of the depth of 345 * the section. 346 * 347 * @return The inner separator extension. 348 * 349 * @see #setInnerSeparatorExtension(double) 350 */ 351 public double getInnerSeparatorExtension() { 352 return this.innerSeparatorExtension; 353 } 354 355 /** 356 * Sets the length of the inner extension of the separator line that is 357 * drawn between sections, as a proportion of the depth of the 358 * sections, and sends a change event to all registered listeners. 359 * 360 * @param proportion the proportion. 361 * 362 * @see #getInnerSeparatorExtension() 363 * @see #setOuterSeparatorExtension(double) 364 */ 365 public void setInnerSeparatorExtension(double proportion) { 366 this.innerSeparatorExtension = proportion; 367 fireChangeEvent(); 368 } 369 370 /** 371 * Returns the length of the outer extension of the separator line that 372 * is drawn between sections, expressed as a proportion of the depth of 373 * the section. 374 * 375 * @return The outer separator extension (as a proportion). 376 * 377 * @see #setOuterSeparatorExtension(double) 378 */ 379 public double getOuterSeparatorExtension() { 380 return this.outerSeparatorExtension; 381 } 382 383 /** 384 * Sets the length of the outer extension of the separator line that is 385 * drawn between sections, as a proportion of the depth of the 386 * sections, and sends a change event to all registered listeners. 387 * 388 * @param proportion the proportion. 389 * 390 * @see #getOuterSeparatorExtension() 391 */ 392 public void setOuterSeparatorExtension(double proportion) { 393 this.outerSeparatorExtension = proportion; 394 fireChangeEvent(); 395 } 396 397 /** 398 * Returns the depth of each section, expressed as a proportion of the 399 * plot radius. 400 * 401 * @return The depth of each section. 402 * 1.0 means a straightforward pie chart. 403 * 404 * @see #setSectionDepth(double) 405 */ 406 public double getSectionDepth() { 407 return this.sectionDepth; 408 } 409 410 /** 411 * The section depth is given as proportion of the plot radius. 412 * Specifying 1.0 results in a straightforward pie chart. 413 * 414 * @param sectionDepth the section depth. 415 * 416 * @see #getSectionDepth() 417 */ 418 public void setSectionDepth(double sectionDepth) { 419 this.sectionDepth = sectionDepth; 420 fireChangeEvent(); 421 } 422 423 /** 424 * Initialises the plot state (which will store the total of all dataset 425 * values, among other things). This method is called once at the 426 * beginning of each drawing. 427 * 428 * @param g2 the graphics device. 429 * @param plotArea the plot area ({@code null} not permitted). 430 * @param plot the plot. 431 * @param index the secondary index ({@code null} for primary 432 * renderer). 433 * @param info collects chart rendering information for return to caller. 434 * 435 * @return A state object (maintains state information relevant to one 436 * chart drawing). 437 */ 438 @Override 439 public PiePlotState initialise(Graphics2D g2, Rectangle2D plotArea, 440 PiePlot plot, Integer index, PlotRenderingInfo info) { 441 PiePlotState state = super.initialise(g2, plotArea, plot, index, info); 442 state.setPassesRequired(3); 443 return state; 444 } 445 446 /** 447 * Draws a single data item. 448 * 449 * @param g2 the graphics device ({@code null} not permitted). 450 * @param section the section index. 451 * @param dataArea the data plot area. 452 * @param state state information for one chart. 453 * @param currentPass the current pass index. 454 */ 455 @Override 456 protected void drawItem(Graphics2D g2, int section, Rectangle2D dataArea, 457 PiePlotState state, int currentPass) { 458 459 PieDataset dataset = getDataset(); 460 Number n = dataset.getValue(section); 461 if (n == null) { 462 return; 463 } 464 double value = n.doubleValue(); 465 double angle1 = 0.0; 466 double angle2 = 0.0; 467 468 Rotation direction = getDirection(); 469 if (direction == Rotation.CLOCKWISE) { 470 angle1 = state.getLatestAngle(); 471 angle2 = angle1 - value / state.getTotal() * 360.0; 472 } 473 else if (direction == Rotation.ANTICLOCKWISE) { 474 angle1 = state.getLatestAngle(); 475 angle2 = angle1 + value / state.getTotal() * 360.0; 476 } 477 else { 478 throw new IllegalStateException("Rotation type not recognised."); 479 } 480 481 double angle = (angle2 - angle1); 482 if (Math.abs(angle) > getMinimumArcAngleToDraw()) { 483 Comparable key = getSectionKey(section); 484 double ep = 0.0; 485 double mep = getMaximumExplodePercent(); 486 if (mep > 0.0) { 487 ep = getExplodePercent(key) / mep; 488 } 489 Rectangle2D arcBounds = getArcBounds(state.getPieArea(), 490 state.getExplodedPieArea(), angle1, angle, ep); 491 Arc2D.Double arc = new Arc2D.Double(arcBounds, angle1, angle, 492 Arc2D.OPEN); 493 494 // create the bounds for the inner arc 495 double depth = this.sectionDepth / 2.0; 496 RectangleInsets s = new RectangleInsets(UnitType.RELATIVE, 497 depth, depth, depth, depth); 498 Rectangle2D innerArcBounds = new Rectangle2D.Double(); 499 innerArcBounds.setRect(arcBounds); 500 s.trim(innerArcBounds); 501 // calculate inner arc in reverse direction, for later 502 // GeneralPath construction 503 Arc2D.Double arc2 = new Arc2D.Double(innerArcBounds, angle1 504 + angle, -angle, Arc2D.OPEN); 505 GeneralPath path = new GeneralPath(); 506 path.moveTo((float) arc.getStartPoint().getX(), 507 (float) arc.getStartPoint().getY()); 508 path.append(arc.getPathIterator(null), false); 509 path.append(arc2.getPathIterator(null), true); 510 path.closePath(); 511 512 Line2D separator = new Line2D.Double(arc2.getEndPoint(), 513 arc.getStartPoint()); 514 515 if (currentPass == 0) { 516 Paint shadowPaint = getShadowPaint(); 517 double shadowXOffset = getShadowXOffset(); 518 double shadowYOffset = getShadowYOffset(); 519 if (shadowPaint != null && getShadowGenerator() == null) { 520 Shape shadowArc = ShapeUtils.createTranslatedShape( 521 path, (float) shadowXOffset, (float) shadowYOffset); 522 g2.setPaint(shadowPaint); 523 g2.fill(shadowArc); 524 } 525 } 526 else if (currentPass == 1) { 527 Paint paint = lookupSectionPaint(key); 528 g2.setPaint(paint); 529 g2.fill(path); 530 Paint outlinePaint = lookupSectionOutlinePaint(key); 531 Stroke outlineStroke = lookupSectionOutlineStroke(key); 532 if (getSectionOutlinesVisible() && outlinePaint != null 533 && outlineStroke != null) { 534 g2.setPaint(outlinePaint); 535 g2.setStroke(outlineStroke); 536 g2.draw(path); 537 } 538 539 if (section == 0) { 540 String nstr = null; 541 if (this.centerTextMode.equals(CenterTextMode.VALUE)) { 542 nstr = this.centerTextFormatter.format(n); 543 } else if (this.centerTextMode.equals(CenterTextMode.FIXED)) { 544 nstr = this.centerText; 545 } 546 if (nstr != null) { 547 g2.setFont(this.centerTextFont); 548 g2.setPaint(this.centerTextColor); 549 TextUtils.drawAlignedString(nstr, g2, 550 (float) dataArea.getCenterX(), 551 (float) dataArea.getCenterY(), 552 TextAnchor.CENTER); 553 } 554 } 555 556 // add an entity for the pie section 557 if (state.getInfo() != null) { 558 EntityCollection entities = state.getEntityCollection(); 559 if (entities != null) { 560 String tip = null; 561 PieToolTipGenerator toolTipGenerator 562 = getToolTipGenerator(); 563 if (toolTipGenerator != null) { 564 tip = toolTipGenerator.generateToolTip(dataset, 565 key); 566 } 567 String url = null; 568 PieURLGenerator urlGenerator = getURLGenerator(); 569 if (urlGenerator != null) { 570 url = urlGenerator.generateURL(dataset, key, 571 getPieIndex()); 572 } 573 PieSectionEntity entity = new PieSectionEntity(path, 574 dataset, getPieIndex(), section, key, tip, 575 url); 576 entities.add(entity); 577 } 578 } 579 } 580 else if (currentPass == 2) { 581 if (this.separatorsVisible) { 582 Line2D extendedSeparator = LineUtils.extendLine( 583 separator, this.innerSeparatorExtension, 584 this.outerSeparatorExtension); 585 g2.setStroke(this.separatorStroke); 586 g2.setPaint(this.separatorPaint); 587 g2.draw(extendedSeparator); 588 } 589 } 590 } 591 state.setLatestAngle(angle2); 592 } 593 594 /** 595 * This method overrides the default value for cases where the ring plot 596 * is very thin. This fixes bug 2121818. 597 * 598 * @return The label link depth, as a proportion of the plot's radius. 599 */ 600 @Override 601 protected double getLabelLinkDepth() { 602 return Math.min(super.getLabelLinkDepth(), getSectionDepth() / 2); 603 } 604 605 /** 606 * Tests this plot for equality with an arbitrary object. 607 * 608 * @param obj the object to test against ({@code null} permitted). 609 * 610 * @return A boolean. 611 */ 612 @Override 613 public boolean equals(Object obj) { 614 if (this == obj) { 615 return true; 616 } 617 if (!(obj instanceof RingPlot)) { 618 return false; 619 } 620 RingPlot that = (RingPlot) obj; 621 if (!this.centerTextMode.equals(that.centerTextMode)) { 622 return false; 623 } 624 if (!Objects.equals(this.centerText, that.centerText)) { 625 return false; 626 } 627 if (!this.centerTextFormatter.equals(that.centerTextFormatter)) { 628 return false; 629 } 630 if (!this.centerTextFont.equals(that.centerTextFont)) { 631 return false; 632 } 633 if (!this.centerTextColor.equals(that.centerTextColor)) { 634 return false; 635 } 636 if (this.separatorsVisible != that.separatorsVisible) { 637 return false; 638 } 639 if (!Objects.equals(this.separatorStroke, that.separatorStroke)) { 640 return false; 641 } 642 if (!PaintUtils.equal(this.separatorPaint, that.separatorPaint)) { 643 return false; 644 } 645 if (this.innerSeparatorExtension != that.innerSeparatorExtension) { 646 return false; 647 } 648 if (this.outerSeparatorExtension != that.outerSeparatorExtension) { 649 return false; 650 } 651 if (this.sectionDepth != that.sectionDepth) { 652 return false; 653 } 654 return super.equals(obj); 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 SerialUtils.writeStroke(this.separatorStroke, stream); 667 SerialUtils.writePaint(this.separatorPaint, 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.separatorStroke = SerialUtils.readStroke(stream); 682 this.separatorPaint = SerialUtils.readPaint(stream); 683 } 684 685}