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