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