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