001 /* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2007, 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-2007, by Object Refinery Limited and Contributors.
031 *
032 * Original Author: Darshan Shah;
033 * Contributor(s): David Gilbert (for Object Refinery Limited);
034 *
035 * $Id: WaterfallBarRenderer.java,v 1.9.2.3 2007/06/08 13:57:38 mungady Exp $
036 *
037 * Changes
038 * -------
039 * 20-Oct-2003 : Version 1, contributed by Darshan Shah (DG);
040 * 06-Nov-2003 : Changed order of parameters in constructor, and added support
041 * for GradientPaint (DG);
042 * 10-Feb-2004 : Updated drawItem() method to make cut-and-paste overriding
043 * easier. Also fixed a bug that meant the minimum bar length
044 * was being ignored (DG);
045 * 04-Oct-2004 : Reworked equals() method and renamed PaintUtils
046 * --> PaintUtilities (DG);
047 * 05-Nov-2004 : Modified drawItem() signature (DG);
048 * 07-Jan-2005 : Renamed getRangeExtent() --> findRangeBounds (DG);
049 * 23-Feb-2005 : Added argument checking (DG);
050 * 20-Apr-2005 : Renamed CategoryLabelGenerator
051 * --> CategoryItemLabelGenerator (DG);
052 * 09-Jun-2005 : Use addItemEntity() from superclass (DG);
053 *
054 */
055
056 package org.jfree.chart.renderer.category;
057
058 import java.awt.Color;
059 import java.awt.GradientPaint;
060 import java.awt.Graphics2D;
061 import java.awt.Paint;
062 import java.awt.Stroke;
063 import java.awt.geom.Rectangle2D;
064 import java.io.IOException;
065 import java.io.ObjectInputStream;
066 import java.io.ObjectOutputStream;
067 import java.io.Serializable;
068
069 import org.jfree.chart.axis.CategoryAxis;
070 import org.jfree.chart.axis.ValueAxis;
071 import org.jfree.chart.entity.EntityCollection;
072 import org.jfree.chart.event.RendererChangeEvent;
073 import org.jfree.chart.labels.CategoryItemLabelGenerator;
074 import org.jfree.chart.plot.CategoryPlot;
075 import org.jfree.chart.plot.PlotOrientation;
076 import org.jfree.chart.renderer.AbstractRenderer;
077 import org.jfree.data.Range;
078 import org.jfree.data.category.CategoryDataset;
079 import org.jfree.data.general.DatasetUtilities;
080 import org.jfree.io.SerialUtilities;
081 import org.jfree.ui.GradientPaintTransformType;
082 import org.jfree.ui.RectangleEdge;
083 import org.jfree.ui.StandardGradientPaintTransformer;
084 import org.jfree.util.PaintUtilities;
085 import org.jfree.util.PublicCloneable;
086
087 /**
088 * A renderer that handles the drawing of waterfall bar charts, for use with
089 * the {@link CategoryPlot} class. Note that the bar colors are defined
090 * using special methods in this class - the inherited methods (for example,
091 * {@link AbstractRenderer#setSeriesPaint(int, Paint)}) are ignored.
092 */
093 public class WaterfallBarRenderer extends BarRenderer
094 implements Cloneable, PublicCloneable,
095 Serializable {
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 range of values the renderer requires to display all the
168 * items from the specified dataset.
169 *
170 * @param dataset the dataset (<code>null</code> not permitted).
171 *
172 * @return The range (or <code>null</code> if the dataset is empty).
173 */
174 public Range findRangeBounds(CategoryDataset dataset) {
175 return DatasetUtilities.findCumulativeRangeBounds(dataset);
176 }
177
178 /**
179 * Returns the paint used to draw the first bar.
180 *
181 * @return The paint (never <code>null</code>).
182 */
183 public Paint getFirstBarPaint() {
184 return this.firstBarPaint;
185 }
186
187 /**
188 * Sets the paint that will be used to draw the first bar and sends a
189 * {@link RendererChangeEvent} to all registered listeners.
190 *
191 * @param paint the paint (<code>null</code> not permitted).
192 */
193 public void setFirstBarPaint(Paint paint) {
194 if (paint == null) {
195 throw new IllegalArgumentException("Null 'paint' argument");
196 }
197 this.firstBarPaint = paint;
198 notifyListeners(new RendererChangeEvent(this));
199 }
200
201 /**
202 * Returns the paint used to draw the last bar.
203 *
204 * @return The paint (never <code>null</code>).
205 */
206 public Paint getLastBarPaint() {
207 return this.lastBarPaint;
208 }
209
210 /**
211 * Sets the paint that will be used to draw the last bar.
212 *
213 * @param paint the paint (<code>null</code> not permitted).
214 */
215 public void setLastBarPaint(Paint paint) {
216 if (paint == null) {
217 throw new IllegalArgumentException("Null 'paint' argument");
218 }
219 this.lastBarPaint = paint;
220 notifyListeners(new RendererChangeEvent(this));
221 }
222
223 /**
224 * Returns the paint used to draw bars with positive values.
225 *
226 * @return The paint (never <code>null</code>).
227 */
228 public Paint getPositiveBarPaint() {
229 return this.positiveBarPaint;
230 }
231
232 /**
233 * Sets the paint that will be used to draw bars having positive values.
234 *
235 * @param paint the paint (<code>null</code> not permitted).
236 */
237 public void setPositiveBarPaint(Paint paint) {
238 if (paint == null) {
239 throw new IllegalArgumentException("Null 'paint' argument");
240 }
241 this.positiveBarPaint = paint;
242 notifyListeners(new RendererChangeEvent(this));
243 }
244
245 /**
246 * Returns the paint used to draw bars with negative values.
247 *
248 * @return The paint (never <code>null</code>).
249 */
250 public Paint getNegativeBarPaint() {
251 return this.negativeBarPaint;
252 }
253
254 /**
255 * Sets the paint that will be used to draw bars having negative values.
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 notifyListeners(new RendererChangeEvent(this));
265 }
266
267 /**
268 * Draws the bar for a single (series, category) data item.
269 *
270 * @param g2 the graphics device.
271 * @param state the renderer state.
272 * @param dataArea the data area.
273 * @param plot the plot.
274 * @param domainAxis the domain axis.
275 * @param rangeAxis the range axis.
276 * @param dataset the dataset.
277 * @param row the row index (zero-based).
278 * @param column the column index (zero-based).
279 * @param pass the pass index.
280 */
281 public void drawItem(Graphics2D g2,
282 CategoryItemRendererState state,
283 Rectangle2D dataArea,
284 CategoryPlot plot,
285 CategoryAxis domainAxis,
286 ValueAxis rangeAxis,
287 CategoryDataset dataset,
288 int row,
289 int column,
290 int pass) {
291
292 double previous = state.getSeriesRunningTotal();
293 if (column == dataset.getColumnCount() - 1) {
294 previous = 0.0;
295 }
296 double current = 0.0;
297 Number n = dataset.getValue(row, column);
298 if (n != null) {
299 current = previous + n.doubleValue();
300 }
301 state.setSeriesRunningTotal(current);
302
303 int seriesCount = getRowCount();
304 int categoryCount = getColumnCount();
305 PlotOrientation orientation = plot.getOrientation();
306
307 double rectX = 0.0;
308 double rectY = 0.0;
309
310 RectangleEdge domainAxisLocation = plot.getDomainAxisEdge();
311 RectangleEdge rangeAxisLocation = plot.getRangeAxisEdge();
312
313 // Y0
314 double j2dy0 = rangeAxis.valueToJava2D(previous, dataArea,
315 rangeAxisLocation);
316
317 // Y1
318 double j2dy1 = rangeAxis.valueToJava2D(current, dataArea,
319 rangeAxisLocation);
320
321 double valDiff = current - previous;
322 if (j2dy1 < j2dy0) {
323 double temp = j2dy1;
324 j2dy1 = j2dy0;
325 j2dy0 = temp;
326 }
327
328 // BAR WIDTH
329 double rectWidth = state.getBarWidth();
330
331 // BAR HEIGHT
332 double rectHeight = Math.max(getMinimumBarLength(),
333 Math.abs(j2dy1 - j2dy0));
334
335 if (orientation == PlotOrientation.HORIZONTAL) {
336 // BAR Y
337 rectY = domainAxis.getCategoryStart(column, getColumnCount(),
338 dataArea, domainAxisLocation);
339 if (seriesCount > 1) {
340 double seriesGap = dataArea.getHeight() * getItemMargin()
341 / (categoryCount * (seriesCount - 1));
342 rectY = rectY + row * (state.getBarWidth() + seriesGap);
343 }
344 else {
345 rectY = rectY + row * state.getBarWidth();
346 }
347
348 rectX = j2dy0;
349 rectHeight = state.getBarWidth();
350 rectWidth = Math.max(getMinimumBarLength(),
351 Math.abs(j2dy1 - j2dy0));
352
353 }
354 else if (orientation == PlotOrientation.VERTICAL) {
355 // BAR X
356 rectX = domainAxis.getCategoryStart(column, getColumnCount(),
357 dataArea, domainAxisLocation);
358
359 if (seriesCount > 1) {
360 double seriesGap = dataArea.getWidth() * getItemMargin()
361 / (categoryCount * (seriesCount - 1));
362 rectX = rectX + row * (state.getBarWidth() + seriesGap);
363 }
364 else {
365 rectX = rectX + row * state.getBarWidth();
366 }
367
368 rectY = j2dy0;
369 }
370 Rectangle2D bar = new Rectangle2D.Double(rectX, rectY, rectWidth,
371 rectHeight);
372 Paint seriesPaint = getFirstBarPaint();
373 if (column == 0) {
374 seriesPaint = getFirstBarPaint();
375 }
376 else if (column == categoryCount - 1) {
377 seriesPaint = getLastBarPaint();
378 }
379 else {
380 if (valDiff < 0.0) {
381 seriesPaint = getNegativeBarPaint();
382 }
383 else if (valDiff > 0.0) {
384 seriesPaint = getPositiveBarPaint();
385 }
386 else {
387 seriesPaint = getLastBarPaint();
388 }
389 }
390 if (getGradientPaintTransformer() != null
391 && seriesPaint instanceof GradientPaint) {
392 GradientPaint gp = (GradientPaint) seriesPaint;
393 seriesPaint = getGradientPaintTransformer().transform(gp, bar);
394 }
395 g2.setPaint(seriesPaint);
396 g2.fill(bar);
397
398 // draw the outline...
399 if (isDrawBarOutline()
400 && state.getBarWidth() > BAR_OUTLINE_WIDTH_THRESHOLD) {
401 Stroke stroke = getItemOutlineStroke(row, column);
402 Paint paint = getItemOutlinePaint(row, column);
403 if (stroke != null && paint != null) {
404 g2.setStroke(stroke);
405 g2.setPaint(paint);
406 g2.draw(bar);
407 }
408 }
409
410 CategoryItemLabelGenerator generator
411 = getItemLabelGenerator(row, column);
412 if (generator != null && isItemLabelVisible(row, column)) {
413 drawItemLabel(g2, dataset, row, column, plot, generator, bar,
414 (valDiff < 0.0));
415 }
416
417 // add an item entity, if this information is being collected
418 EntityCollection entities = state.getEntityCollection();
419 if (entities != null) {
420 addItemEntity(entities, dataset, row, column, bar);
421 }
422
423 }
424
425 /**
426 * Tests an object for equality with this instance.
427 *
428 * @param obj the object (<code>null</code> permitted).
429 *
430 * @return A boolean.
431 */
432 public boolean equals(Object obj) {
433
434 if (obj == this) {
435 return true;
436 }
437 if (!super.equals(obj)) {
438 return false;
439 }
440 if (!(obj instanceof WaterfallBarRenderer)) {
441 return false;
442 }
443 WaterfallBarRenderer that = (WaterfallBarRenderer) obj;
444 if (!PaintUtilities.equal(this.firstBarPaint, that.firstBarPaint)) {
445 return false;
446 }
447 if (!PaintUtilities.equal(this.lastBarPaint, that.lastBarPaint)) {
448 return false;
449 }
450 if (!PaintUtilities.equal(this.positiveBarPaint,
451 that.positiveBarPaint)) {
452 return false;
453 }
454 if (!PaintUtilities.equal(this.negativeBarPaint,
455 that.negativeBarPaint)) {
456 return false;
457 }
458 return true;
459
460 }
461
462 /**
463 * Provides serialization support.
464 *
465 * @param stream the output stream.
466 *
467 * @throws IOException if there is an I/O error.
468 */
469 private void writeObject(ObjectOutputStream stream) throws IOException {
470 stream.defaultWriteObject();
471 SerialUtilities.writePaint(this.firstBarPaint, stream);
472 SerialUtilities.writePaint(this.lastBarPaint, stream);
473 SerialUtilities.writePaint(this.positiveBarPaint, stream);
474 SerialUtilities.writePaint(this.negativeBarPaint, stream);
475 }
476
477 /**
478 * Provides serialization support.
479 *
480 * @param stream the input stream.
481 *
482 * @throws IOException if there is an I/O error.
483 * @throws ClassNotFoundException if there is a classpath problem.
484 */
485 private void readObject(ObjectInputStream stream)
486 throws IOException, ClassNotFoundException {
487 stream.defaultReadObject();
488 this.firstBarPaint = SerialUtilities.readPaint(stream);
489 this.lastBarPaint = SerialUtilities.readPaint(stream);
490 this.positiveBarPaint = SerialUtilities.readPaint(stream);
491 this.negativeBarPaint = SerialUtilities.readPaint(stream);
492 }
493
494 }