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