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