Source for org.jfree.chart.plot.RingPlot

   1: /* ===========================================================
   2:  * JFreeChart : a free chart library for the Java(tm) platform
   3:  * ===========================================================
   4:  *
   5:  * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
   6:  *
   7:  * Project Info:  http://www.jfree.org/jfreechart/index.html
   8:  *
   9:  * This library is free software; you can redistribute it and/or modify it 
  10:  * under the terms of the GNU Lesser General Public License as published by 
  11:  * the Free Software Foundation; either version 2.1 of the License, or 
  12:  * (at your option) any later version.
  13:  *
  14:  * This library is distributed in the hope that it will be useful, but 
  15:  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
  16:  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
  17:  * License for more details.
  18:  *
  19:  * You should have received a copy of the GNU Lesser General Public
  20:  * License along with this library; if not, write to the Free Software
  21:  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
  22:  * USA.  
  23:  *
  24:  * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
  25:  * in the United States and other countries.]
  26:  *
  27:  * -------------
  28:  * RingPlot.java
  29:  * -------------
  30:  * (C) Copyright 2004-2007, by Object Refinery Limited.
  31:  *
  32:  * Original Author:  David Gilbert (for Object Refinery Limtied);
  33:  * Contributor(s):   -;
  34:  *
  35:  * Changes
  36:  * -------
  37:  * 08-Nov-2004 : Version 1 (DG);
  38:  * 22-Feb-2005 : Renamed DonutPlot --> RingPlot (DG);
  39:  * 06-Jun-2005 : Added default constructor and fixed equals() method to handle
  40:  *               GradientPaint (DG);
  41:  * ------------- JFREECHART 1.0.x ---------------------------------------------
  42:  * 20-Dec-2005 : Fixed problem with entity shape (bug 1386328) (DG);
  43:  * 27-Sep-2006 : Updated drawItem() method for new lookup methods (DG);
  44:  * 12-Oct-2006 : Added configurable section depth (DG);
  45:  * 14-Feb-2007 : Added notification in setSectionDepth() method (DG);
  46:  *
  47:  */
  48: 
  49: package org.jfree.chart.plot;
  50: 
  51: import java.awt.BasicStroke;
  52: import java.awt.Color;
  53: import java.awt.Graphics2D;
  54: import java.awt.Paint;
  55: import java.awt.Shape;
  56: import java.awt.Stroke;
  57: import java.awt.geom.Arc2D;
  58: import java.awt.geom.GeneralPath;
  59: import java.awt.geom.Line2D;
  60: import java.awt.geom.Rectangle2D;
  61: import java.io.IOException;
  62: import java.io.ObjectInputStream;
  63: import java.io.ObjectOutputStream;
  64: import java.io.Serializable;
  65: 
  66: import org.jfree.chart.entity.EntityCollection;
  67: import org.jfree.chart.entity.PieSectionEntity;
  68: import org.jfree.chart.event.PlotChangeEvent;
  69: import org.jfree.chart.labels.PieToolTipGenerator;
  70: import org.jfree.chart.urls.PieURLGenerator;
  71: import org.jfree.data.general.PieDataset;
  72: import org.jfree.io.SerialUtilities;
  73: import org.jfree.ui.RectangleInsets;
  74: import org.jfree.util.ObjectUtilities;
  75: import org.jfree.util.PaintUtilities;
  76: import org.jfree.util.Rotation;
  77: import org.jfree.util.ShapeUtilities;
  78: import org.jfree.util.UnitType;
  79: 
  80: /**
  81:  * A customised pie plot that leaves a hole in the middle.
  82:  */
  83: public class RingPlot extends PiePlot implements Cloneable, Serializable {
  84:     
  85:     /** For serialization. */
  86:     private static final long serialVersionUID = 1556064784129676620L;
  87:     
  88:     /** 
  89:      * A flag that controls whether or not separators are drawn between the
  90:      * sections of the chart.
  91:      */
  92:     private boolean separatorsVisible;
  93:     
  94:     /** The stroke used to draw separators. */
  95:     private transient Stroke separatorStroke;
  96:     
  97:     /** The paint used to draw separators. */
  98:     private transient Paint separatorPaint;
  99:     
 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: }