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