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