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