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