Source for org.jfree.chart.renderer.category.WaterfallBarRenderer

   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:  * WaterfallBarRenderer.java
  29:  * -------------------------
  30:  * (C) Copyright 2003-2005, by Object Refinery Limited and Contributors.
  31:  *
  32:  * Original Author:  Darshan Shah;
  33:  * Contributor(s):   David Gilbert (for Object Refinery Limited);
  34:  *
  35:  * $Id: WaterfallBarRenderer.java,v 1.9.2.2 2005/10/25 20:54:16 mungady Exp $
  36:  *
  37:  * Changes
  38:  * -------
  39:  * 20-Oct-2003 : Version 1, contributed by Darshan Shah (DG);
  40:  * 06-Nov-2003 : Changed order of parameters in constructor, and added support 
  41:  *               for GradientPaint (DG);
  42:  * 10-Feb-2004 : Updated drawItem() method to make cut-and-paste overriding 
  43:  *               easier.  Also fixed a bug that meant the minimum bar length 
  44:  *               was being ignored (DG);
  45:  * 04-Oct-2004 : Reworked equals() method and renamed PaintUtils 
  46:  *               --> PaintUtilities (DG);
  47:  * 05-Nov-2004 : Modified drawItem() signature (DG);
  48:  * 07-Jan-2005 : Renamed getRangeExtent() --> findRangeBounds (DG);
  49:  * 23-Feb-2005 : Added argument checking (DG);
  50:  * 20-Apr-2005 : Renamed CategoryLabelGenerator 
  51:  *               --> CategoryItemLabelGenerator (DG);
  52:  * 09-Jun-2005 : Use addItemEntity() from superclass (DG);
  53:  */
  54: 
  55: package org.jfree.chart.renderer.category;
  56: 
  57: import java.awt.Color;
  58: import java.awt.GradientPaint;
  59: import java.awt.Graphics2D;
  60: import java.awt.Paint;
  61: import java.awt.Stroke;
  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.axis.CategoryAxis;
  69: import org.jfree.chart.axis.ValueAxis;
  70: import org.jfree.chart.entity.EntityCollection;
  71: import org.jfree.chart.event.RendererChangeEvent;
  72: import org.jfree.chart.labels.CategoryItemLabelGenerator;
  73: import org.jfree.chart.plot.CategoryPlot;
  74: import org.jfree.chart.plot.PlotOrientation;
  75: import org.jfree.chart.renderer.AbstractRenderer;
  76: import org.jfree.data.Range;
  77: import org.jfree.data.category.CategoryDataset;
  78: import org.jfree.data.general.DatasetUtilities;
  79: import org.jfree.io.SerialUtilities;
  80: import org.jfree.ui.GradientPaintTransformType;
  81: import org.jfree.ui.RectangleEdge;
  82: import org.jfree.ui.StandardGradientPaintTransformer;
  83: import org.jfree.util.PaintUtilities;
  84: import org.jfree.util.PublicCloneable;
  85: 
  86: /**
  87:  * A renderer that handles the drawing of waterfall bar charts, for use with 
  88:  * the {@link CategoryPlot} class.  Note that the bar colors are defined 
  89:  * using special methods in this class - the inherited methods (for example,
  90:  * {@link AbstractRenderer#setSeriesPaint(int, Paint)}) are ignored.
  91:  */
  92: public class WaterfallBarRenderer extends BarRenderer 
  93:                                   implements Cloneable, PublicCloneable, 
  94:                                              Serializable {
  95: 
  96:     /** For serialization. */
  97:     private static final long serialVersionUID = -2482910643727230911L;
  98:     
  99:     /** The paint used to draw the first bar. */
 100:     private transient Paint firstBarPaint;
 101: 
 102:     /** The paint used to draw the last bar. */
 103:     private transient Paint lastBarPaint;
 104: 
 105:     /** The paint used to draw bars having positive values. */
 106:     private transient Paint positiveBarPaint;
 107: 
 108:     /** The paint used to draw bars having negative values. */
 109:     private transient Paint negativeBarPaint;
 110: 
 111:     /**
 112:      * Constructs a new renderer with default values for the bar colors.
 113:      */
 114:     public WaterfallBarRenderer() {
 115:         this(
 116:             new GradientPaint(
 117:                 0.0f, 0.0f, new Color(0x22, 0x22, 0xFF), 
 118:                 0.0f, 0.0f, new Color(0x66, 0x66, 0xFF)
 119:             ), 
 120:             new GradientPaint(
 121:                 0.0f, 0.0f, new Color(0x22, 0xFF, 0x22), 
 122:                 0.0f, 0.0f, new Color(0x66, 0xFF, 0x66)
 123:             ), 
 124:             new GradientPaint(
 125:                 0.0f, 0.0f, new Color(0xFF, 0x22, 0x22), 
 126:                 0.0f, 0.0f, new Color(0xFF, 0x66, 0x66)
 127:             ),
 128:             new GradientPaint(
 129:                 0.0f, 0.0f, new Color(0xFF, 0xFF, 0x22), 
 130:                 0.0f, 0.0f, new Color(0xFF, 0xFF, 0x66)
 131:             )
 132:         );
 133:     }
 134: 
 135:     /**
 136:      * Constructs a new waterfall renderer.
 137:      *
 138:      * @param firstBarPaint  the color of the first bar (<code>null</code> not 
 139:      *                       permitted).
 140:      * @param positiveBarPaint  the color for bars with positive values 
 141:      *                          (<code>null</code> not permitted).
 142:      * @param negativeBarPaint  the color for bars with negative values 
 143:      *                          (<code>null</code> not permitted).
 144:      * @param lastBarPaint  the color of the last bar (<code>null</code> not 
 145:      *                      permitted).
 146:      */
 147:     public WaterfallBarRenderer(Paint firstBarPaint, 
 148:                                 Paint positiveBarPaint, 
 149:                                 Paint negativeBarPaint,
 150:                                 Paint lastBarPaint) {
 151:         super();
 152:         if (firstBarPaint == null) {
 153:             throw new IllegalArgumentException("Null 'firstBarPaint' argument");
 154:         }
 155:         if (positiveBarPaint == null) {
 156:             throw new IllegalArgumentException(
 157:                 "Null 'positiveBarPaint' argument"
 158:             );   
 159:         }
 160:         if (negativeBarPaint == null) {
 161:             throw new IllegalArgumentException(
 162:                 "Null 'negativeBarPaint' argument"
 163:             );   
 164:         }
 165:         if (lastBarPaint == null) {
 166:             throw new IllegalArgumentException("Null 'lastBarPaint' argument");
 167:         }
 168:         this.firstBarPaint = firstBarPaint;
 169:         this.lastBarPaint = lastBarPaint;
 170:         this.positiveBarPaint = positiveBarPaint;
 171:         this.negativeBarPaint = negativeBarPaint;
 172:         setGradientPaintTransformer(
 173:             new StandardGradientPaintTransformer(
 174:                 GradientPaintTransformType.CENTER_VERTICAL
 175:             )
 176:         );
 177:         setMinimumBarLength(1.0);
 178:     }
 179: 
 180:     /**
 181:      * Returns the range of values the renderer requires to display all the 
 182:      * items from the specified dataset.
 183:      * 
 184:      * @param dataset  the dataset (<code>null</code> not permitted).
 185:      * 
 186:      * @return The range (or <code>null</code> if the dataset is empty).
 187:      */
 188:     public Range findRangeBounds(CategoryDataset dataset) {
 189:         return DatasetUtilities.findCumulativeRangeBounds(dataset);   
 190:     }
 191: 
 192:     /**
 193:      * Returns the paint used to draw the first bar.
 194:      * 
 195:      * @return The paint (never <code>null</code>).
 196:      */
 197:     public Paint getFirstBarPaint() {
 198:         return this.firstBarPaint;
 199:     }
 200:     
 201:     /**
 202:      * Sets the paint that will be used to draw the first bar and sends a
 203:      * {@link RendererChangeEvent} to all registered listeners.
 204:      *
 205:      * @param paint  the paint (<code>null</code> not permitted).
 206:      */
 207:     public void setFirstBarPaint(Paint paint) {
 208:         if (paint == null) {
 209:             throw new IllegalArgumentException("Null 'paint' argument");   
 210:         }
 211:         this.firstBarPaint = paint;
 212:         notifyListeners(new RendererChangeEvent(this));
 213:     }
 214: 
 215:     /**
 216:      * Returns the paint used to draw the last bar.
 217:      * 
 218:      * @return The paint (never <code>null</code>).
 219:      */
 220:     public Paint getLastBarPaint() {
 221:         return this.lastBarPaint;
 222:     }
 223:     
 224:     /**
 225:      * Sets the paint that will be used to draw the last bar.
 226:      *
 227:      * @param paint  the paint (<code>null</code> not permitted).
 228:      */
 229:     public void setLastBarPaint(Paint paint) {
 230:         if (paint == null) {
 231:             throw new IllegalArgumentException("Null 'paint' argument");   
 232:         }
 233:         this.lastBarPaint = paint;
 234:         notifyListeners(new RendererChangeEvent(this));
 235:     }
 236: 
 237:     /**
 238:      * Returns the paint used to draw bars with positive values.
 239:      * 
 240:      * @return The paint (never <code>null</code>).
 241:      */
 242:     public Paint getPositiveBarPaint() {
 243:         return this.positiveBarPaint;
 244:     }
 245:     
 246:     /**
 247:      * Sets the paint that will be used to draw bars having positive values.
 248:      *
 249:      * @param paint  the paint (<code>null</code> not permitted).
 250:      */
 251:     public void setPositiveBarPaint(Paint paint) {
 252:         if (paint == null) {
 253:             throw new IllegalArgumentException("Null 'paint' argument");   
 254:         }
 255:         this.positiveBarPaint = paint;
 256:         notifyListeners(new RendererChangeEvent(this));
 257:     }
 258: 
 259:     /**
 260:      * Returns the paint used to draw bars with negative values.
 261:      * 
 262:      * @return The paint (never <code>null</code>).
 263:      */
 264:     public Paint getNegativeBarPaint() {
 265:         return this.negativeBarPaint;
 266:     }
 267:     
 268:     /**
 269:      * Sets the paint that will be used to draw bars having negative values.
 270:      *
 271:      * @param paint  the paint (<code>null</code> not permitted).
 272:      */
 273:     public void setNegativeBarPaint(Paint paint) {
 274:         if (paint == null) {
 275:             throw new IllegalArgumentException("Null 'paint' argument");   
 276:         }
 277:         this.negativeBarPaint = paint;
 278:         notifyListeners(new RendererChangeEvent(this));
 279:     }
 280: 
 281:     /**
 282:      * Draws the bar for a single (series, category) data item.
 283:      *
 284:      * @param g2  the graphics device.
 285:      * @param state  the renderer state.
 286:      * @param dataArea  the data area.
 287:      * @param plot  the plot.
 288:      * @param domainAxis  the domain axis.
 289:      * @param rangeAxis  the range axis.
 290:      * @param dataset  the dataset.
 291:      * @param row  the row index (zero-based).
 292:      * @param column  the column index (zero-based).
 293:      * @param pass  the pass index.
 294:      */
 295:     public void drawItem(Graphics2D g2,
 296:                          CategoryItemRendererState state,
 297:                          Rectangle2D dataArea,
 298:                          CategoryPlot plot,
 299:                          CategoryAxis domainAxis,
 300:                          ValueAxis rangeAxis,
 301:                          CategoryDataset dataset,
 302:                          int row,
 303:                          int column,
 304:                          int pass) {
 305: 
 306:         double previous = state.getSeriesRunningTotal();
 307:         if (column == dataset.getColumnCount() - 1) {
 308:             previous = 0.0;
 309:         }
 310:         double current = 0.0;
 311:         Number n = dataset.getValue(row, column);
 312:         if (n != null) {
 313:             current = previous + n.doubleValue();
 314:         }
 315:         state.setSeriesRunningTotal(current);
 316:         
 317:         int seriesCount = getRowCount();
 318:         int categoryCount = getColumnCount();
 319:         PlotOrientation orientation = plot.getOrientation();
 320:         
 321:         double rectX = 0.0;
 322:         double rectY = 0.0;
 323: 
 324:         RectangleEdge domainAxisLocation = plot.getDomainAxisEdge();
 325:         RectangleEdge rangeAxisLocation = plot.getRangeAxisEdge();
 326:         
 327:         // Y0
 328:         double j2dy0 = rangeAxis.valueToJava2D(
 329:             previous, dataArea, rangeAxisLocation
 330:         );
 331: 
 332:         // Y1
 333:         double j2dy1 = rangeAxis.valueToJava2D(
 334:             current, dataArea, rangeAxisLocation
 335:         );
 336: 
 337:         double valDiff = current - previous;
 338:         if (j2dy1 < j2dy0) {
 339:             double temp = j2dy1;
 340:             j2dy1 = j2dy0;
 341:             j2dy0 = temp;
 342:         }
 343: 
 344:         // BAR WIDTH
 345:         double rectWidth = state.getBarWidth();
 346: 
 347:         // BAR HEIGHT
 348:         double rectHeight = Math.max(
 349:             getMinimumBarLength(), Math.abs(j2dy1 - j2dy0)
 350:         );
 351: 
 352:         if (orientation == PlotOrientation.HORIZONTAL) {
 353:             // BAR Y
 354:             rectY = domainAxis.getCategoryStart(
 355:                 column, getColumnCount(), dataArea, domainAxisLocation
 356:             );
 357:             if (seriesCount > 1) {
 358:                 double seriesGap = dataArea.getHeight() * getItemMargin()
 359:                                    / (categoryCount * (seriesCount - 1));
 360:                 rectY = rectY + row * (state.getBarWidth() + seriesGap);
 361:             }
 362:             else {
 363:                 rectY = rectY + row * state.getBarWidth();
 364:             }
 365:              
 366:             rectX = j2dy0;
 367:             rectHeight = state.getBarWidth();
 368:             rectWidth = Math.max(
 369:                 getMinimumBarLength(), Math.abs(j2dy1 - j2dy0)
 370:             );
 371: 
 372:         }
 373:         else if (orientation == PlotOrientation.VERTICAL) {
 374:             // BAR X
 375:             rectX = domainAxis.getCategoryStart(
 376:                 column, getColumnCount(), dataArea, domainAxisLocation
 377:             );
 378: 
 379:             if (seriesCount > 1) {
 380:                 double seriesGap = dataArea.getWidth() * getItemMargin()
 381:                                    / (categoryCount * (seriesCount - 1));
 382:                 rectX = rectX + row * (state.getBarWidth() + seriesGap);
 383:             }
 384:             else {
 385:                 rectX = rectX + row * state.getBarWidth();
 386:             }
 387: 
 388:             rectY = j2dy0;
 389:         }
 390:         Rectangle2D bar = new Rectangle2D.Double(
 391:             rectX, rectY, rectWidth, rectHeight
 392:         );
 393:         Paint seriesPaint = getFirstBarPaint();
 394:         if (column == 0) {
 395:             seriesPaint = getFirstBarPaint();
 396:         }
 397:         else if (column == categoryCount - 1) {
 398:             seriesPaint = getLastBarPaint();    
 399:         } 
 400:         else {
 401:             if (valDiff < 0.0) {
 402:                 seriesPaint = getNegativeBarPaint();
 403:             } 
 404:             else if (valDiff > 0.0) {
 405:                 seriesPaint = getPositiveBarPaint();
 406:             } 
 407:             else {
 408:                 seriesPaint = getLastBarPaint();
 409:             }
 410:         }
 411:         if (getGradientPaintTransformer() != null 
 412:                 && seriesPaint instanceof GradientPaint) {
 413:             GradientPaint gp = (GradientPaint) seriesPaint;
 414:             seriesPaint = getGradientPaintTransformer().transform(gp, bar);
 415:         }
 416:         g2.setPaint(seriesPaint);
 417:         g2.fill(bar);
 418:         
 419:         // draw the outline...
 420:         if (isDrawBarOutline() 
 421:                 && state.getBarWidth() > BAR_OUTLINE_WIDTH_THRESHOLD) {
 422:             Stroke stroke = getItemOutlineStroke(row, column);
 423:             Paint paint = getItemOutlinePaint(row, column);
 424:             if (stroke != null && paint != null) {
 425:                 g2.setStroke(stroke);
 426:                 g2.setPaint(paint);
 427:                 g2.draw(bar);
 428:             }
 429:         }
 430:         
 431:         CategoryItemLabelGenerator generator 
 432:             = getItemLabelGenerator(row, column);
 433:         if (generator != null && isItemLabelVisible(row, column)) {
 434:             drawItemLabel(
 435:                 g2, dataset, row, column, plot, generator, bar, (valDiff < 0.0)
 436:             );
 437:         }        
 438: 
 439:         // add an item entity, if this information is being collected
 440:         EntityCollection entities = state.getEntityCollection();
 441:         if (entities != null) {
 442:             addItemEntity(entities, dataset, row, column, bar);
 443:         }
 444: 
 445:     }
 446:     
 447:     /**
 448:      * Tests an object for equality with this instance.
 449:      * 
 450:      * @param obj  the object (<code>null</code> permitted).
 451:      * 
 452:      * @return A boolean.
 453:      */
 454:     public boolean equals(Object obj) {
 455:         
 456:         if (obj == this) {
 457:             return true;
 458:         }
 459:         if (!super.equals(obj)) {
 460:             return false;
 461:         }
 462:         if (!(obj instanceof WaterfallBarRenderer)) {
 463:             return false;
 464:         }
 465:         WaterfallBarRenderer that = (WaterfallBarRenderer) obj;
 466:         if (!PaintUtilities.equal(this.firstBarPaint, that.firstBarPaint)) {
 467:             return false;
 468:         }
 469:         if (!PaintUtilities.equal(this.lastBarPaint, that.lastBarPaint)) {
 470:             return false;
 471:         }             
 472:         if (!PaintUtilities.equal(this.positiveBarPaint, 
 473:                 that.positiveBarPaint)) {
 474:             return false;
 475:         }             
 476:         if (!PaintUtilities.equal(this.negativeBarPaint, 
 477:                 that.negativeBarPaint)) {
 478:             return false;
 479:         }             
 480:         return true;
 481:         
 482:     }
 483:     
 484:     /**
 485:      * Provides serialization support.
 486:      *
 487:      * @param stream  the output stream.
 488:      *
 489:      * @throws IOException  if there is an I/O error.
 490:      */
 491:     private void writeObject(ObjectOutputStream stream) throws IOException {
 492:         stream.defaultWriteObject();
 493:         SerialUtilities.writePaint(this.firstBarPaint, stream);
 494:         SerialUtilities.writePaint(this.lastBarPaint, stream);
 495:         SerialUtilities.writePaint(this.positiveBarPaint, stream);
 496:         SerialUtilities.writePaint(this.negativeBarPaint, stream);
 497:     }
 498: 
 499:     /**
 500:      * Provides serialization support.
 501:      *
 502:      * @param stream  the input stream.
 503:      *
 504:      * @throws IOException  if there is an I/O error.
 505:      * @throws ClassNotFoundException  if there is a classpath problem.
 506:      */
 507:     private void readObject(ObjectInputStream stream) 
 508:         throws IOException, ClassNotFoundException {
 509:         stream.defaultReadObject();
 510:         this.firstBarPaint = SerialUtilities.readPaint(stream);
 511:         this.lastBarPaint = SerialUtilities.readPaint(stream);
 512:         this.positiveBarPaint = SerialUtilities.readPaint(stream);
 513:         this.negativeBarPaint = SerialUtilities.readPaint(stream);
 514:     }
 515: 
 516: }