    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
    006     *
    007     * Project Info:  http://www.jfree.org/jfreechart/index.html
    008     *
    009     * This library is free software; you can redistribute it and/or modify it
    010     * under the terms of the GNU Lesser General Public License as published by
    011     * the Free Software Foundation; either version 2.1 of the License, or
    012     * (at your option) any later version.
    013     *
    014     * This library is distributed in the hope that it will be useful, but
    015     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
    016     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
    017     * License for more details.
    018     *
    019     * You should have received a copy of the GNU Lesser General Public
    020     * License along with this library; if not, write to the Free Software
    021     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
    022     * USA.
    023     *
    024     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
    025     * in the United States and other countries.]
    026     *
    027     * ------------------------
    028     * StackedAreaRenderer.java
    029     * ------------------------
    030     * (C) Copyright 2002-2007, 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,
    090                                                Serializable {
    091
    092        /** For serialization. */
    093        private static final long serialVersionUID = -3595635038460823663L;
    094
    095        /** A flag that controls whether the areas display values or percentages. */
    096        private boolean renderAsPercentages;
    097
    098        /**
    099         * Creates a new renderer.
    100         */
    101        public StackedAreaRenderer() {
    102            this(false);
    103        }
    104
    105        /**
    106         * Creates a new renderer.
    107         *
    108         * @param renderAsPercentages  a flag that controls whether the data values
    109         *                             are rendered as percentages.
    110         */
    111        public StackedAreaRenderer(boolean renderAsPercentages) {
    112            super();
    113            this.renderAsPercentages = renderAsPercentages;
    114        }
    115
    116        /**
    117         * Returns <code>true</code> if the renderer displays each item value as
    118         * a percentage (so that the stacked areas add to 100%), and
    119         * <code>false</code> otherwise.
    120         *
    121         * @return A boolean.
    122         *
    123         * @since 1.0.3
    124         */
    125        public boolean getRenderAsPercentages() {
    126            return this.renderAsPercentages;
    127        }
    128
    129        /**
    130         * Sets the flag that controls whether the renderer displays each item
    131         * value as a percentage (so that the stacked areas add to 100%), and sends
    132         * a {@link RendererChangeEvent} to all registered listeners.
    133         *
    134         * @param asPercentages  the flag.
    135         *
    136         * @since 1.0.3
    137         */
    138        public void setRenderAsPercentages(boolean asPercentages) {
    139            this.renderAsPercentages = asPercentages;
    140            fireChangeEvent();
    141        }
    142
    143        /**
    144         * Returns the number of passes (<code>2</code>) required by this renderer.
    145         * The first pass is used to draw the bars, the second pass is used to
    146         * draw the item labels (if visible).
    147         *
    148         * @return The number of passes required by the renderer.
    149         */
    150        public int getPassCount() {
    151            return 2;
    152        }
    153
    154        /**
    155         * Returns the range of values the renderer requires to display all the
    156         * items from the specified dataset.
    157         *
    158         * @param dataset  the dataset (<code>null</code> not permitted).
    159         *
    160         * @return The range (or <code>null</code> if the dataset is empty).
    161         */
    162        public Range findRangeBounds(CategoryDataset dataset) {
    163            if (this.renderAsPercentages) {
    164                return new Range(0.0, 1.0);
    165            }
    166            else {
    167                return DatasetUtilities.findStackedRangeBounds(dataset);
    168            }
    169        }
    170
    171        /**
    172         * Draw a single data item.
    173         *
    174         * @param g2  the graphics device.
    175         * @param state  the renderer state.
    176         * @param dataArea  the data plot area.
    177         * @param plot  the plot.
    178         * @param domainAxis  the domain axis.
    179         * @param rangeAxis  the range axis.
    180         * @param dataset  the data.
    181         * @param row  the row index (zero-based).
    182         * @param column  the column index (zero-based).
    183         * @param pass  the pass index.
    184         */
    185        public void drawItem(Graphics2D g2,
    186                             CategoryItemRendererState state,
    187                             Rectangle2D dataArea,
    188                             CategoryPlot plot,
    189                             CategoryAxis domainAxis,
    190                             ValueAxis rangeAxis,
    191                             CategoryDataset dataset,
    192                             int row,
    193                             int column,
    194                             int pass) {
    195
    196            // setup for collecting optional entity info...
    197            Shape entityArea = null;
    198            EntityCollection entities = state.getEntityCollection();
    199
    200            double y1 = 0.0;
    201            Number n = dataset.getValue(row, column);
    202            if (n != null) {
    203                y1 = n.doubleValue();
    204            }
    205            double[] stack1 = getStackValues(dataset, row, column);
    206
    207
    208            // leave the y values (y1, y0) untranslated as it is going to be be
    209            // stacked up later by previous series values, after this it will be
    210            // translated.
    211            double xx1 = domainAxis.getCategoryMiddle(column, getColumnCount(),
    212                    dataArea, plot.getDomainAxisEdge());
    213
    214
    215            // get the previous point and the next point so we can calculate a
    216            // "hot spot" for the area (used by the chart entity)...
    217            double y0 = 0.0;
    218            n = dataset.getValue(row, Math.max(column - 1, 0));
    219            if (n != null) {
    220                y0 = n.doubleValue();
    221            }
    222            double[] stack0 = getStackValues(dataset, row, Math.max(column - 1, 0));
    223
    224            // FIXME: calculate xx0
    225            double xx0 = domainAxis.getCategoryStart(column, getColumnCount(),
    226                    dataArea, plot.getDomainAxisEdge());
    227
    228            int itemCount = dataset.getColumnCount();
    229            double y2 = 0.0;
    230            n = dataset.getValue(row, Math.min(column + 1, itemCount - 1));
    231            if (n != null) {
    232                y2 = n.doubleValue();
    233            }
    234            double[] stack2 = getStackValues(dataset, row, Math.min(column + 1,
    235                    itemCount - 1));
    236
    237            double xx2 = domainAxis.getCategoryEnd(column, getColumnCount(),
    238                    dataArea, plot.getDomainAxisEdge());
    239
    240            // FIXME: calculate xxLeft and xxRight
    241            double xxLeft = xx0;
    242            double xxRight = xx2;
    243
    244            double[] stackLeft = averageStackValues(stack0, stack1);
    245            double[] stackRight = averageStackValues(stack1, stack2);
    246            double[] adjStackLeft = adjustedStackValues(stack0, stack1);
    247            double[] adjStackRight = adjustedStackValues(stack1, stack2);
    248
    249            float transY1;
    250
    251            RectangleEdge edge1 = plot.getRangeAxisEdge();
    252
    253            GeneralPath left = new GeneralPath();
    254            GeneralPath right = new GeneralPath();
    255            if (y1 >= 0.0) {  // handle positive value
    256                transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1], dataArea,
    257                        edge1);
    258                float transStack1 = (float) rangeAxis.valueToJava2D(stack1[1],
    259                        dataArea, edge1);
    260                float transStackLeft = (float) rangeAxis.valueToJava2D(
    261                        adjStackLeft[1], dataArea, edge1);
    262
    263                // LEFT POLYGON
    264                if (y0 >= 0.0) {
    265                    double yleft = (y0 + y1) / 2.0 + stackLeft[1];
    266                    float transYLeft
    267                        = (float) rangeAxis.valueToJava2D(yleft, dataArea, edge1);
    268                    left.moveTo((float) xx1, transY1);
    269                    left.lineTo((float) xx1, transStack1);
    270                    left.lineTo((float) xxLeft, transStackLeft);
    271                    left.lineTo((float) xxLeft, transYLeft);
    272                    left.closePath();
    273                }
    274                else {
    275                    left.moveTo((float) xx1, transStack1);
    276                    left.lineTo((float) xx1, transY1);
    277                    left.lineTo((float) xxLeft, transStackLeft);
    278                    left.closePath();
    279                }
    280
    281                float transStackRight = (float) rangeAxis.valueToJava2D(
    282                        adjStackRight[1], dataArea, edge1);
    283                // RIGHT POLYGON
    284                if (y2 >= 0.0) {
    285                    double yright = (y1 + y2) / 2.0 + stackRight[1];
    286                    float transYRight
    287                        = (float) rangeAxis.valueToJava2D(yright, dataArea, edge1);
    288                    right.moveTo((float) xx1, transStack1);
    289                    right.lineTo((float) xx1, transY1);
    290                    right.lineTo((float) xxRight, transYRight);
    291                    right.lineTo((float) xxRight, transStackRight);
    292                    right.closePath();
    293                }
    294                else {
    295                    right.moveTo((float) xx1, transStack1);
    296                    right.lineTo((float) xx1, transY1);
    297                    right.lineTo((float) xxRight, transStackRight);
    298                    right.closePath();
    299                }
    300            }
    301            else {  // handle negative value
    302                transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0], dataArea,
    303                        edge1);
    304                float transStack1 = (float) rangeAxis.valueToJava2D(stack1[0],
    305                        dataArea, edge1);
    306                float transStackLeft = (float) rangeAxis.valueToJava2D(
    307                        adjStackLeft[0], dataArea, edge1);
    308
    309                // LEFT POLYGON
    310                if (y0 >= 0.0) {
    311                    left.moveTo((float) xx1, transStack1);
    312                    left.lineTo((float) xx1, transY1);
    313                    left.lineTo((float) xxLeft, transStackLeft);
    314                    left.clone();
    315                }
    316                else {
    317                    double yleft = (y0 + y1) / 2.0 + stackLeft[0];
    318                    float transYLeft = (float) rangeAxis.valueToJava2D(yleft,
    319                            dataArea, edge1);
    320                    left.moveTo((float) xx1, transY1);
    321                    left.lineTo((float) xx1, transStack1);
    322                    left.lineTo((float) xxLeft, transStackLeft);
    323                    left.lineTo((float) xxLeft, transYLeft);
    324                    left.closePath();
    325                }
    326                float transStackRight = (float) rangeAxis.valueToJava2D(
    327                        adjStackRight[0], dataArea, edge1);
    328
    329                // RIGHT POLYGON
    330                if (y2 >= 0.0) {
    331                    right.moveTo((float) xx1, transStack1);
    332                    right.lineTo((float) xx1, transY1);
    333                    right.lineTo((float) xxRight, transStackRight);
    334                    right.closePath();
    335                }
    336                else {
    337                    double yright = (y1 + y2) / 2.0 + stackRight[0];
    338                    float transYRight = (float) rangeAxis.valueToJava2D(yright,
    339                            dataArea, edge1);
    340                    right.moveTo((float) xx1, transStack1);
    341                    right.lineTo((float) xx1, transY1);
    342                    right.lineTo((float) xxRight, transYRight);
    343                    right.lineTo((float) xxRight, transStackRight);
    344                    right.closePath();
    345                }
    346            }
    347
    348            g2.setPaint(getItemPaint(row, column));
    349            g2.setStroke(getItemStroke(row, column));
    350
    351            //  Get series Paint and Stroke
    352            Paint itemPaint = getItemPaint(row, column);
    353            if (pass == 0) {
    354                g2.setPaint(itemPaint);
    355                g2.fill(left);
    356                g2.fill(right);
    357            }
    358
    359            // add an entity for the item...
    360            if (entities != null) {
    361                GeneralPath gp = new GeneralPath(left);
    362                gp.append(right, false);
    363                entityArea = gp;
    364                addItemEntity(entities, dataset, row, column, entityArea);
    365            }
    366
    367        }
    368
    369        /**
    370         * Calculates the stacked value of the all series up to, but not including
    371         * <code>series</code> for the specified category, <code>category</code>.
    372         * It returns 0.0 if <code>series</code> is the first series, i.e. 0.
    373         *
    374         * @param dataset  the dataset (<code>null</code> not permitted).
    375         * @param series  the series.
    376         * @param category  the category.
    377         *
    378         * @return double returns a cumulative value for all series' values up to
    379         *         but excluding <code>series</code> for Object
    380         *         <code>category</code>.
    381         */
    382        protected double getPreviousHeight(CategoryDataset dataset,
    383                                           int series, int category) {
    384
    385            double result = 0.0;
    386            Number n;
    387            double total = 0.0;
    388            if (this.renderAsPercentages) {
    389                total = DataUtilities.calculateColumnTotal(dataset, category);
    390            }
    391            for (int i = 0; i < series; i++) {
    392                n = dataset.getValue(i, category);
    393                if (n != null) {
    394                    double v = n.doubleValue();
    395                    if (this.renderAsPercentages) {
    396                        v = v / total;
    397                    }
    398                    result += v;
    399                }
    400            }
    401            return result;
    402
    403        }
    404
    405        /**
    406         * Calculates the stacked values (one positive and one negative) of all
    407         * series up to, but not including, <code>series</code> for the specified
    408         * item. It returns [0.0, 0.0] if <code>series</code> is the first series.
    409         *
    410         * @param dataset  the dataset (<code>null</code> not permitted).
    411         * @param series  the series index.
    412         * @param index  the item index.
    413         *
    414         * @return An array containing the cumulative negative and positive values
    415         *     for all series values up to but excluding <code>series</code>
    416         *     for <code>index</code>.
    417         */
    418        protected double[] getStackValues(CategoryDataset dataset,
    419                int series, int index) {
    420            double[] result = new double[2];
    421            for (int i = 0; i < series; i++) {
    422                if (isSeriesVisible(i)) {
    423                    double v = 0.0;
    424                    Number n = dataset.getValue(i, index);
    425                    if (n != null) {
    426                        v = n.doubleValue();
    427                    }
    428                    if (!Double.isNaN(v)) {
    429                        if (v >= 0.0) {
    430                            result[1] += v;
    431                        }
    432                        else {
    433                            result[0] += v;
    434                        }
    435                    }
    436                }
    437            }
    438            return result;
    439        }
    440
    441        /**
    442         * Returns a pair of "stack" values calculated as the mean of the two
    443         * specified stack value pairs.
    444         *
    445         * @param stack1  the first stack pair.
    446         * @param stack2  the second stack pair.
    447         *
    448         * @return A pair of average stack values.
    449         */
    450        private double[] averageStackValues(double[] stack1, double[] stack2) {
    451            double[] result = new double[2];
    452            result[0] = (stack1[0] + stack2[0]) / 2.0;
    453            result[1] = (stack1[1] + stack2[1]) / 2.0;
    454            return result;
    455        }
    456
    457        /**
    458         * Calculates adjusted stack values from the supplied values.  The value is
    459         * the mean of the supplied values, unless either of the supplied values
    460         * is zero, in which case the adjusted value is zero also.
    461         *
    462         * @param stack1  the first stack pair.
    463         * @param stack2  the second stack pair.
    464         *
    465         * @return A pair of average stack values.
    466         */
    467        private double[] adjustedStackValues(double[] stack1, double[] stack2) {
    468            double[] result = new double[2];
    469            if (stack1[0] == 0.0 || stack2[0] == 0.0) {
    470                result[0] = 0.0;
    471            }
    472            else {
    473                result[0] = (stack1[0] + stack2[0]) / 2.0;
    474            }
    475            if (stack1[1] == 0.0 || stack2[1] == 0.0) {
    476                result[1] = 0.0;
    477            }
    478            else {
    479                result[1] = (stack1[1] + stack2[1]) / 2.0;
    480            }
    481            return result;
    482        }
    483
    484        /**
    485         * Checks this instance for equality with an arbitrary object.
    486         *
    487         * @param obj  the object (<code>null</code> not permitted).
    488         *
    489         * @return A boolean.
    490         */
    491        public boolean equals(Object obj) {
    492            if (obj == this) {
    493                return true;
    494            }
    495            if (!(obj instanceof StackedAreaRenderer)) {
    496                return false;
    497            }
    498            StackedAreaRenderer that = (StackedAreaRenderer) obj;
    499            if (this.renderAsPercentages != that.renderAsPercentages) {
    500                return false;
    501            }
    502            return super.equals(obj);
    503        }
    504    }