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