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-2005, 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, 2005, 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.5 2005/12/20 17:26:12 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.0 ---------------------------------------------
  44:  * 20-Dec-2005 : Fixed problem with entity shape (bug 1386328) (DG);
  45:  * 
  46:  */
  47: 
  48: package org.jfree.chart.plot;
  49: 
  50: import java.awt.BasicStroke;
  51: import java.awt.Color;
  52: import java.awt.Graphics2D;
  53: import java.awt.Paint;
  54: import java.awt.Shape;
  55: import java.awt.Stroke;
  56: import java.awt.geom.Arc2D;
  57: import java.awt.geom.GeneralPath;
  58: import java.awt.geom.Line2D;
  59: import java.awt.geom.Rectangle2D;
  60: import java.io.IOException;
  61: import java.io.ObjectInputStream;
  62: import java.io.ObjectOutputStream;
  63: import java.io.Serializable;
  64: 
  65: import org.jfree.chart.entity.EntityCollection;
  66: import org.jfree.chart.entity.PieSectionEntity;
  67: import org.jfree.chart.event.PlotChangeEvent;
  68: import org.jfree.chart.labels.PieToolTipGenerator;
  69: import org.jfree.chart.urls.PieURLGenerator;
  70: import org.jfree.data.general.PieDataset;
  71: import org.jfree.io.SerialUtilities;
  72: import org.jfree.ui.RectangleInsets;
  73: import org.jfree.util.ObjectUtilities;
  74: import org.jfree.util.PaintUtilities;
  75: import org.jfree.util.Rotation;
  76: import org.jfree.util.ShapeUtilities;
  77: import org.jfree.util.UnitType;
  78: 
  79: /**
  80:  * A customised pie plot that leaves a hole in the middle.
  81:  */
  82: public class RingPlot extends PiePlot implements Cloneable, Serializable {
  83:     
  84:     /** For serialization. */
  85:     private static final long serialVersionUID = 1556064784129676620L;
  86:     
  87:     /** 
  88:      * A flag that controls whether or not separators are drawn between the
  89:      * sections of the chart.
  90:      */
  91:     private boolean separatorsVisible;
  92:     
  93:     /** The stroke used to draw separators. */
  94:     private transient Stroke separatorStroke;
  95:     
  96:     /** The paint used to draw separators. */
  97:     private transient Paint separatorPaint;
  98:     
  99:     /** 
 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: }