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