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:  * $Id: RingPlot.java,v 1.4.2.12 2007/02/14 14:10:25 mungady Exp $
  36:  *
  37:  * Changes
  38:  * -------
  39:  * 08-Nov-2004 : Version 1 (DG);
  40:  * 22-Feb-2005 : Renamed DonutPlot --> RingPlot (DG);
  41:  * 06-Jun-2005 : Added default constructor and fixed equals() method to handle
  42:  *               GradientPaint (DG);
  43:  * ------------- JFREECHART 1.0.x ---------------------------------------------
  44:  * 20-Dec-2005 : Fixed problem with entity shape (bug 1386328) (DG);
  45:  * 27-Sep-2006 : Updated drawItem() method for new lookup methods (DG);
  46:  * 12-Oct-2006 : Added configurable section depth (DG);
  47:  * 14-Feb-2007 : Added notification in setSectionDepth() method (DG);
  48:  *
  49:  */
  50: 
  51: package org.jfree.chart.plot;
  52: 
  53: import java.awt.BasicStroke;
  54: import java.awt.Color;
  55: import java.awt.Graphics2D;
  56: import java.awt.Paint;
  57: import java.awt.Shape;
  58: import java.awt.Stroke;
  59: import java.awt.geom.Arc2D;
  60: import java.awt.geom.GeneralPath;
  61: import java.awt.geom.Line2D;
  62: import java.awt.geom.Rectangle2D;
  63: import java.io.IOException;
  64: import java.io.ObjectInputStream;
  65: import java.io.ObjectOutputStream;
  66: import java.io.Serializable;
  67: 
  68: import org.jfree.chart.entity.EntityCollection;
  69: import org.jfree.chart.entity.PieSectionEntity;
  70: import org.jfree.chart.event.PlotChangeEvent;
  71: import org.jfree.chart.labels.PieToolTipGenerator;
  72: import org.jfree.chart.urls.PieURLGenerator;
  73: import org.jfree.data.general.PieDataset;
  74: import org.jfree.io.SerialUtilities;
  75: import org.jfree.ui.RectangleInsets;
  76: import org.jfree.util.ObjectUtilities;
  77: import org.jfree.util.PaintUtilities;
  78: import org.jfree.util.Rotation;
  79: import org.jfree.util.ShapeUtilities;
  80: import org.jfree.util.UnitType;
  81: 
  82: /**
  83:  * A customised pie plot that leaves a hole in the middle.
  84:  */
  85: public class RingPlot extends PiePlot implements Cloneable, Serializable {
  86:     
  87:     /** For serialization. */
  88:     private static final long serialVersionUID = 1556064784129676620L;
  89:     
  90:     /** 
  91:      * A flag that controls whether or not separators are drawn between the
  92:      * sections of the chart.
  93:      */
  94:     private boolean separatorsVisible;
  95:     
  96:     /** The stroke used to draw separators. */
  97:     private transient Stroke separatorStroke;
  98:     
  99:     /** 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: }