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