001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2011, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * ------------------------- 028 * WaterfallBarRenderer.java 029 * ------------------------- 030 * (C) Copyright 2003-2009, 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 * 26-Sep-2008 : Fixed bug with bar alignment when maximumBarWidth is 053 * applied (DG); 054 * 04-Feb-2009 : Updated findRangeBounds to handle null dataset consistently 055 * with other renderers (DG); 056 * 19-May-2009 : Fixed FindBugs warnings, patch by Michal Wozniak (DG); 057 * 058 */ 059 060package org.jfree.chart.renderer.category; 061 062import java.awt.Color; 063import java.awt.GradientPaint; 064import java.awt.Graphics2D; 065import java.awt.Paint; 066import java.awt.Stroke; 067import java.awt.geom.Rectangle2D; 068import java.io.IOException; 069import java.io.ObjectInputStream; 070import java.io.ObjectOutputStream; 071 072import org.jfree.chart.axis.CategoryAxis; 073import org.jfree.chart.axis.ValueAxis; 074import org.jfree.chart.entity.EntityCollection; 075import org.jfree.chart.event.RendererChangeEvent; 076import org.jfree.chart.labels.CategoryItemLabelGenerator; 077import org.jfree.chart.plot.CategoryPlot; 078import org.jfree.chart.plot.PlotOrientation; 079import org.jfree.chart.renderer.AbstractRenderer; 080import org.jfree.data.Range; 081import org.jfree.data.category.CategoryDataset; 082import org.jfree.io.SerialUtilities; 083import org.jfree.ui.GradientPaintTransformType; 084import org.jfree.ui.RectangleEdge; 085import org.jfree.ui.StandardGradientPaintTransformer; 086import org.jfree.util.PaintUtilities; 087 088/** 089 * A renderer that handles the drawing of waterfall bar charts, for use with 090 * the {@link CategoryPlot} class. Some quirks to note: 091 * <ul> 092 * <li>the value in the last category of the dataset should be (redundantly) 093 * specified as the sum of the items in the preceding categories - otherwise 094 * the final bar in the plot will be incorrectly plotted;</li> 095 * <li>the bar colors are defined using special methods in this class - the 096 * inherited methods (for example, 097 * {@link AbstractRenderer#setSeriesPaint(int, Paint)}) are ignored;</li> 098 * </ul> 099 * The example shown here is generated by the 100 * <code>WaterfallChartDemo1.java</code> program included in the JFreeChart 101 * Demo Collection: 102 * <br><br> 103 * <img src="../../../../../images/WaterfallBarRendererSample.png" 104 * alt="WaterfallBarRendererSample.png" /> 105 */ 106public class WaterfallBarRenderer extends BarRenderer { 107 108 /** For serialization. */ 109 private static final long serialVersionUID = -2482910643727230911L; 110 111 /** The paint used to draw the first bar. */ 112 private transient Paint firstBarPaint; 113 114 /** The paint used to draw the last bar. */ 115 private transient Paint lastBarPaint; 116 117 /** The paint used to draw bars having positive values. */ 118 private transient Paint positiveBarPaint; 119 120 /** The paint used to draw bars having negative values. */ 121 private transient Paint negativeBarPaint; 122 123 /** 124 * Constructs a new renderer with default values for the bar colors. 125 */ 126 public WaterfallBarRenderer() { 127 this(new GradientPaint(0.0f, 0.0f, new Color(0x22, 0x22, 0xFF), 128 0.0f, 0.0f, new Color(0x66, 0x66, 0xFF)), 129 new GradientPaint(0.0f, 0.0f, new Color(0x22, 0xFF, 0x22), 130 0.0f, 0.0f, new Color(0x66, 0xFF, 0x66)), 131 new GradientPaint(0.0f, 0.0f, new Color(0xFF, 0x22, 0x22), 132 0.0f, 0.0f, new Color(0xFF, 0x66, 0x66)), 133 new GradientPaint(0.0f, 0.0f, new Color(0xFF, 0xFF, 0x22), 134 0.0f, 0.0f, new Color(0xFF, 0xFF, 0x66))); 135 } 136 137 /** 138 * Constructs a new waterfall renderer. 139 * 140 * @param firstBarPaint the color of the first bar (<code>null</code> not 141 * permitted). 142 * @param positiveBarPaint the color for bars with positive values 143 * (<code>null</code> not permitted). 144 * @param negativeBarPaint the color for bars with negative values 145 * (<code>null</code> not permitted). 146 * @param lastBarPaint the color of the last bar (<code>null</code> not 147 * permitted). 148 */ 149 public WaterfallBarRenderer(Paint firstBarPaint, 150 Paint positiveBarPaint, 151 Paint negativeBarPaint, 152 Paint lastBarPaint) { 153 super(); 154 if (firstBarPaint == null) { 155 throw new IllegalArgumentException("Null 'firstBarPaint' argument"); 156 } 157 if (positiveBarPaint == null) { 158 throw new IllegalArgumentException( 159 "Null 'positiveBarPaint' argument"); 160 } 161 if (negativeBarPaint == null) { 162 throw new IllegalArgumentException( 163 "Null 'negativeBarPaint' argument"); 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(new StandardGradientPaintTransformer( 173 GradientPaintTransformType.CENTER_VERTICAL)); 174 setMinimumBarLength(1.0); 175 } 176 177 /** 178 * Returns the paint used to draw the first bar. 179 * 180 * @return The paint (never <code>null</code>). 181 */ 182 public Paint getFirstBarPaint() { 183 return this.firstBarPaint; 184 } 185 186 /** 187 * Sets the paint that will be used to draw the first bar and sends a 188 * {@link RendererChangeEvent} to all registered listeners. 189 * 190 * @param paint the paint (<code>null</code> not permitted). 191 */ 192 public void setFirstBarPaint(Paint paint) { 193 if (paint == null) { 194 throw new IllegalArgumentException("Null 'paint' argument"); 195 } 196 this.firstBarPaint = paint; 197 fireChangeEvent(); 198 } 199 200 /** 201 * Returns the paint used to draw the last bar. 202 * 203 * @return The paint (never <code>null</code>). 204 */ 205 public Paint getLastBarPaint() { 206 return this.lastBarPaint; 207 } 208 209 /** 210 * Sets the paint that will be used to draw the last bar and sends a 211 * {@link RendererChangeEvent} to all registered listeners. 212 * 213 * @param paint the paint (<code>null</code> not permitted). 214 */ 215 public void setLastBarPaint(Paint paint) { 216 if (paint == null) { 217 throw new IllegalArgumentException("Null 'paint' argument"); 218 } 219 this.lastBarPaint = paint; 220 fireChangeEvent(); 221 } 222 223 /** 224 * Returns the paint used to draw bars with positive values. 225 * 226 * @return The paint (never <code>null</code>). 227 */ 228 public Paint getPositiveBarPaint() { 229 return this.positiveBarPaint; 230 } 231 232 /** 233 * Sets the paint that will be used to draw bars having positive values. 234 * 235 * @param paint the paint (<code>null</code> not permitted). 236 */ 237 public void setPositiveBarPaint(Paint paint) { 238 if (paint == null) { 239 throw new IllegalArgumentException("Null 'paint' argument"); 240 } 241 this.positiveBarPaint = paint; 242 fireChangeEvent(); 243 } 244 245 /** 246 * Returns the paint used to draw bars with negative values. 247 * 248 * @return The paint (never <code>null</code>). 249 */ 250 public Paint getNegativeBarPaint() { 251 return this.negativeBarPaint; 252 } 253 254 /** 255 * Sets the paint that will be used to draw bars having negative values, 256 * and sends a {@link RendererChangeEvent} to all registered listeners. 257 * 258 * @param paint the paint (<code>null</code> not permitted). 259 */ 260 public void setNegativeBarPaint(Paint paint) { 261 if (paint == null) { 262 throw new IllegalArgumentException("Null 'paint' argument"); 263 } 264 this.negativeBarPaint = paint; 265 fireChangeEvent(); 266 } 267 268 /** 269 * Returns the range of values the renderer requires to display all the 270 * items from the specified dataset. 271 * 272 * @param dataset the dataset (<code>null</code> not permitted). 273 * 274 * @return The range (or <code>null</code> if the dataset is empty). 275 */ 276 public Range findRangeBounds(CategoryDataset dataset) { 277 if (dataset == null) { 278 return null; 279 } 280 boolean allItemsNull = true; // we'll set this to false if there is at 281 // least one non-null data item... 282 double minimum = 0.0; 283 double maximum = 0.0; 284 int columnCount = dataset.getColumnCount(); 285 for (int row = 0; row < dataset.getRowCount(); row++) { 286 double runningTotal = 0.0; 287 for (int column = 0; column <= columnCount - 1; column++) { 288 Number n = dataset.getValue(row, column); 289 if (n != null) { 290 allItemsNull = false; 291 double value = n.doubleValue(); 292 if (column == columnCount - 1) { 293 // treat the last column value as an absolute 294 runningTotal = value; 295 } 296 else { 297 runningTotal = runningTotal + value; 298 } 299 minimum = Math.min(minimum, runningTotal); 300 maximum = Math.max(maximum, runningTotal); 301 } 302 } 303 304 } 305 if (!allItemsNull) { 306 return new Range(minimum, maximum); 307 } 308 else { 309 return null; 310 } 311 312 } 313 314 /** 315 * Draws the bar for a single (series, category) data item. 316 * 317 * @param g2 the graphics device. 318 * @param state the renderer state. 319 * @param dataArea the data area. 320 * @param plot the plot. 321 * @param domainAxis the domain axis. 322 * @param rangeAxis the range axis. 323 * @param dataset the dataset. 324 * @param row the row index (zero-based). 325 * @param column the column index (zero-based). 326 * @param pass the pass index. 327 */ 328 public void drawItem(Graphics2D g2, 329 CategoryItemRendererState state, 330 Rectangle2D dataArea, 331 CategoryPlot plot, 332 CategoryAxis domainAxis, 333 ValueAxis rangeAxis, 334 CategoryDataset dataset, 335 int row, 336 int column, 337 int pass) { 338 339 double previous = state.getSeriesRunningTotal(); 340 if (column == dataset.getColumnCount() - 1) { 341 previous = 0.0; 342 } 343 double current = 0.0; 344 Number n = dataset.getValue(row, column); 345 if (n != null) { 346 current = previous + n.doubleValue(); 347 } 348 state.setSeriesRunningTotal(current); 349 350 int categoryCount = getColumnCount(); 351 PlotOrientation orientation = plot.getOrientation(); 352 353 double rectX = 0.0; 354 double rectY = 0.0; 355 356 RectangleEdge rangeAxisLocation = plot.getRangeAxisEdge(); 357 358 // Y0 359 double j2dy0 = rangeAxis.valueToJava2D(previous, dataArea, 360 rangeAxisLocation); 361 362 // Y1 363 double j2dy1 = rangeAxis.valueToJava2D(current, dataArea, 364 rangeAxisLocation); 365 366 double valDiff = current - previous; 367 if (j2dy1 < j2dy0) { 368 double temp = j2dy1; 369 j2dy1 = j2dy0; 370 j2dy0 = temp; 371 } 372 373 // BAR WIDTH 374 double rectWidth = state.getBarWidth(); 375 376 // BAR HEIGHT 377 double rectHeight = Math.max(getMinimumBarLength(), 378 Math.abs(j2dy1 - j2dy0)); 379 380 Comparable seriesKey = dataset.getRowKey(row); 381 Comparable categoryKey = dataset.getColumnKey(column); 382 if (orientation == PlotOrientation.HORIZONTAL) { 383 rectY = domainAxis.getCategorySeriesMiddle(categoryKey, seriesKey, 384 dataset, getItemMargin(), dataArea, RectangleEdge.LEFT); 385 386 rectX = j2dy0; 387 rectHeight = state.getBarWidth(); 388 rectY = rectY - rectHeight / 2.0; 389 rectWidth = Math.max(getMinimumBarLength(), 390 Math.abs(j2dy1 - j2dy0)); 391 392 } 393 else if (orientation == PlotOrientation.VERTICAL) { 394 rectX = domainAxis.getCategorySeriesMiddle(categoryKey, seriesKey, 395 dataset, getItemMargin(), dataArea, RectangleEdge.TOP); 396 rectX = rectX - rectWidth / 2.0; 397 rectY = j2dy0; 398 } 399 Rectangle2D bar = new Rectangle2D.Double(rectX, rectY, rectWidth, 400 rectHeight); 401 Paint seriesPaint; 402 if (column == 0) { 403 seriesPaint = getFirstBarPaint(); 404 } 405 else if (column == categoryCount - 1) { 406 seriesPaint = getLastBarPaint(); 407 } 408 else { 409 if (valDiff < 0.0) { 410 seriesPaint = getNegativeBarPaint(); 411 } 412 else if (valDiff > 0.0) { 413 seriesPaint = getPositiveBarPaint(); 414 } 415 else { 416 seriesPaint = getLastBarPaint(); 417 } 418 } 419 if (getGradientPaintTransformer() != null 420 && seriesPaint instanceof GradientPaint) { 421 GradientPaint gp = (GradientPaint) seriesPaint; 422 seriesPaint = getGradientPaintTransformer().transform(gp, bar); 423 } 424 g2.setPaint(seriesPaint); 425 g2.fill(bar); 426 427 // draw the outline... 428 if (isDrawBarOutline() 429 && state.getBarWidth() > BAR_OUTLINE_WIDTH_THRESHOLD) { 430 Stroke stroke = getItemOutlineStroke(row, column); 431 Paint paint = getItemOutlinePaint(row, column); 432 if (stroke != null && paint != null) { 433 g2.setStroke(stroke); 434 g2.setPaint(paint); 435 g2.draw(bar); 436 } 437 } 438 439 CategoryItemLabelGenerator generator 440 = getItemLabelGenerator(row, column); 441 if (generator != null && isItemLabelVisible(row, column)) { 442 drawItemLabel(g2, dataset, row, column, plot, generator, bar, 443 (valDiff < 0.0)); 444 } 445 446 // add an item entity, if this information is being collected 447 EntityCollection entities = state.getEntityCollection(); 448 if (entities != null) { 449 addItemEntity(entities, dataset, row, column, bar); 450 } 451 452 } 453 454 /** 455 * Tests an object for equality with this instance. 456 * 457 * @param obj the object (<code>null</code> permitted). 458 * 459 * @return A boolean. 460 */ 461 public boolean equals(Object obj) { 462 463 if (obj == this) { 464 return true; 465 } 466 if (!super.equals(obj)) { 467 return false; 468 } 469 if (!(obj instanceof WaterfallBarRenderer)) { 470 return false; 471 } 472 WaterfallBarRenderer that = (WaterfallBarRenderer) obj; 473 if (!PaintUtilities.equal(this.firstBarPaint, that.firstBarPaint)) { 474 return false; 475 } 476 if (!PaintUtilities.equal(this.lastBarPaint, that.lastBarPaint)) { 477 return false; 478 } 479 if (!PaintUtilities.equal(this.positiveBarPaint, 480 that.positiveBarPaint)) { 481 return false; 482 } 483 if (!PaintUtilities.equal(this.negativeBarPaint, 484 that.negativeBarPaint)) { 485 return false; 486 } 487 return true; 488 489 } 490 491 /** 492 * Provides serialization support. 493 * 494 * @param stream the output stream. 495 * 496 * @throws IOException if there is an I/O error. 497 */ 498 private void writeObject(ObjectOutputStream stream) throws IOException { 499 stream.defaultWriteObject(); 500 SerialUtilities.writePaint(this.firstBarPaint, stream); 501 SerialUtilities.writePaint(this.lastBarPaint, stream); 502 SerialUtilities.writePaint(this.positiveBarPaint, stream); 503 SerialUtilities.writePaint(this.negativeBarPaint, stream); 504 } 505 506 /** 507 * Provides serialization support. 508 * 509 * @param stream the input stream. 510 * 511 * @throws IOException if there is an I/O error. 512 * @throws ClassNotFoundException if there is a classpath problem. 513 */ 514 private void readObject(ObjectInputStream stream) 515 throws IOException, ClassNotFoundException { 516 stream.defaultReadObject(); 517 this.firstBarPaint = SerialUtilities.readPaint(stream); 518 this.lastBarPaint = SerialUtilities.readPaint(stream); 519 this.positiveBarPaint = SerialUtilities.readPaint(stream); 520 this.negativeBarPaint = SerialUtilities.readPaint(stream); 521 } 522 523}