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