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