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