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