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