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