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