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 }