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
059package org.jfree.chart.renderer.category;
060
061import java.awt.Color;
062import java.awt.GradientPaint;
063import java.awt.Graphics2D;
064import java.awt.Paint;
065import java.awt.Stroke;
066import java.awt.geom.Rectangle2D;
067import java.io.IOException;
068import java.io.ObjectInputStream;
069import java.io.ObjectOutputStream;
070
071import org.jfree.chart.axis.CategoryAxis;
072import org.jfree.chart.axis.ValueAxis;
073import org.jfree.chart.entity.EntityCollection;
074import org.jfree.chart.event.RendererChangeEvent;
075import org.jfree.chart.labels.CategoryItemLabelGenerator;
076import org.jfree.chart.plot.CategoryPlot;
077import org.jfree.chart.plot.PlotOrientation;
078import org.jfree.chart.renderer.AbstractRenderer;
079import org.jfree.data.Range;
080import org.jfree.data.category.CategoryDataset;
081import org.jfree.io.SerialUtilities;
082import org.jfree.ui.GradientPaintTransformType;
083import org.jfree.ui.RectangleEdge;
084import org.jfree.ui.StandardGradientPaintTransformer;
085import 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 */
105public 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}