001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2011, 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-2011, 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 * 050 */ 051 052package org.jfree.chart.plot; 053 054import java.awt.BasicStroke; 055import java.awt.Color; 056import java.awt.Graphics2D; 057import java.awt.Paint; 058import java.awt.Shape; 059import java.awt.Stroke; 060import java.awt.geom.Arc2D; 061import java.awt.geom.GeneralPath; 062import java.awt.geom.Line2D; 063import java.awt.geom.Rectangle2D; 064import java.io.IOException; 065import java.io.ObjectInputStream; 066import java.io.ObjectOutputStream; 067import java.io.Serializable; 068 069import org.jfree.chart.entity.EntityCollection; 070import org.jfree.chart.entity.PieSectionEntity; 071import org.jfree.chart.event.PlotChangeEvent; 072import org.jfree.chart.labels.PieToolTipGenerator; 073import org.jfree.chart.urls.PieURLGenerator; 074import org.jfree.data.general.PieDataset; 075import org.jfree.io.SerialUtilities; 076import org.jfree.ui.RectangleInsets; 077import org.jfree.util.ObjectUtilities; 078import org.jfree.util.PaintUtilities; 079import org.jfree.util.Rotation; 080import org.jfree.util.ShapeUtilities; 081import org.jfree.util.UnitType; 082 083/** 084 * A customised pie plot that leaves a hole in the middle. 085 */ 086public class RingPlot extends PiePlot implements Cloneable, Serializable { 087 088 /** For serialization. */ 089 private static final long serialVersionUID = 1556064784129676620L; 090 091 /** 092 * A flag that controls whether or not separators are drawn between the 093 * sections of the chart. 094 */ 095 private boolean separatorsVisible; 096 097 /** The stroke used to draw separators. */ 098 private transient Stroke separatorStroke; 099 100 /** The paint used to draw separators. */ 101 private transient Paint separatorPaint; 102 103 /** 104 * The length of the inner separator extension (as a percentage of the 105 * depth of the sections). 106 */ 107 private double innerSeparatorExtension; 108 109 /** 110 * The length of the outer separator extension (as a percentage of the 111 * depth of the sections). 112 */ 113 private double outerSeparatorExtension; 114 115 /** 116 * The depth of the section as a percentage of the diameter. 117 */ 118 private double sectionDepth; 119 120 /** 121 * Creates a new plot with a <code>null</code> dataset. 122 */ 123 public RingPlot() { 124 this(null); 125 } 126 127 /** 128 * Creates a new plot for the specified dataset. 129 * 130 * @param dataset the dataset (<code>null</code> permitted). 131 */ 132 public RingPlot(PieDataset dataset) { 133 super(dataset); 134 this.separatorsVisible = true; 135 this.separatorStroke = new BasicStroke(0.5f); 136 this.separatorPaint = Color.gray; 137 this.innerSeparatorExtension = 0.20; // twenty percent 138 this.outerSeparatorExtension = 0.20; // twenty percent 139 this.sectionDepth = 0.20; // 20% 140 } 141 142 /** 143 * Returns a flag that indicates whether or not separators are drawn between 144 * the sections in the chart. 145 * 146 * @return A boolean. 147 * 148 * @see #setSeparatorsVisible(boolean) 149 */ 150 public boolean getSeparatorsVisible() { 151 return this.separatorsVisible; 152 } 153 154 /** 155 * Sets the flag that controls whether or not separators are drawn between 156 * the sections in the chart, and sends a {@link PlotChangeEvent} to all 157 * registered listeners. 158 * 159 * @param visible the flag. 160 * 161 * @see #getSeparatorsVisible() 162 */ 163 public void setSeparatorsVisible(boolean visible) { 164 this.separatorsVisible = visible; 165 fireChangeEvent(); 166 } 167 168 /** 169 * Returns the separator stroke. 170 * 171 * @return The stroke (never <code>null</code>). 172 * 173 * @see #setSeparatorStroke(Stroke) 174 */ 175 public Stroke getSeparatorStroke() { 176 return this.separatorStroke; 177 } 178 179 /** 180 * Sets the stroke used to draw the separator between sections and sends 181 * a {@link PlotChangeEvent} to all registered listeners. 182 * 183 * @param stroke the stroke (<code>null</code> not permitted). 184 * 185 * @see #getSeparatorStroke() 186 */ 187 public void setSeparatorStroke(Stroke stroke) { 188 if (stroke == null) { 189 throw new IllegalArgumentException("Null 'stroke' argument."); 190 } 191 this.separatorStroke = stroke; 192 fireChangeEvent(); 193 } 194 195 /** 196 * Returns the separator paint. 197 * 198 * @return The paint (never <code>null</code>). 199 * 200 * @see #setSeparatorPaint(Paint) 201 */ 202 public Paint getSeparatorPaint() { 203 return this.separatorPaint; 204 } 205 206 /** 207 * Sets the paint used to draw the separator between sections and sends a 208 * {@link PlotChangeEvent} to all registered listeners. 209 * 210 * @param paint the paint (<code>null</code> not permitted). 211 * 212 * @see #getSeparatorPaint() 213 */ 214 public void setSeparatorPaint(Paint paint) { 215 if (paint == null) { 216 throw new IllegalArgumentException("Null 'paint' argument."); 217 } 218 this.separatorPaint = paint; 219 fireChangeEvent(); 220 } 221 222 /** 223 * Returns the length of the inner extension of the separator line that 224 * is drawn between sections, expressed as a percentage of the depth of 225 * the section. 226 * 227 * @return The inner separator extension (as a percentage). 228 * 229 * @see #setInnerSeparatorExtension(double) 230 */ 231 public double getInnerSeparatorExtension() { 232 return this.innerSeparatorExtension; 233 } 234 235 /** 236 * Sets the length of the inner extension of the separator line that is 237 * drawn between sections, as a percentage of the depth of the 238 * sections, and sends a {@link PlotChangeEvent} to all registered 239 * listeners. 240 * 241 * @param percent the percentage. 242 * 243 * @see #getInnerSeparatorExtension() 244 * @see #setOuterSeparatorExtension(double) 245 */ 246 public void setInnerSeparatorExtension(double percent) { 247 this.innerSeparatorExtension = percent; 248 fireChangeEvent(); 249 } 250 251 /** 252 * Returns the length of the outer extension of the separator line that 253 * is drawn between sections, expressed as a percentage of the depth of 254 * the section. 255 * 256 * @return The outer separator extension (as a percentage). 257 * 258 * @see #setOuterSeparatorExtension(double) 259 */ 260 public double getOuterSeparatorExtension() { 261 return this.outerSeparatorExtension; 262 } 263 264 /** 265 * Sets the length of the outer extension of the separator line that is 266 * drawn between sections, as a percentage of the depth of the 267 * sections, and sends a {@link PlotChangeEvent} to all registered 268 * listeners. 269 * 270 * @param percent the percentage. 271 * 272 * @see #getOuterSeparatorExtension() 273 */ 274 public void setOuterSeparatorExtension(double percent) { 275 this.outerSeparatorExtension = percent; 276 fireChangeEvent(); 277 } 278 279 /** 280 * Returns the depth of each section, expressed as a percentage of the 281 * plot radius. 282 * 283 * @return The depth of each section. 284 * 285 * @see #setSectionDepth(double) 286 * @since 1.0.3 287 */ 288 public double getSectionDepth() { 289 return this.sectionDepth; 290 } 291 292 /** 293 * The section depth is given as percentage of the plot radius. 294 * Specifying 1.0 results in a straightforward pie chart. 295 * 296 * @param sectionDepth the section depth. 297 * 298 * @see #getSectionDepth() 299 * @since 1.0.3 300 */ 301 public void setSectionDepth(double sectionDepth) { 302 this.sectionDepth = sectionDepth; 303 fireChangeEvent(); 304 } 305 306 /** 307 * Initialises the plot state (which will store the total of all dataset 308 * values, among other things). This method is called once at the 309 * beginning of each drawing. 310 * 311 * @param g2 the graphics device. 312 * @param plotArea the plot area (<code>null</code> not permitted). 313 * @param plot the plot. 314 * @param index the secondary index (<code>null</code> for primary 315 * renderer). 316 * @param info collects chart rendering information for return to caller. 317 * 318 * @return A state object (maintains state information relevant to one 319 * chart drawing). 320 */ 321 public PiePlotState initialise(Graphics2D g2, Rectangle2D plotArea, 322 PiePlot plot, Integer index, PlotRenderingInfo info) { 323 324 PiePlotState state = super.initialise(g2, plotArea, plot, index, info); 325 state.setPassesRequired(3); 326 return state; 327 328 } 329 330 /** 331 * Draws a single data item. 332 * 333 * @param g2 the graphics device (<code>null</code> not permitted). 334 * @param section the section index. 335 * @param dataArea the data plot area. 336 * @param state state information for one chart. 337 * @param currentPass the current pass index. 338 */ 339 protected void drawItem(Graphics2D g2, 340 int section, 341 Rectangle2D dataArea, 342 PiePlotState state, 343 int currentPass) { 344 345 PieDataset dataset = getDataset(); 346 Number n = dataset.getValue(section); 347 if (n == null) { 348 return; 349 } 350 double value = n.doubleValue(); 351 double angle1 = 0.0; 352 double angle2 = 0.0; 353 354 Rotation direction = getDirection(); 355 if (direction == Rotation.CLOCKWISE) { 356 angle1 = state.getLatestAngle(); 357 angle2 = angle1 - value / state.getTotal() * 360.0; 358 } 359 else if (direction == Rotation.ANTICLOCKWISE) { 360 angle1 = state.getLatestAngle(); 361 angle2 = angle1 + value / state.getTotal() * 360.0; 362 } 363 else { 364 throw new IllegalStateException("Rotation type not recognised."); 365 } 366 367 double angle = (angle2 - angle1); 368 if (Math.abs(angle) > getMinimumArcAngleToDraw()) { 369 Comparable key = getSectionKey(section); 370 double ep = 0.0; 371 double mep = getMaximumExplodePercent(); 372 if (mep > 0.0) { 373 ep = getExplodePercent(key) / mep; 374 } 375 Rectangle2D arcBounds = getArcBounds(state.getPieArea(), 376 state.getExplodedPieArea(), angle1, angle, ep); 377 Arc2D.Double arc = new Arc2D.Double(arcBounds, angle1, angle, 378 Arc2D.OPEN); 379 380 // create the bounds for the inner arc 381 double depth = this.sectionDepth / 2.0; 382 RectangleInsets s = new RectangleInsets(UnitType.RELATIVE, 383 depth, depth, depth, depth); 384 Rectangle2D innerArcBounds = new Rectangle2D.Double(); 385 innerArcBounds.setRect(arcBounds); 386 s.trim(innerArcBounds); 387 // calculate inner arc in reverse direction, for later 388 // GeneralPath construction 389 Arc2D.Double arc2 = new Arc2D.Double(innerArcBounds, angle1 390 + angle, -angle, Arc2D.OPEN); 391 GeneralPath path = new GeneralPath(); 392 path.moveTo((float) arc.getStartPoint().getX(), 393 (float) arc.getStartPoint().getY()); 394 path.append(arc.getPathIterator(null), false); 395 path.append(arc2.getPathIterator(null), true); 396 path.closePath(); 397 398 Line2D separator = new Line2D.Double(arc2.getEndPoint(), 399 arc.getStartPoint()); 400 401 if (currentPass == 0) { 402 Paint shadowPaint = getShadowPaint(); 403 double shadowXOffset = getShadowXOffset(); 404 double shadowYOffset = getShadowYOffset(); 405 if (shadowPaint != null && getShadowGenerator() == null) { 406 Shape shadowArc = ShapeUtilities.createTranslatedShape( 407 path, (float) shadowXOffset, (float) shadowYOffset); 408 g2.setPaint(shadowPaint); 409 g2.fill(shadowArc); 410 } 411 } 412 else if (currentPass == 1) { 413 Paint paint = lookupSectionPaint(key); 414 g2.setPaint(paint); 415 g2.fill(path); 416 Paint outlinePaint = lookupSectionOutlinePaint(key); 417 Stroke outlineStroke = lookupSectionOutlineStroke(key); 418 if (getSectionOutlinesVisible() && outlinePaint != null 419 && outlineStroke != null) { 420 g2.setPaint(outlinePaint); 421 g2.setStroke(outlineStroke); 422 g2.draw(path); 423 } 424 425 // add an entity for the pie section 426 if (state.getInfo() != null) { 427 EntityCollection entities = state.getEntityCollection(); 428 if (entities != null) { 429 String tip = null; 430 PieToolTipGenerator toolTipGenerator 431 = getToolTipGenerator(); 432 if (toolTipGenerator != null) { 433 tip = toolTipGenerator.generateToolTip(dataset, 434 key); 435 } 436 String url = null; 437 PieURLGenerator urlGenerator = getURLGenerator(); 438 if (urlGenerator != null) { 439 url = urlGenerator.generateURL(dataset, key, 440 getPieIndex()); 441 } 442 PieSectionEntity entity = new PieSectionEntity(path, 443 dataset, getPieIndex(), section, key, tip, 444 url); 445 entities.add(entity); 446 } 447 } 448 } 449 else if (currentPass == 2) { 450 if (this.separatorsVisible) { 451 Line2D extendedSeparator = extendLine(separator, 452 this.innerSeparatorExtension, 453 this.outerSeparatorExtension); 454 g2.setStroke(this.separatorStroke); 455 g2.setPaint(this.separatorPaint); 456 g2.draw(extendedSeparator); 457 } 458 } 459 } 460 state.setLatestAngle(angle2); 461 } 462 463 /** 464 * This method overrides the default value for cases where the ring plot 465 * is very thin. This fixes bug 2121818. 466 * 467 * @return The label link depth, as a percentage of the plot's radius. 468 */ 469 protected double getLabelLinkDepth() { 470 return Math.min(super.getLabelLinkDepth(), getSectionDepth() / 2); 471 } 472 473 /** 474 * Tests this plot for equality with an arbitrary object. 475 * 476 * @param obj the object to test against (<code>null</code> permitted). 477 * 478 * @return A boolean. 479 */ 480 public boolean equals(Object obj) { 481 if (this == obj) { 482 return true; 483 } 484 if (!(obj instanceof RingPlot)) { 485 return false; 486 } 487 RingPlot that = (RingPlot) obj; 488 if (this.separatorsVisible != that.separatorsVisible) { 489 return false; 490 } 491 if (!ObjectUtilities.equal(this.separatorStroke, 492 that.separatorStroke)) { 493 return false; 494 } 495 if (!PaintUtilities.equal(this.separatorPaint, that.separatorPaint)) { 496 return false; 497 } 498 if (this.innerSeparatorExtension != that.innerSeparatorExtension) { 499 return false; 500 } 501 if (this.outerSeparatorExtension != that.outerSeparatorExtension) { 502 return false; 503 } 504 if (this.sectionDepth != that.sectionDepth) { 505 return false; 506 } 507 return super.equals(obj); 508 } 509 510 /** 511 * Creates a new line by extending an existing line. 512 * 513 * @param line the line (<code>null</code> not permitted). 514 * @param startPercent the amount to extend the line at the start point 515 * end. 516 * @param endPercent the amount to extend the line at the end point end. 517 * 518 * @return A new line. 519 */ 520 private Line2D extendLine(Line2D line, double startPercent, 521 double endPercent) { 522 if (line == null) { 523 throw new IllegalArgumentException("Null 'line' argument."); 524 } 525 double x1 = line.getX1(); 526 double x2 = line.getX2(); 527 double deltaX = x2 - x1; 528 double y1 = line.getY1(); 529 double y2 = line.getY2(); 530 double deltaY = y2 - y1; 531 x1 = x1 - (startPercent * deltaX); 532 y1 = y1 - (startPercent * deltaY); 533 x2 = x2 + (endPercent * deltaX); 534 y2 = y2 + (endPercent * deltaY); 535 return new Line2D.Double(x1, y1, x2, y2); 536 } 537 538 /** 539 * Provides serialization support. 540 * 541 * @param stream the output stream. 542 * 543 * @throws IOException if there is an I/O error. 544 */ 545 private void writeObject(ObjectOutputStream stream) throws IOException { 546 stream.defaultWriteObject(); 547 SerialUtilities.writeStroke(this.separatorStroke, stream); 548 SerialUtilities.writePaint(this.separatorPaint, stream); 549 } 550 551 /** 552 * Provides serialization support. 553 * 554 * @param stream the input stream. 555 * 556 * @throws IOException if there is an I/O error. 557 * @throws ClassNotFoundException if there is a classpath problem. 558 */ 559 private void readObject(ObjectInputStream stream) 560 throws IOException, ClassNotFoundException { 561 stream.defaultReadObject(); 562 this.separatorStroke = SerialUtilities.readStroke(stream); 563 this.separatorPaint = SerialUtilities.readPaint(stream); 564 } 565 566}