    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     * StackedAreaRenderer.java
    029     * ------------------------
    030     * (C) Copyright 2002-2008, by Dan Rivett (d.rivett@ukonline.co.uk) and
    031     *                          Contributors.
    032     *
    033     * Original Author:  Dan Rivett (adapted from AreaCategoryItemRenderer);
    034     * Contributor(s):   Jon Iles;
    035     *                   David Gilbert (for Object Refinery Limited);
    036     *                   Christian W. Zuckschwerdt;
    037     *
    038     * Changes:
    039     * --------
    040     * 20-Sep-2002 : Version 1, contributed by Dan Rivett;
    041     * 24-Oct-2002 : Amendments for changes in CategoryDataset interface and
    042     *               CategoryToolTipGenerator interface (DG);
    043     * 01-Nov-2002 : Added tooltips (DG);
    044     * 06-Nov-2002 : Renamed drawCategoryItem() --> drawItem() and now using axis
    045     *               for category spacing. Renamed StackedAreaCategoryItemRenderer
    046     *               --> StackedAreaRenderer (DG);
    047     * 26-Nov-2002 : Switched CategoryDataset --> TableDataset (DG);
    048     * 26-Nov-2002 : Replaced isStacked() method with getRangeType() method (DG);
    049     * 17-Jan-2003 : Moved plot classes to a separate package (DG);
    050     * 25-Mar-2003 : Implemented Serializable (DG);
    051     * 13-May-2003 : Modified to take into account the plot orientation (DG);
    052     * 30-Jul-2003 : Modified entity constructor (CZ);
    053     * 07-Oct-2003 : Added renderer state (DG);
    054     * 29-Apr-2004 : Added getRangeExtent() override (DG);
    055     * 05-Nov-2004 : Modified drawItem() signature (DG);
    056     * 07-Jan-2005 : Renamed getRangeExtent() --> findRangeBounds() (DG);
    057     * ------------- JFREECHART 1.0.x ---------------------------------------------
    058     * 11-Oct-2006 : Added support for rendering data values as percentages,
    059     *               and added a second pass for drawing item labels (DG);
    060     *
    061     */
    062
    063    package org.jfree.chart.renderer.category;
    064
    065    import java.awt.Graphics2D;
    066    import java.awt.Paint;
    067    import java.awt.Shape;
    068    import java.awt.geom.GeneralPath;
    069    import java.awt.geom.Rectangle2D;
    070    import java.io.Serializable;
    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.plot.CategoryPlot;
    077    import org.jfree.data.DataUtilities;
    078    import org.jfree.data.Range;
    079    import org.jfree.data.category.CategoryDataset;
    080    import org.jfree.data.general.DatasetUtilities;
    081    import org.jfree.ui.RectangleEdge;
    082    import org.jfree.util.PublicCloneable;
    083
    084    /**
    085     * A renderer that draws stacked area charts for a
    086     * {@link org.jfree.chart.plot.CategoryPlot}.
    087     */
    088    public class StackedAreaRenderer extends AreaRenderer
    089            implements Cloneable, PublicCloneable, Serializable {
    090
    091        /** For serialization. */
    092        private static final long serialVersionUID = -3595635038460823663L;
    093
    094        /** A flag that controls whether the areas display values or percentages. */
    095        private boolean renderAsPercentages;
    096
    097        /**
    098         * Creates a new renderer.
    099         */
    100        public StackedAreaRenderer() {
    101            this(false);
    102        }
    103
    104        /**
    105         * Creates a new renderer.
    106         *
    107         * @param renderAsPercentages  a flag that controls whether the data values
    108         *                             are rendered as percentages.
    109         */
    110        public StackedAreaRenderer(boolean renderAsPercentages) {
    111            super();
    112            this.renderAsPercentages = renderAsPercentages;
    113        }
    114
    115        /**
    116         * Returns <code>true</code> if the renderer displays each item value as
    117         * a percentage (so that the stacked areas add to 100%), and
    118         * <code>false</code> otherwise.
    119         *
    120         * @return A boolean.
    121         *
    122         * @since 1.0.3
    123         */
    124        public boolean getRenderAsPercentages() {
    125            return this.renderAsPercentages;
    126        }
    127
    128        /**
    129         * Sets the flag that controls whether the renderer displays each item
    130         * value as a percentage (so that the stacked areas add to 100%), and sends
    131         * a {@link RendererChangeEvent} to all registered listeners.
    132         *
    133         * @param asPercentages  the flag.
    134         *
    135         * @since 1.0.3
    136         */
    137        public void setRenderAsPercentages(boolean asPercentages) {
    138            this.renderAsPercentages = asPercentages;
    139            fireChangeEvent();
    140        }
    141
    142        /**
    143         * Returns the number of passes (<code>2</code>) required by this renderer.
    144         * The first pass is used to draw the bars, the second pass is used to
    145         * draw the item labels (if visible).
    146         *
    147         * @return The number of passes required by the renderer.
    148         */
    149        public int getPassCount() {
    150            return 2;
    151        }
    152
    153        /**
    154         * Returns the range of values the renderer requires to display all the
    155         * items from the specified dataset.
    156         *
    157         * @param dataset  the dataset (<code>null</code> not permitted).
    158         *
    159         * @return The range (or <code>null</code> if the dataset is empty).
    160         */
    161        public Range findRangeBounds(CategoryDataset dataset) {
    162            if (this.renderAsPercentages) {
    163                return new Range(0.0, 1.0);
    164            }
    165            else {
    166                return DatasetUtilities.findStackedRangeBounds(dataset);
    167            }
    168        }
    169
    170        /**
    171         * Draw a single data item.
    172         *
    173         * @param g2  the graphics device.
    174         * @param state  the renderer state.
    175         * @param dataArea  the data plot area.
    176         * @param plot  the plot.
    177         * @param domainAxis  the domain axis.
    178         * @param rangeAxis  the range axis.
    179         * @param dataset  the data.
    180         * @param row  the row index (zero-based).
    181         * @param column  the column index (zero-based).
    182         * @param pass  the pass index.
    183         */
    184        public void drawItem(Graphics2D g2,
    185                             CategoryItemRendererState state,
    186                             Rectangle2D dataArea,
    187                             CategoryPlot plot,
    188                             CategoryAxis domainAxis,
    189                             ValueAxis rangeAxis,
    190                             CategoryDataset dataset,
    191                             int row,
    192                             int column,
    193                             int pass) {
    194
    195            // setup for collecting optional entity info...
    196            Shape entityArea = null;
    197            EntityCollection entities = state.getEntityCollection();
    198
    199            double y1 = 0.0;
    200            Number n = dataset.getValue(row, column);
    201            if (n != null) {
    202                y1 = n.doubleValue();
    203            }
    204            double[] stack1 = getStackValues(dataset, row, column);
    205
    206
    207            // leave the y values (y1, y0) untranslated as it is going to be be
    208            // stacked up later by previous series values, after this it will be
    209            // translated.
    210            double xx1 = domainAxis.getCategoryMiddle(column, getColumnCount(),
    211                    dataArea, plot.getDomainAxisEdge());
    212
    213
    214            // get the previous point and the next point so we can calculate a
    215            // "hot spot" for the area (used by the chart entity)...
    216            double y0 = 0.0;
    217            n = dataset.getValue(row, Math.max(column - 1, 0));
    218            if (n != null) {
    219                y0 = n.doubleValue();
    220            }
    221            double[] stack0 = getStackValues(dataset, row, Math.max(column - 1, 0));
    222
    223            // FIXME: calculate xx0
    224            double xx0 = domainAxis.getCategoryStart(column, getColumnCount(),
    225                    dataArea, plot.getDomainAxisEdge());
    226
    227            int itemCount = dataset.getColumnCount();
    228            double y2 = 0.0;
    229            n = dataset.getValue(row, Math.min(column + 1, itemCount - 1));
    230            if (n != null) {
    231                y2 = n.doubleValue();
    232            }
    233            double[] stack2 = getStackValues(dataset, row, Math.min(column + 1,
    234                    itemCount - 1));
    235
    236            double xx2 = domainAxis.getCategoryEnd(column, getColumnCount(),
    237                    dataArea, plot.getDomainAxisEdge());
    238
    239            // FIXME: calculate xxLeft and xxRight
    240            double xxLeft = xx0;
    241            double xxRight = xx2;
    242
    243            double[] stackLeft = averageStackValues(stack0, stack1);
    244            double[] stackRight = averageStackValues(stack1, stack2);
    245            double[] adjStackLeft = adjustedStackValues(stack0, stack1);
    246            double[] adjStackRight = adjustedStackValues(stack1, stack2);
    247
    248            float transY1;
    249
    250            RectangleEdge edge1 = plot.getRangeAxisEdge();
    251
    252            GeneralPath left = new GeneralPath();
    253            GeneralPath right = new GeneralPath();
    254            if (y1 >= 0.0) {  // handle positive value
    255                transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1], dataArea,
    256                        edge1);
    257                float transStack1 = (float) rangeAxis.valueToJava2D(stack1[1],
    258                        dataArea, edge1);
    259                float transStackLeft = (float) rangeAxis.valueToJava2D(
    260                        adjStackLeft[1], dataArea, edge1);
    261
    262                // LEFT POLYGON
    263                if (y0 >= 0.0) {
    264                    double yleft = (y0 + y1) / 2.0 + stackLeft[1];
    265                    float transYLeft
    266                        = (float) rangeAxis.valueToJava2D(yleft, dataArea, edge1);
    267                    left.moveTo((float) xx1, transY1);
    268                    left.lineTo((float) xx1, transStack1);
    269                    left.lineTo((float) xxLeft, transStackLeft);
    270                    left.lineTo((float) xxLeft, transYLeft);
    271                    left.closePath();
    272                }
    273                else {
    274                    left.moveTo((float) xx1, transStack1);
    275                    left.lineTo((float) xx1, transY1);
    276                    left.lineTo((float) xxLeft, transStackLeft);
    277                    left.closePath();
    278                }
    279
    280                float transStackRight = (float) rangeAxis.valueToJava2D(
    281                        adjStackRight[1], dataArea, edge1);
    282                // RIGHT POLYGON
    283                if (y2 >= 0.0) {
    284                    double yright = (y1 + y2) / 2.0 + stackRight[1];
    285                    float transYRight
    286                        = (float) rangeAxis.valueToJava2D(yright, dataArea, edge1);
    287                    right.moveTo((float) xx1, transStack1);
    288                    right.lineTo((float) xx1, transY1);
    289                    right.lineTo((float) xxRight, transYRight);
    290                    right.lineTo((float) xxRight, transStackRight);
    291                    right.closePath();
    292                }
    293                else {
    294                    right.moveTo((float) xx1, transStack1);
    295                    right.lineTo((float) xx1, transY1);
    296                    right.lineTo((float) xxRight, transStackRight);
    297                    right.closePath();
    298                }
    299            }
    300            else {  // handle negative value
    301                transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0], dataArea,
    302                        edge1);
    303                float transStack1 = (float) rangeAxis.valueToJava2D(stack1[0],
    304                        dataArea, edge1);
    305                float transStackLeft = (float) rangeAxis.valueToJava2D(
    306                        adjStackLeft[0], dataArea, edge1);
    307
    308                // LEFT POLYGON
    309                if (y0 >= 0.0) {
    310                    left.moveTo((float) xx1, transStack1);
    311                    left.lineTo((float) xx1, transY1);
    312                    left.lineTo((float) xxLeft, transStackLeft);
    313                    left.clone();
    314                }
    315                else {
    316                    double yleft = (y0 + y1) / 2.0 + stackLeft[0];
    317                    float transYLeft = (float) rangeAxis.valueToJava2D(yleft,
    318                            dataArea, edge1);
    319                    left.moveTo((float) xx1, transY1);
    320                    left.lineTo((float) xx1, transStack1);
    321                    left.lineTo((float) xxLeft, transStackLeft);
    322                    left.lineTo((float) xxLeft, transYLeft);
    323                    left.closePath();
    324                }
    325                float transStackRight = (float) rangeAxis.valueToJava2D(
    326                        adjStackRight[0], dataArea, edge1);
    327
    328                // RIGHT POLYGON
    329                if (y2 >= 0.0) {
    330                    right.moveTo((float) xx1, transStack1);
    331                    right.lineTo((float) xx1, transY1);
    332                    right.lineTo((float) xxRight, transStackRight);
    333                    right.closePath();
    334                }
    335                else {
    336                    double yright = (y1 + y2) / 2.0 + stackRight[0];
    337                    float transYRight = (float) rangeAxis.valueToJava2D(yright,
    338                            dataArea, edge1);
    339                    right.moveTo((float) xx1, transStack1);
    340                    right.lineTo((float) xx1, transY1);
    341                    right.lineTo((float) xxRight, transYRight);
    342                    right.lineTo((float) xxRight, transStackRight);
    343                    right.closePath();
    344                }
    345            }
    346
    347            g2.setPaint(getItemPaint(row, column));
    348            g2.setStroke(getItemStroke(row, column));
    349
    350            //  Get series Paint and Stroke
    351            Paint itemPaint = getItemPaint(row, column);
    352            if (pass == 0) {
    353                g2.setPaint(itemPaint);
    354                g2.fill(left);
    355                g2.fill(right);
    356            }
    357
    358            // add an entity for the item...
    359            if (entities != null) {
    360                GeneralPath gp = new GeneralPath(left);
    361                gp.append(right, false);
    362                entityArea = gp;
    363                addItemEntity(entities, dataset, row, column, entityArea);
    364            }
    365
    366        }
    367
    368        /**
    369         * Calculates the stacked value of the all series up to, but not including
    370         * <code>series</code> for the specified category, <code>category</code>.
    371         * It returns 0.0 if <code>series</code> is the first series, i.e. 0.
    372         *
    373         * @param dataset  the dataset (<code>null</code> not permitted).
    374         * @param series  the series.
    375         * @param category  the category.
    376         *
    377         * @return double returns a cumulative value for all series' values up to
    378         *         but excluding <code>series</code> for Object
    379         *         <code>category</code>.
    380         */
    381        protected double getPreviousHeight(CategoryDataset dataset,
    382                                           int series, int category) {
    383
    384            double result = 0.0;
    385            Number n;
    386            double total = 0.0;
    387            if (this.renderAsPercentages) {
    388                total = DataUtilities.calculateColumnTotal(dataset, category);
    389            }
    390            for (int i = 0; i < series; i++) {
    391                n = dataset.getValue(i, category);
    392                if (n != null) {
    393                    double v = n.doubleValue();
    394                    if (this.renderAsPercentages) {
    395                        v = v / total;
    396                    }
    397                    result += v;
    398                }
    399            }
    400            return result;
    401
    402        }
    403
    404        /**
    405         * Calculates the stacked values (one positive and one negative) of all
    406         * series up to, but not including, <code>series</code> for the specified
    407         * item. It returns [0.0, 0.0] if <code>series</code> is the first series.
    408         *
    409         * @param dataset  the dataset (<code>null</code> not permitted).
    410         * @param series  the series index.
    411         * @param index  the item index.
    412         *
    413         * @return An array containing the cumulative negative and positive values
    414         *     for all series values up to but excluding <code>series</code>
    415         *     for <code>index</code>.
    416         */
    417        protected double[] getStackValues(CategoryDataset dataset,
    418                int series, int index) {
    419            double[] result = new double[2];
    420            for (int i = 0; i < series; i++) {
    421                if (isSeriesVisible(i)) {
    422                    double v = 0.0;
    423                    Number n = dataset.getValue(i, index);
    424                    if (n != null) {
    425                        v = n.doubleValue();
    426                    }
    427                    if (!Double.isNaN(v)) {
    428                        if (v >= 0.0) {
    429                            result[1] += v;
    430                        }
    431                        else {
    432                            result[0] += v;
    433                        }
    434                    }
    435                }
    436            }
    437            return result;
    438        }
    439
    440        /**
    441         * Returns a pair of "stack" values calculated as the mean of the two
    442         * specified stack value pairs.
    443         *
    444         * @param stack1  the first stack pair.
    445         * @param stack2  the second stack pair.
    446         *
    447         * @return A pair of average stack values.
    448         */
    449        private double[] averageStackValues(double[] stack1, double[] stack2) {
    450            double[] result = new double[2];
    451            result[0] = (stack1[0] + stack2[0]) / 2.0;
    452            result[1] = (stack1[1] + stack2[1]) / 2.0;
    453            return result;
    454        }
    455
    456        /**
    457         * Calculates adjusted stack values from the supplied values.  The value is
    458         * the mean of the supplied values, unless either of the supplied values
    459         * is zero, in which case the adjusted value is zero also.
    460         *
    461         * @param stack1  the first stack pair.
    462         * @param stack2  the second stack pair.
    463         *
    464         * @return A pair of average stack values.
    465         */
    466        private double[] adjustedStackValues(double[] stack1, double[] stack2) {
    467            double[] result = new double[2];
    468            if (stack1[0] == 0.0 || stack2[0] == 0.0) {
    469                result[0] = 0.0;
    470            }
    471            else {
    472                result[0] = (stack1[0] + stack2[0]) / 2.0;
    473            }
    474            if (stack1[1] == 0.0 || stack2[1] == 0.0) {
    475                result[1] = 0.0;
    476            }
    477            else {
    478                result[1] = (stack1[1] + stack2[1]) / 2.0;
    479            }
    480            return result;
    481        }
    482
    483        /**
    484         * Checks this instance for equality with an arbitrary object.
    485         *
    486         * @param obj  the object (<code>null</code> not permitted).
    487         *
    488         * @return A boolean.
    489         */
    490        public boolean equals(Object obj) {
    491            if (obj == this) {
    492                return true;
    493            }
    494            if (!(obj instanceof StackedAreaRenderer)) {
    495                return false;
    496            }
    497            StackedAreaRenderer that = (StackedAreaRenderer) obj;
    498            if (this.renderAsPercentages != that.renderAsPercentages) {
    499                return false;
    500            }
    501            return super.equals(obj);
    502        }
    503    }