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
054package org.jfree.chart.renderer.category;
055
056import java.awt.Color;
057import java.awt.GradientPaint;
058import java.awt.Graphics2D;
059import java.awt.Paint;
060import java.awt.Stroke;
061import java.awt.geom.Rectangle2D;
062import java.io.IOException;
063import java.io.ObjectInputStream;
064import java.io.ObjectOutputStream;
065import java.io.Serializable;
066
067import org.jfree.chart.axis.CategoryAxis;
068import org.jfree.chart.axis.ValueAxis;
069import org.jfree.chart.entity.EntityCollection;
070import org.jfree.chart.event.RendererChangeEvent;
071import org.jfree.chart.labels.CategoryItemLabelGenerator;
072import org.jfree.chart.plot.CategoryPlot;
073import org.jfree.chart.plot.PlotOrientation;
074import org.jfree.chart.renderer.AbstractRenderer;
075import org.jfree.data.Range;
076import org.jfree.data.category.CategoryDataset;
077import org.jfree.data.general.DatasetUtilities;
078import org.jfree.io.SerialUtilities;
079import org.jfree.ui.GradientPaintTransformType;
080import org.jfree.ui.RectangleEdge;
081import org.jfree.ui.StandardGradientPaintTransformer;
082import org.jfree.util.PaintUtilities;
083import 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 */
091public 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        notifyListeners(new RendererChangeEvent(this));
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.
210     *
211     * @param paint  the paint (<code>null</code> not permitted).
212     */
213    public void setLastBarPaint(Paint paint) {
214        if (paint == null) {
215            throw new IllegalArgumentException("Null 'paint' argument");   
216        }
217        this.lastBarPaint = paint;
218        notifyListeners(new RendererChangeEvent(this));
219    }
220
221    /**
222     * Returns the paint used to draw bars with positive values.
223     * 
224     * @return The paint (never <code>null</code>).
225     */
226    public Paint getPositiveBarPaint() {
227        return this.positiveBarPaint;
228    }
229    
230    /**
231     * Sets the paint that will be used to draw bars having positive values.
232     *
233     * @param paint  the paint (<code>null</code> not permitted).
234     */
235    public void setPositiveBarPaint(Paint paint) {
236        if (paint == null) {
237            throw new IllegalArgumentException("Null 'paint' argument");   
238        }
239        this.positiveBarPaint = paint;
240        notifyListeners(new RendererChangeEvent(this));
241    }
242
243    /**
244     * Returns the paint used to draw bars with negative values.
245     * 
246     * @return The paint (never <code>null</code>).
247     */
248    public Paint getNegativeBarPaint() {
249        return this.negativeBarPaint;
250    }
251    
252    /**
253     * Sets the paint that will be used to draw bars having negative values.
254     *
255     * @param paint  the paint (<code>null</code> not permitted).
256     */
257    public void setNegativeBarPaint(Paint paint) {
258        if (paint == null) {
259            throw new IllegalArgumentException("Null 'paint' argument");   
260        }
261        this.negativeBarPaint = paint;
262        notifyListeners(new RendererChangeEvent(this));
263    }
264
265    /**
266     * Draws the bar for a single (series, category) data item.
267     *
268     * @param g2  the graphics device.
269     * @param state  the renderer state.
270     * @param dataArea  the data area.
271     * @param plot  the plot.
272     * @param domainAxis  the domain axis.
273     * @param rangeAxis  the range axis.
274     * @param dataset  the dataset.
275     * @param row  the row index (zero-based).
276     * @param column  the column index (zero-based).
277     * @param pass  the pass index.
278     */
279    public void drawItem(Graphics2D g2,
280                         CategoryItemRendererState state,
281                         Rectangle2D dataArea,
282                         CategoryPlot plot,
283                         CategoryAxis domainAxis,
284                         ValueAxis rangeAxis,
285                         CategoryDataset dataset,
286                         int row,
287                         int column,
288                         int pass) {
289
290        double previous = state.getSeriesRunningTotal();
291        if (column == dataset.getColumnCount() - 1) {
292            previous = 0.0;
293        }
294        double current = 0.0;
295        Number n = dataset.getValue(row, column);
296        if (n != null) {
297            current = previous + n.doubleValue();
298        }
299        state.setSeriesRunningTotal(current);
300        
301        int seriesCount = getRowCount();
302        int categoryCount = getColumnCount();
303        PlotOrientation orientation = plot.getOrientation();
304        
305        double rectX = 0.0;
306        double rectY = 0.0;
307
308        RectangleEdge domainAxisLocation = plot.getDomainAxisEdge();
309        RectangleEdge rangeAxisLocation = plot.getRangeAxisEdge();
310        
311        // Y0
312        double j2dy0 = rangeAxis.valueToJava2D(previous, dataArea, 
313                rangeAxisLocation);
314
315        // Y1
316        double j2dy1 = rangeAxis.valueToJava2D(current, dataArea, 
317                rangeAxisLocation);
318
319        double valDiff = current - previous;
320        if (j2dy1 < j2dy0) {
321            double temp = j2dy1;
322            j2dy1 = j2dy0;
323            j2dy0 = temp;
324        }
325
326        // BAR WIDTH
327        double rectWidth = state.getBarWidth();
328
329        // BAR HEIGHT
330        double rectHeight = Math.max(getMinimumBarLength(), 
331                Math.abs(j2dy1 - j2dy0));
332
333        if (orientation == PlotOrientation.HORIZONTAL) {
334            // BAR Y
335            rectY = domainAxis.getCategoryStart(column, getColumnCount(), 
336                    dataArea, domainAxisLocation);
337            if (seriesCount > 1) {
338                double seriesGap = dataArea.getHeight() * getItemMargin()
339                                   / (categoryCount * (seriesCount - 1));
340                rectY = rectY + row * (state.getBarWidth() + seriesGap);
341            }
342            else {
343                rectY = rectY + row * state.getBarWidth();
344            }
345             
346            rectX = j2dy0;
347            rectHeight = state.getBarWidth();
348            rectWidth = Math.max(getMinimumBarLength(), 
349                    Math.abs(j2dy1 - j2dy0));
350
351        }
352        else if (orientation == PlotOrientation.VERTICAL) {
353            // BAR X
354            rectX = domainAxis.getCategoryStart(column, getColumnCount(), 
355                    dataArea, domainAxisLocation);
356
357            if (seriesCount > 1) {
358                double seriesGap = dataArea.getWidth() * getItemMargin()
359                                   / (categoryCount * (seriesCount - 1));
360                rectX = rectX + row * (state.getBarWidth() + seriesGap);
361            }
362            else {
363                rectX = rectX + row * state.getBarWidth();
364            }
365
366            rectY = j2dy0;
367        }
368        Rectangle2D bar = new Rectangle2D.Double(rectX, rectY, rectWidth, 
369                rectHeight);
370        Paint seriesPaint = getFirstBarPaint();
371        if (column == 0) {
372            seriesPaint = getFirstBarPaint();
373        }
374        else if (column == categoryCount - 1) {
375            seriesPaint = getLastBarPaint();    
376        } 
377        else {
378            if (valDiff < 0.0) {
379                seriesPaint = getNegativeBarPaint();
380            } 
381            else if (valDiff > 0.0) {
382                seriesPaint = getPositiveBarPaint();
383            } 
384            else {
385                seriesPaint = getLastBarPaint();
386            }
387        }
388        if (getGradientPaintTransformer() != null 
389                && seriesPaint instanceof GradientPaint) {
390            GradientPaint gp = (GradientPaint) seriesPaint;
391            seriesPaint = getGradientPaintTransformer().transform(gp, bar);
392        }
393        g2.setPaint(seriesPaint);
394        g2.fill(bar);
395        
396        // draw the outline...
397        if (isDrawBarOutline() 
398                && state.getBarWidth() > BAR_OUTLINE_WIDTH_THRESHOLD) {
399            Stroke stroke = getItemOutlineStroke(row, column);
400            Paint paint = getItemOutlinePaint(row, column);
401            if (stroke != null && paint != null) {
402                g2.setStroke(stroke);
403                g2.setPaint(paint);
404                g2.draw(bar);
405            }
406        }
407        
408        CategoryItemLabelGenerator generator 
409            = getItemLabelGenerator(row, column);
410        if (generator != null && isItemLabelVisible(row, column)) {
411            drawItemLabel(g2, dataset, row, column, plot, generator, bar, 
412                    (valDiff < 0.0));
413        }        
414
415        // add an item entity, if this information is being collected
416        EntityCollection entities = state.getEntityCollection();
417        if (entities != null) {
418            addItemEntity(entities, dataset, row, column, bar);
419        }
420
421    }
422    
423    /**
424     * Tests an object for equality with this instance.
425     * 
426     * @param obj  the object (<code>null</code> permitted).
427     * 
428     * @return A boolean.
429     */
430    public boolean equals(Object obj) {
431        
432        if (obj == this) {
433            return true;
434        }
435        if (!super.equals(obj)) {
436            return false;
437        }
438        if (!(obj instanceof WaterfallBarRenderer)) {
439            return false;
440        }
441        WaterfallBarRenderer that = (WaterfallBarRenderer) obj;
442        if (!PaintUtilities.equal(this.firstBarPaint, that.firstBarPaint)) {
443            return false;
444        }
445        if (!PaintUtilities.equal(this.lastBarPaint, that.lastBarPaint)) {
446            return false;
447        }             
448        if (!PaintUtilities.equal(this.positiveBarPaint, 
449                that.positiveBarPaint)) {
450            return false;
451        }             
452        if (!PaintUtilities.equal(this.negativeBarPaint, 
453                that.negativeBarPaint)) {
454            return false;
455        }             
456        return true;
457        
458    }
459    
460    /**
461     * Provides serialization support.
462     *
463     * @param stream  the output stream.
464     *
465     * @throws IOException  if there is an I/O error.
466     */
467    private void writeObject(ObjectOutputStream stream) throws IOException {
468        stream.defaultWriteObject();
469        SerialUtilities.writePaint(this.firstBarPaint, stream);
470        SerialUtilities.writePaint(this.lastBarPaint, stream);
471        SerialUtilities.writePaint(this.positiveBarPaint, stream);
472        SerialUtilities.writePaint(this.negativeBarPaint, stream);
473    }
474
475    /**
476     * Provides serialization support.
477     *
478     * @param stream  the input stream.
479     *
480     * @throws IOException  if there is an I/O error.
481     * @throws ClassNotFoundException  if there is a classpath problem.
482     */
483    private void readObject(ObjectInputStream stream) 
484        throws IOException, ClassNotFoundException {
485        stream.defaultReadObject();
486        this.firstBarPaint = SerialUtilities.readPaint(stream);
487        this.lastBarPaint = SerialUtilities.readPaint(stream);
488        this.positiveBarPaint = SerialUtilities.readPaint(stream);
489        this.negativeBarPaint = SerialUtilities.readPaint(stream);
490    }
491
492}