001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2007, 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 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 025 * in the United States and other countries.] 026 * 027 * ------------- 028 * RingPlot.java 029 * ------------- 030 * (C) Copyright 2004-2007, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limtied); 033 * Contributor(s): -; 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 * 047 */ 048 049 package org.jfree.chart.plot; 050 051 import java.awt.BasicStroke; 052 import java.awt.Color; 053 import java.awt.Graphics2D; 054 import java.awt.Paint; 055 import java.awt.Shape; 056 import java.awt.Stroke; 057 import java.awt.geom.Arc2D; 058 import java.awt.geom.GeneralPath; 059 import java.awt.geom.Line2D; 060 import java.awt.geom.Rectangle2D; 061 import java.io.IOException; 062 import java.io.ObjectInputStream; 063 import java.io.ObjectOutputStream; 064 import java.io.Serializable; 065 066 import org.jfree.chart.entity.EntityCollection; 067 import org.jfree.chart.entity.PieSectionEntity; 068 import org.jfree.chart.event.PlotChangeEvent; 069 import org.jfree.chart.labels.PieToolTipGenerator; 070 import org.jfree.chart.urls.PieURLGenerator; 071 import org.jfree.data.general.PieDataset; 072 import org.jfree.io.SerialUtilities; 073 import org.jfree.ui.RectangleInsets; 074 import org.jfree.util.ObjectUtilities; 075 import org.jfree.util.PaintUtilities; 076 import org.jfree.util.Rotation; 077 import org.jfree.util.ShapeUtilities; 078 import org.jfree.util.UnitType; 079 080 /** 081 * A customised pie plot that leaves a hole in the middle. 082 */ 083 public class RingPlot extends PiePlot implements Cloneable, Serializable { 084 085 /** For serialization. */ 086 private static final long serialVersionUID = 1556064784129676620L; 087 088 /** 089 * A flag that controls whether or not separators are drawn between the 090 * sections of the chart. 091 */ 092 private boolean separatorsVisible; 093 094 /** The stroke used to draw separators. */ 095 private transient Stroke separatorStroke; 096 097 /** The paint used to draw separators. */ 098 private transient Paint separatorPaint; 099 100 /** 101 * The length of the inner separator extension (as a percentage of the 102 * depth of the sections). 103 */ 104 private double innerSeparatorExtension; 105 106 /** 107 * The length of the outer separator extension (as a percentage of the 108 * depth of the sections). 109 */ 110 private double outerSeparatorExtension; 111 112 /** 113 * The depth of the section as a percentage of the diameter. 114 */ 115 private double sectionDepth; 116 117 /** 118 * Creates a new plot with a <code>null</code> dataset. 119 */ 120 public RingPlot() { 121 this(null); 122 } 123 124 /** 125 * Creates a new plot for the specified dataset. 126 * 127 * @param dataset the dataset (<code>null</code> permitted). 128 */ 129 public RingPlot(PieDataset dataset) { 130 super(dataset); 131 this.separatorsVisible = true; 132 this.separatorStroke = new BasicStroke(0.5f); 133 this.separatorPaint = Color.gray; 134 this.innerSeparatorExtension = 0.20; // twenty percent 135 this.outerSeparatorExtension = 0.20; // twenty percent 136 this.sectionDepth = 0.20; // 20% 137 } 138 139 /** 140 * Returns a flag that indicates whether or not separators are drawn between 141 * the sections in the chart. 142 * 143 * @return A boolean. 144 * 145 * @see #setSeparatorsVisible(boolean) 146 */ 147 public boolean getSeparatorsVisible() { 148 return this.separatorsVisible; 149 } 150 151 /** 152 * Sets the flag that controls whether or not separators are drawn between 153 * the sections in the chart, and sends a {@link PlotChangeEvent} to all 154 * registered listeners. 155 * 156 * @param visible the flag. 157 * 158 * @see #getSeparatorsVisible() 159 */ 160 public void setSeparatorsVisible(boolean visible) { 161 this.separatorsVisible = visible; 162 fireChangeEvent(); 163 } 164 165 /** 166 * Returns the separator stroke. 167 * 168 * @return The stroke (never <code>null</code>). 169 * 170 * @see #setSeparatorStroke(Stroke) 171 */ 172 public Stroke getSeparatorStroke() { 173 return this.separatorStroke; 174 } 175 176 /** 177 * Sets the stroke used to draw the separator between sections and sends 178 * a {@link PlotChangeEvent} to all registered listeners. 179 * 180 * @param stroke the stroke (<code>null</code> not permitted). 181 * 182 * @see #getSeparatorStroke() 183 */ 184 public void setSeparatorStroke(Stroke stroke) { 185 if (stroke == null) { 186 throw new IllegalArgumentException("Null 'stroke' argument."); 187 } 188 this.separatorStroke = stroke; 189 fireChangeEvent(); 190 } 191 192 /** 193 * Returns the separator paint. 194 * 195 * @return The paint (never <code>null</code>). 196 * 197 * @see #setSeparatorPaint(Paint) 198 */ 199 public Paint getSeparatorPaint() { 200 return this.separatorPaint; 201 } 202 203 /** 204 * Sets the paint used to draw the separator between sections and sends a 205 * {@link PlotChangeEvent} to all registered listeners. 206 * 207 * @param paint the paint (<code>null</code> not permitted). 208 * 209 * @see #getSeparatorPaint() 210 */ 211 public void setSeparatorPaint(Paint paint) { 212 if (paint == null) { 213 throw new IllegalArgumentException("Null 'paint' argument."); 214 } 215 this.separatorPaint = paint; 216 fireChangeEvent(); 217 } 218 219 /** 220 * Returns the length of the inner extension of the separator line that 221 * is drawn between sections, expressed as a percentage of the depth of 222 * the section. 223 * 224 * @return The inner separator extension (as a percentage). 225 * 226 * @see #setInnerSeparatorExtension(double) 227 */ 228 public double getInnerSeparatorExtension() { 229 return this.innerSeparatorExtension; 230 } 231 232 /** 233 * Sets the length of the inner extension of the separator line that is 234 * drawn between sections, as a percentage of the depth of the 235 * sections, and sends a {@link PlotChangeEvent} to all registered 236 * listeners. 237 * 238 * @param percent the percentage. 239 * 240 * @see #getInnerSeparatorExtension() 241 * @see #setOuterSeparatorExtension(double) 242 */ 243 public void setInnerSeparatorExtension(double percent) { 244 this.innerSeparatorExtension = percent; 245 fireChangeEvent(); 246 } 247 248 /** 249 * Returns the length of the outer extension of the separator line that 250 * is drawn between sections, expressed as a percentage of the depth of 251 * the section. 252 * 253 * @return The outer separator extension (as a percentage). 254 * 255 * @see #setOuterSeparatorExtension(double) 256 */ 257 public double getOuterSeparatorExtension() { 258 return this.outerSeparatorExtension; 259 } 260 261 /** 262 * Sets the length of the outer extension of the separator line that is 263 * drawn between sections, as a percentage of the depth of the 264 * sections, and sends a {@link PlotChangeEvent} to all registered 265 * listeners. 266 * 267 * @param percent the percentage. 268 * 269 * @see #getOuterSeparatorExtension() 270 */ 271 public void setOuterSeparatorExtension(double percent) { 272 this.outerSeparatorExtension = percent; 273 fireChangeEvent(); 274 } 275 276 /** 277 * Returns the depth of each section, expressed as a percentage of the 278 * plot radius. 279 * 280 * @return The depth of each section. 281 * 282 * @see #setSectionDepth(double) 283 * @since 1.0.3 284 */ 285 public double getSectionDepth() { 286 return this.sectionDepth; 287 } 288 289 /** 290 * The section depth is given as percentage of the plot radius. 291 * Specifying 1.0 results in a straightforward pie chart. 292 * 293 * @param sectionDepth the section depth. 294 * 295 * @see #getSectionDepth() 296 * @since 1.0.3 297 */ 298 public void setSectionDepth(double sectionDepth) { 299 this.sectionDepth = sectionDepth; 300 fireChangeEvent(); 301 } 302 303 /** 304 * Initialises the plot state (which will store the total of all dataset 305 * values, among other things). This method is called once at the 306 * beginning of each drawing. 307 * 308 * @param g2 the graphics device. 309 * @param plotArea the plot area (<code>null</code> not permitted). 310 * @param plot the plot. 311 * @param index the secondary index (<code>null</code> for primary 312 * renderer). 313 * @param info collects chart rendering information for return to caller. 314 * 315 * @return A state object (maintains state information relevant to one 316 * chart drawing). 317 */ 318 public PiePlotState initialise(Graphics2D g2, Rectangle2D plotArea, 319 PiePlot plot, Integer index, PlotRenderingInfo info) { 320 321 PiePlotState state = super.initialise(g2, plotArea, plot, index, info); 322 state.setPassesRequired(3); 323 return state; 324 325 } 326 327 /** 328 * Draws a single data item. 329 * 330 * @param g2 the graphics device (<code>null</code> not permitted). 331 * @param section the section index. 332 * @param dataArea the data plot area. 333 * @param state state information for one chart. 334 * @param currentPass the current pass index. 335 */ 336 protected void drawItem(Graphics2D g2, 337 int section, 338 Rectangle2D dataArea, 339 PiePlotState state, 340 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) { 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, true); 411 g2.setPaint(paint); 412 g2.fill(path); 413 Paint outlinePaint = lookupSectionOutlinePaint(key); 414 Stroke outlineStroke = lookupSectionOutlineStroke(key); 415 if (outlinePaint != null && outlineStroke != null) { 416 g2.setPaint(outlinePaint); 417 g2.setStroke(outlineStroke); 418 g2.draw(path); 419 } 420 421 // add an entity for the pie section 422 if (state.getInfo() != null) { 423 EntityCollection entities = state.getEntityCollection(); 424 if (entities != null) { 425 String tip = null; 426 PieToolTipGenerator toolTipGenerator 427 = getToolTipGenerator(); 428 if (toolTipGenerator != null) { 429 tip = toolTipGenerator.generateToolTip(dataset, 430 key); 431 } 432 String url = null; 433 PieURLGenerator urlGenerator = getURLGenerator(); 434 if (urlGenerator != null) { 435 url = urlGenerator.generateURL(dataset, key, 436 getPieIndex()); 437 } 438 PieSectionEntity entity = new PieSectionEntity(path, 439 dataset, getPieIndex(), section, key, tip, 440 url); 441 entities.add(entity); 442 } 443 } 444 } 445 else if (currentPass == 2) { 446 if (this.separatorsVisible) { 447 Line2D extendedSeparator = extendLine(separator, 448 this.innerSeparatorExtension, 449 this.outerSeparatorExtension); 450 g2.setStroke(this.separatorStroke); 451 g2.setPaint(this.separatorPaint); 452 g2.draw(extendedSeparator); 453 } 454 } 455 } 456 state.setLatestAngle(angle2); 457 } 458 459 /** 460 * Tests this plot for equality with an arbitrary object. 461 * 462 * @param obj the object to test against (<code>null</code> permitted). 463 * 464 * @return A boolean. 465 */ 466 public boolean equals(Object obj) { 467 if (this == obj) { 468 return true; 469 } 470 if (!(obj instanceof RingPlot)) { 471 return false; 472 } 473 RingPlot that = (RingPlot) obj; 474 if (this.separatorsVisible != that.separatorsVisible) { 475 return false; 476 } 477 if (!ObjectUtilities.equal(this.separatorStroke, 478 that.separatorStroke)) { 479 return false; 480 } 481 if (!PaintUtilities.equal(this.separatorPaint, that.separatorPaint)) { 482 return false; 483 } 484 if (this.innerSeparatorExtension != that.innerSeparatorExtension) { 485 return false; 486 } 487 if (this.outerSeparatorExtension != that.outerSeparatorExtension) { 488 return false; 489 } 490 if (this.sectionDepth != that.sectionDepth) { 491 return false; 492 } 493 return super.equals(obj); 494 } 495 496 /** 497 * Creates a new line by extending an existing line. 498 * 499 * @param line the line (<code>null</code> not permitted). 500 * @param startPercent the amount to extend the line at the start point 501 * end. 502 * @param endPercent the amount to extend the line at the end point end. 503 * 504 * @return A new line. 505 */ 506 private Line2D extendLine(Line2D line, double startPercent, 507 double endPercent) { 508 if (line == null) { 509 throw new IllegalArgumentException("Null 'line' argument."); 510 } 511 double x1 = line.getX1(); 512 double x2 = line.getX2(); 513 double deltaX = x2 - x1; 514 double y1 = line.getY1(); 515 double y2 = line.getY2(); 516 double deltaY = y2 - y1; 517 x1 = x1 - (startPercent * deltaX); 518 y1 = y1 - (startPercent * deltaY); 519 x2 = x2 + (endPercent * deltaX); 520 y2 = y2 + (endPercent * deltaY); 521 return new Line2D.Double(x1, y1, x2, y2); 522 } 523 524 /** 525 * Provides serialization support. 526 * 527 * @param stream the output stream. 528 * 529 * @throws IOException if there is an I/O error. 530 */ 531 private void writeObject(ObjectOutputStream stream) throws IOException { 532 stream.defaultWriteObject(); 533 SerialUtilities.writeStroke(this.separatorStroke, stream); 534 SerialUtilities.writePaint(this.separatorPaint, stream); 535 } 536 537 /** 538 * Provides serialization support. 539 * 540 * @param stream the input stream. 541 * 542 * @throws IOException if there is an I/O error. 543 * @throws ClassNotFoundException if there is a classpath problem. 544 */ 545 private void readObject(ObjectInputStream stream) 546 throws IOException, ClassNotFoundException { 547 stream.defaultReadObject(); 548 this.separatorStroke = SerialUtilities.readStroke(stream); 549 this.separatorPaint = SerialUtilities.readPaint(stream); 550 } 551 552 }