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     * 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     * 
052     */
053    
054    package org.jfree.chart.renderer.category;
055    
056    import java.awt.Color;
057    import java.awt.GradientPaint;
058    import java.awt.Graphics2D;
059    import java.awt.Paint;
060    import java.awt.Stroke;
061    import java.awt.geom.Rectangle2D;
062    import java.io.IOException;
063    import java.io.ObjectInputStream;
064    import java.io.ObjectOutputStream;
065    import java.io.Serializable;
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.data.general.DatasetUtilities;
078    import org.jfree.io.SerialUtilities;
079    import org.jfree.ui.GradientPaintTransformType;
080    import org.jfree.ui.RectangleEdge;
081    import org.jfree.ui.StandardGradientPaintTransformer;
082    import org.jfree.util.PaintUtilities;
083    import org.jfree.util.PublicCloneable;
084    
085    /**
086     * A renderer that handles the drawing of waterfall bar charts, for use with 
087     * the {@link CategoryPlot} class.  Note that the bar colors are defined 
088     * using special methods in this class - the inherited methods (for example,
089     * {@link AbstractRenderer#setSeriesPaint(int, Paint)}) are ignored.
090     */
091    public class WaterfallBarRenderer extends BarRenderer 
092                                      implements Cloneable, PublicCloneable, 
093                                                 Serializable {
094    
095        /** For serialization. */
096        private static final long serialVersionUID = -2482910643727230911L;
097        
098        /** The paint used to draw the first bar. */
099        private transient Paint firstBarPaint;
100    
101        /** The paint used to draw the last bar. */
102        private transient Paint lastBarPaint;
103    
104        /** The paint used to draw bars having positive values. */
105        private transient Paint positiveBarPaint;
106    
107        /** The paint used to draw bars having negative values. */
108        private transient Paint negativeBarPaint;
109    
110        /**
111         * Constructs a new renderer with default values for the bar colors.
112         */
113        public WaterfallBarRenderer() {
114            this(new GradientPaint(0.0f, 0.0f, new Color(0x22, 0x22, 0xFF), 
115                    0.0f, 0.0f, new Color(0x66, 0x66, 0xFF)), 
116                    new GradientPaint(0.0f, 0.0f, new Color(0x22, 0xFF, 0x22), 
117                    0.0f, 0.0f, new Color(0x66, 0xFF, 0x66)), 
118                    new GradientPaint(0.0f, 0.0f, new Color(0xFF, 0x22, 0x22), 
119                    0.0f, 0.0f, new Color(0xFF, 0x66, 0x66)),
120                    new GradientPaint(0.0f, 0.0f, new Color(0xFF, 0xFF, 0x22), 
121                    0.0f, 0.0f, new Color(0xFF, 0xFF, 0x66)));
122        }
123    
124        /**
125         * Constructs a new waterfall renderer.
126         *
127         * @param firstBarPaint  the color of the first bar (<code>null</code> not 
128         *                       permitted).
129         * @param positiveBarPaint  the color for bars with positive values 
130         *                          (<code>null</code> not permitted).
131         * @param negativeBarPaint  the color for bars with negative values 
132         *                          (<code>null</code> not permitted).
133         * @param lastBarPaint  the color of the last bar (<code>null</code> not 
134         *                      permitted).
135         */
136        public WaterfallBarRenderer(Paint firstBarPaint, 
137                                    Paint positiveBarPaint, 
138                                    Paint negativeBarPaint,
139                                    Paint lastBarPaint) {
140            super();
141            if (firstBarPaint == null) {
142                throw new IllegalArgumentException("Null 'firstBarPaint' argument");
143            }
144            if (positiveBarPaint == null) {
145                throw new IllegalArgumentException(
146                        "Null 'positiveBarPaint' argument");   
147            }
148            if (negativeBarPaint == null) {
149                throw new IllegalArgumentException(
150                        "Null 'negativeBarPaint' argument");   
151            }
152            if (lastBarPaint == null) {
153                throw new IllegalArgumentException("Null 'lastBarPaint' argument");
154            }
155            this.firstBarPaint = firstBarPaint;
156            this.lastBarPaint = lastBarPaint;
157            this.positiveBarPaint = positiveBarPaint;
158            this.negativeBarPaint = negativeBarPaint;
159            setGradientPaintTransformer(new StandardGradientPaintTransformer(
160                    GradientPaintTransformType.CENTER_VERTICAL));
161            setMinimumBarLength(1.0);
162        }
163    
164        /**
165         * Returns the range of values the renderer requires to display all the 
166         * items from the specified dataset.
167         * 
168         * @param dataset  the dataset (<code>null</code> not permitted).
169         * 
170         * @return The range (or <code>null</code> if the dataset is empty).
171         */
172        public Range findRangeBounds(CategoryDataset dataset) {
173            return DatasetUtilities.findCumulativeRangeBounds(dataset);   
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         * 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    }