001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2007, 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-2007, by Object Refinery Limited and Contributors.
031     *
032     * Original Author:  Darshan Shah;
033     * Contributor(s):   David Gilbert (for Object Refinery Limited);
034     *
035     * $Id: WaterfallBarRenderer.java,v 1.9.2.3 2007/06/08 13:57:38 mungady Exp $
036     *
037     * Changes
038     * -------
039     * 20-Oct-2003 : Version 1, contributed by Darshan Shah (DG);
040     * 06-Nov-2003 : Changed order of parameters in constructor, and added support 
041     *               for GradientPaint (DG);
042     * 10-Feb-2004 : Updated drawItem() method to make cut-and-paste overriding 
043     *               easier.  Also fixed a bug that meant the minimum bar length 
044     *               was being ignored (DG);
045     * 04-Oct-2004 : Reworked equals() method and renamed PaintUtils 
046     *               --> PaintUtilities (DG);
047     * 05-Nov-2004 : Modified drawItem() signature (DG);
048     * 07-Jan-2005 : Renamed getRangeExtent() --> findRangeBounds (DG);
049     * 23-Feb-2005 : Added argument checking (DG);
050     * 20-Apr-2005 : Renamed CategoryLabelGenerator 
051     *               --> CategoryItemLabelGenerator (DG);
052     * 09-Jun-2005 : Use addItemEntity() from superclass (DG);
053     * 
054     */
055    
056    package org.jfree.chart.renderer.category;
057    
058    import java.awt.Color;
059    import java.awt.GradientPaint;
060    import java.awt.Graphics2D;
061    import java.awt.Paint;
062    import java.awt.Stroke;
063    import java.awt.geom.Rectangle2D;
064    import java.io.IOException;
065    import java.io.ObjectInputStream;
066    import java.io.ObjectOutputStream;
067    import java.io.Serializable;
068    
069    import org.jfree.chart.axis.CategoryAxis;
070    import org.jfree.chart.axis.ValueAxis;
071    import org.jfree.chart.entity.EntityCollection;
072    import org.jfree.chart.event.RendererChangeEvent;
073    import org.jfree.chart.labels.CategoryItemLabelGenerator;
074    import org.jfree.chart.plot.CategoryPlot;
075    import org.jfree.chart.plot.PlotOrientation;
076    import org.jfree.chart.renderer.AbstractRenderer;
077    import org.jfree.data.Range;
078    import org.jfree.data.category.CategoryDataset;
079    import org.jfree.data.general.DatasetUtilities;
080    import org.jfree.io.SerialUtilities;
081    import org.jfree.ui.GradientPaintTransformType;
082    import org.jfree.ui.RectangleEdge;
083    import org.jfree.ui.StandardGradientPaintTransformer;
084    import org.jfree.util.PaintUtilities;
085    import org.jfree.util.PublicCloneable;
086    
087    /**
088     * A renderer that handles the drawing of waterfall bar charts, for use with 
089     * the {@link CategoryPlot} class.  Note that the bar colors are defined 
090     * using special methods in this class - the inherited methods (for example,
091     * {@link AbstractRenderer#setSeriesPaint(int, Paint)}) are ignored.
092     */
093    public class WaterfallBarRenderer extends BarRenderer 
094                                      implements Cloneable, PublicCloneable, 
095                                                 Serializable {
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 range of values the renderer requires to display all the 
168         * items from the specified dataset.
169         * 
170         * @param dataset  the dataset (<code>null</code> not permitted).
171         * 
172         * @return The range (or <code>null</code> if the dataset is empty).
173         */
174        public Range findRangeBounds(CategoryDataset dataset) {
175            return DatasetUtilities.findCumulativeRangeBounds(dataset);   
176        }
177    
178        /**
179         * Returns the paint used to draw the first bar.
180         * 
181         * @return The paint (never <code>null</code>).
182         */
183        public Paint getFirstBarPaint() {
184            return this.firstBarPaint;
185        }
186        
187        /**
188         * Sets the paint that will be used to draw the first bar and sends a
189         * {@link RendererChangeEvent} to all registered listeners.
190         *
191         * @param paint  the paint (<code>null</code> not permitted).
192         */
193        public void setFirstBarPaint(Paint paint) {
194            if (paint == null) {
195                throw new IllegalArgumentException("Null 'paint' argument");   
196            }
197            this.firstBarPaint = paint;
198            notifyListeners(new RendererChangeEvent(this));
199        }
200    
201        /**
202         * Returns the paint used to draw the last bar.
203         * 
204         * @return The paint (never <code>null</code>).
205         */
206        public Paint getLastBarPaint() {
207            return this.lastBarPaint;
208        }
209        
210        /**
211         * Sets the paint that will be used to draw the last bar.
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            notifyListeners(new RendererChangeEvent(this));
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            notifyListeners(new RendererChangeEvent(this));
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         *
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            notifyListeners(new RendererChangeEvent(this));
265        }
266    
267        /**
268         * Draws the bar for a single (series, category) data item.
269         *
270         * @param g2  the graphics device.
271         * @param state  the renderer state.
272         * @param dataArea  the data area.
273         * @param plot  the plot.
274         * @param domainAxis  the domain axis.
275         * @param rangeAxis  the range axis.
276         * @param dataset  the dataset.
277         * @param row  the row index (zero-based).
278         * @param column  the column index (zero-based).
279         * @param pass  the pass index.
280         */
281        public void drawItem(Graphics2D g2,
282                             CategoryItemRendererState state,
283                             Rectangle2D dataArea,
284                             CategoryPlot plot,
285                             CategoryAxis domainAxis,
286                             ValueAxis rangeAxis,
287                             CategoryDataset dataset,
288                             int row,
289                             int column,
290                             int pass) {
291    
292            double previous = state.getSeriesRunningTotal();
293            if (column == dataset.getColumnCount() - 1) {
294                previous = 0.0;
295            }
296            double current = 0.0;
297            Number n = dataset.getValue(row, column);
298            if (n != null) {
299                current = previous + n.doubleValue();
300            }
301            state.setSeriesRunningTotal(current);
302            
303            int seriesCount = getRowCount();
304            int categoryCount = getColumnCount();
305            PlotOrientation orientation = plot.getOrientation();
306            
307            double rectX = 0.0;
308            double rectY = 0.0;
309    
310            RectangleEdge domainAxisLocation = plot.getDomainAxisEdge();
311            RectangleEdge rangeAxisLocation = plot.getRangeAxisEdge();
312            
313            // Y0
314            double j2dy0 = rangeAxis.valueToJava2D(previous, dataArea, 
315                    rangeAxisLocation);
316    
317            // Y1
318            double j2dy1 = rangeAxis.valueToJava2D(current, dataArea, 
319                    rangeAxisLocation);
320    
321            double valDiff = current - previous;
322            if (j2dy1 < j2dy0) {
323                double temp = j2dy1;
324                j2dy1 = j2dy0;
325                j2dy0 = temp;
326            }
327    
328            // BAR WIDTH
329            double rectWidth = state.getBarWidth();
330    
331            // BAR HEIGHT
332            double rectHeight = Math.max(getMinimumBarLength(), 
333                    Math.abs(j2dy1 - j2dy0));
334    
335            if (orientation == PlotOrientation.HORIZONTAL) {
336                // BAR Y
337                rectY = domainAxis.getCategoryStart(column, getColumnCount(), 
338                        dataArea, domainAxisLocation);
339                if (seriesCount > 1) {
340                    double seriesGap = dataArea.getHeight() * getItemMargin()
341                                       / (categoryCount * (seriesCount - 1));
342                    rectY = rectY + row * (state.getBarWidth() + seriesGap);
343                }
344                else {
345                    rectY = rectY + row * state.getBarWidth();
346                }
347                 
348                rectX = j2dy0;
349                rectHeight = state.getBarWidth();
350                rectWidth = Math.max(getMinimumBarLength(), 
351                        Math.abs(j2dy1 - j2dy0));
352    
353            }
354            else if (orientation == PlotOrientation.VERTICAL) {
355                // BAR X
356                rectX = domainAxis.getCategoryStart(column, getColumnCount(), 
357                        dataArea, domainAxisLocation);
358    
359                if (seriesCount > 1) {
360                    double seriesGap = dataArea.getWidth() * getItemMargin()
361                                       / (categoryCount * (seriesCount - 1));
362                    rectX = rectX + row * (state.getBarWidth() + seriesGap);
363                }
364                else {
365                    rectX = rectX + row * state.getBarWidth();
366                }
367    
368                rectY = j2dy0;
369            }
370            Rectangle2D bar = new Rectangle2D.Double(rectX, rectY, rectWidth, 
371                    rectHeight);
372            Paint seriesPaint = getFirstBarPaint();
373            if (column == 0) {
374                seriesPaint = getFirstBarPaint();
375            }
376            else if (column == categoryCount - 1) {
377                seriesPaint = getLastBarPaint();    
378            } 
379            else {
380                if (valDiff < 0.0) {
381                    seriesPaint = getNegativeBarPaint();
382                } 
383                else if (valDiff > 0.0) {
384                    seriesPaint = getPositiveBarPaint();
385                } 
386                else {
387                    seriesPaint = getLastBarPaint();
388                }
389            }
390            if (getGradientPaintTransformer() != null 
391                    && seriesPaint instanceof GradientPaint) {
392                GradientPaint gp = (GradientPaint) seriesPaint;
393                seriesPaint = getGradientPaintTransformer().transform(gp, bar);
394            }
395            g2.setPaint(seriesPaint);
396            g2.fill(bar);
397            
398            // draw the outline...
399            if (isDrawBarOutline() 
400                    && state.getBarWidth() > BAR_OUTLINE_WIDTH_THRESHOLD) {
401                Stroke stroke = getItemOutlineStroke(row, column);
402                Paint paint = getItemOutlinePaint(row, column);
403                if (stroke != null && paint != null) {
404                    g2.setStroke(stroke);
405                    g2.setPaint(paint);
406                    g2.draw(bar);
407                }
408            }
409            
410            CategoryItemLabelGenerator generator 
411                = getItemLabelGenerator(row, column);
412            if (generator != null && isItemLabelVisible(row, column)) {
413                drawItemLabel(g2, dataset, row, column, plot, generator, bar, 
414                        (valDiff < 0.0));
415            }        
416    
417            // add an item entity, if this information is being collected
418            EntityCollection entities = state.getEntityCollection();
419            if (entities != null) {
420                addItemEntity(entities, dataset, row, column, bar);
421            }
422    
423        }
424        
425        /**
426         * Tests an object for equality with this instance.
427         * 
428         * @param obj  the object (<code>null</code> permitted).
429         * 
430         * @return A boolean.
431         */
432        public boolean equals(Object obj) {
433            
434            if (obj == this) {
435                return true;
436            }
437            if (!super.equals(obj)) {
438                return false;
439            }
440            if (!(obj instanceof WaterfallBarRenderer)) {
441                return false;
442            }
443            WaterfallBarRenderer that = (WaterfallBarRenderer) obj;
444            if (!PaintUtilities.equal(this.firstBarPaint, that.firstBarPaint)) {
445                return false;
446            }
447            if (!PaintUtilities.equal(this.lastBarPaint, that.lastBarPaint)) {
448                return false;
449            }             
450            if (!PaintUtilities.equal(this.positiveBarPaint, 
451                    that.positiveBarPaint)) {
452                return false;
453            }             
454            if (!PaintUtilities.equal(this.negativeBarPaint, 
455                    that.negativeBarPaint)) {
456                return false;
457            }             
458            return true;
459            
460        }
461        
462        /**
463         * Provides serialization support.
464         *
465         * @param stream  the output stream.
466         *
467         * @throws IOException  if there is an I/O error.
468         */
469        private void writeObject(ObjectOutputStream stream) throws IOException {
470            stream.defaultWriteObject();
471            SerialUtilities.writePaint(this.firstBarPaint, stream);
472            SerialUtilities.writePaint(this.lastBarPaint, stream);
473            SerialUtilities.writePaint(this.positiveBarPaint, stream);
474            SerialUtilities.writePaint(this.negativeBarPaint, stream);
475        }
476    
477        /**
478         * Provides serialization support.
479         *
480         * @param stream  the input stream.
481         *
482         * @throws IOException  if there is an I/O error.
483         * @throws ClassNotFoundException  if there is a classpath problem.
484         */
485        private void readObject(ObjectInputStream stream) 
486            throws IOException, ClassNotFoundException {
487            stream.defaultReadObject();
488            this.firstBarPaint = SerialUtilities.readPaint(stream);
489            this.lastBarPaint = SerialUtilities.readPaint(stream);
490            this.positiveBarPaint = SerialUtilities.readPaint(stream);
491            this.negativeBarPaint = SerialUtilities.readPaint(stream);
492        }
493    
494    }