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