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