001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors. 006 * 007 * Project Info: http://www.jfree.org/jfreechart/index.html 008 * 009 * This library is free software; you can redistribute it and/or modify it 010 * under the terms of the GNU Lesser General Public License as published by 011 * the Free Software Foundation; either version 2.1 of the License, or 012 * (at your option) any later version. 013 * 014 * This library is distributed in the hope that it will be useful, but 015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 017 * License for more details. 018 * 019 * You should have received a copy of the GNU Lesser General Public 020 * License along with this library; if not, write to the Free Software 021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 022 * USA. 023 * 024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 025 * in the United States and other countries.] 026 * 027 * ------------------------- 028 * WaterfallBarRenderer.java 029 * ------------------------- 030 * (C) Copyright 2003-2008, by Object Refinery Limited and Contributors. 031 * 032 * Original Author: Darshan Shah; 033 * Contributor(s): David Gilbert (for Object Refinery Limited); 034 * 035 * Changes 036 * ------- 037 * 20-Oct-2003 : Version 1, contributed by Darshan Shah (DG); 038 * 06-Nov-2003 : Changed order of parameters in constructor, and added support 039 * for GradientPaint (DG); 040 * 10-Feb-2004 : Updated drawItem() method to make cut-and-paste overriding 041 * easier. Also fixed a bug that meant the minimum bar length 042 * was being ignored (DG); 043 * 04-Oct-2004 : Reworked equals() method and renamed PaintUtils 044 * --> PaintUtilities (DG); 045 * 05-Nov-2004 : Modified drawItem() signature (DG); 046 * 07-Jan-2005 : Renamed getRangeExtent() --> findRangeBounds (DG); 047 * 23-Feb-2005 : Added argument checking (DG); 048 * 20-Apr-2005 : Renamed CategoryLabelGenerator 049 * --> CategoryItemLabelGenerator (DG); 050 * 09-Jun-2005 : Use addItemEntity() from superclass (DG); 051 * 27-Mar-2008 : Fixed error in findRangeBounds() method (DG); 052 * 053 */ 054 055 package org.jfree.chart.renderer.category; 056 057 import java.awt.Color; 058 import java.awt.GradientPaint; 059 import java.awt.Graphics2D; 060 import java.awt.Paint; 061 import java.awt.Stroke; 062 import java.awt.geom.Rectangle2D; 063 import java.io.IOException; 064 import java.io.ObjectInputStream; 065 import java.io.ObjectOutputStream; 066 067 import org.jfree.chart.axis.CategoryAxis; 068 import org.jfree.chart.axis.ValueAxis; 069 import org.jfree.chart.entity.EntityCollection; 070 import org.jfree.chart.event.RendererChangeEvent; 071 import org.jfree.chart.labels.CategoryItemLabelGenerator; 072 import org.jfree.chart.plot.CategoryPlot; 073 import org.jfree.chart.plot.PlotOrientation; 074 import org.jfree.chart.renderer.AbstractRenderer; 075 import org.jfree.data.Range; 076 import org.jfree.data.category.CategoryDataset; 077 import org.jfree.io.SerialUtilities; 078 import org.jfree.ui.GradientPaintTransformType; 079 import org.jfree.ui.RectangleEdge; 080 import org.jfree.ui.StandardGradientPaintTransformer; 081 import org.jfree.util.PaintUtilities; 082 083 /** 084 * A renderer that handles the drawing of waterfall bar charts, for use with 085 * the {@link CategoryPlot} class. Some quirks to note: 086 * <ul> 087 * <li>the value in the last category of the dataset should be (redundantly) 088 * specified as the sum of the items in the preceding categories - otherwise 089 * the final bar in the plot will be incorrectly plotted;</li> 090 * <li>the bar colors are defined using special methods in this class - the 091 * inherited methods (for example, 092 * {@link AbstractRenderer#setSeriesPaint(int, Paint)}) are ignored;</li> 093 * </ul> 094 */ 095 public class WaterfallBarRenderer extends BarRenderer { 096 097 /** For serialization. */ 098 private static final long serialVersionUID = -2482910643727230911L; 099 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 }