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