    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2009, by Object Refinery Limited and Contributors.
    006     *
    007     * Project Info:  http://www.jfree.org/jfreechart/index.html
    008     *
    009     * This library is free software; you can redistribute it and/or modify it
    010     * under the terms of the GNU Lesser General Public License as published by
    011     * the Free Software Foundation; either version 2.1 of the License, or
    012     * (at your option) any later version.
    013     *
    014     * This library is distributed in the hope that it will be useful, but
    015     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
    016     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
    017     * License for more details.
    018     *
    019     * You should have received a copy of the GNU Lesser General Public
    020     * License along with this library; if not, write to the Free Software
    021     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
    022     * USA.
    023     *
    024     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
    025     * in the United States and other countries.]
    026     *
    027     * ------------------------
    028     * StackedAreaRenderer.java
    029     * ------------------------
    030     * (C) Copyright 2002-2009, by Dan Rivett (d.rivett@ukonline.co.uk) and
    031     *                          Contributors.
    032     *
    033     * Original Author:  Dan Rivett (adapted from AreaRenderer);
    034     * Contributor(s):   Jon Iles;
    035     *                   David Gilbert (for Object Refinery Limited);
    036     *                   Christian W. Zuckschwerdt;
    037     *                   Peter Kolb (patch 2511330);
    038     *
    039     * Changes:
    040     * --------
    041     * 20-Sep-2002 : Version 1, contributed by Dan Rivett;
    042     * 24-Oct-2002 : Amendments for changes in CategoryDataset interface and
    043     *               CategoryToolTipGenerator interface (DG);
    044     * 01-Nov-2002 : Added tooltips (DG);
    045     * 06-Nov-2002 : Renamed drawCategoryItem() --> drawItem() and now using axis
    046     *               for category spacing. Renamed StackedAreaCategoryItemRenderer
    047     *               --> StackedAreaRenderer (DG);
    048     * 26-Nov-2002 : Switched CategoryDataset --> TableDataset (DG);
    049     * 26-Nov-2002 : Replaced isStacked() method with getRangeType() method (DG);
    050     * 17-Jan-2003 : Moved plot classes to a separate package (DG);
    051     * 25-Mar-2003 : Implemented Serializable (DG);
    052     * 13-May-2003 : Modified to take into account the plot orientation (DG);
    053     * 30-Jul-2003 : Modified entity constructor (CZ);
    054     * 07-Oct-2003 : Added renderer state (DG);
    055     * 29-Apr-2004 : Added getRangeExtent() override (DG);
    056     * 05-Nov-2004 : Modified drawItem() signature (DG);
    057     * 07-Jan-2005 : Renamed getRangeExtent() --> findRangeBounds() (DG);
    058     * ------------- JFREECHART 1.0.x ---------------------------------------------
    059     * 11-Oct-2006 : Added support for rendering data values as percentages,
    060     *               and added a second pass for drawing item labels (DG);
    061     * 04-Feb-2009 : Fixed support for hidden series, and bug in findRangeBounds()
    062     *               method for null dataset (PK/DG);
    063     * 04-Feb-2009 : Added item label support, and generate entities only in first
    064     *               pass (DG);
    065     * 04-Feb-2009 : Fixed bug for renderAsPercentages == true (DG);
    066     *
    067     */
    068
    069    package org.jfree.chart.renderer.category;
    070
    071    import java.awt.Graphics2D;
    072    import java.awt.Paint;
    073    import java.awt.Shape;
    074    import java.awt.geom.GeneralPath;
    075    import java.awt.geom.Rectangle2D;
    076    import java.io.Serializable;
    077
    078    import org.jfree.chart.axis.CategoryAxis;
    079    import org.jfree.chart.axis.ValueAxis;
    080    import org.jfree.chart.entity.EntityCollection;
    081    import org.jfree.chart.event.RendererChangeEvent;
    082    import org.jfree.chart.plot.CategoryPlot;
    083    import org.jfree.data.DataUtilities;
    084    import org.jfree.data.Range;
    085    import org.jfree.data.category.CategoryDataset;
    086    import org.jfree.data.general.DatasetUtilities;
    087    import org.jfree.ui.RectangleEdge;
    088    import org.jfree.util.PublicCloneable;
    089
    090    /**
    091     * A renderer that draws stacked area charts for a {@link CategoryPlot}.
    092     * The example shown here is generated by the
    093     * <code>StackedAreaChartDemo1.java</code> program included in the
    094     * JFreeChart Demo Collection:
    095     * <br><br>
    096     * <img src="../../../../../images/StackedAreaRendererSample.png"
    097     * alt="StackedAreaRendererSample.png" />
    098     */
    099    public class StackedAreaRenderer extends AreaRenderer
    100            implements Cloneable, PublicCloneable, Serializable {
    101
    102        /** For serialization. */
    103        private static final long serialVersionUID = -3595635038460823663L;
    104
    105        /** A flag that controls whether the areas display values or percentages. */
    106        private boolean renderAsPercentages;
    107
    108        /**
    109         * Creates a new renderer.
    110         */
    111        public StackedAreaRenderer() {
    112            this(false);
    113        }
    114
    115        /**
    116         * Creates a new renderer.
    117         *
    118         * @param renderAsPercentages  a flag that controls whether the data values
    119         *                             are rendered as percentages.
    120         */
    121        public StackedAreaRenderer(boolean renderAsPercentages) {
    122            super();
    123            this.renderAsPercentages = renderAsPercentages;
    124        }
    125
    126        /**
    127         * Returns <code>true</code> if the renderer displays each item value as
    128         * a percentage (so that the stacked areas add to 100%), and
    129         * <code>false</code> otherwise.
    130         *
    131         * @return A boolean.
    132         *
    133         * @since 1.0.3
    134         */
    135        public boolean getRenderAsPercentages() {
    136            return this.renderAsPercentages;
    137        }
    138
    139        /**
    140         * Sets the flag that controls whether the renderer displays each item
    141         * value as a percentage (so that the stacked areas add to 100%), and sends
    142         * a {@link RendererChangeEvent} to all registered listeners.
    143         *
    144         * @param asPercentages  the flag.
    145         *
    146         * @since 1.0.3
    147         */
    148        public void setRenderAsPercentages(boolean asPercentages) {
    149            this.renderAsPercentages = asPercentages;
    150            fireChangeEvent();
    151        }
    152
    153        /**
    154         * Returns the number of passes (<code>2</code>) required by this renderer.
    155         * The first pass is used to draw the areas, the second pass is used to
    156         * draw the item labels (if visible).
    157         *
    158         * @return The number of passes required by the renderer.
    159         */
    160        public int getPassCount() {
    161            return 2;
    162        }
    163
    164        /**
    165         * Returns the range of values the renderer requires to display all the
    166         * items from the specified dataset.
    167         *
    168         * @param dataset  the dataset (<code>null</code> not permitted).
    169         *
    170         * @return The range (or <code>null</code> if the dataset is empty).
    171         */
    172        public Range findRangeBounds(CategoryDataset dataset) {
    173            if (dataset == null) {
    174                return null;
    175            }
    176            if (this.renderAsPercentages) {
    177                return new Range(0.0, 1.0);
    178            }
    179            else {
    180                return DatasetUtilities.findStackedRangeBounds(dataset);
    181            }
    182        }
    183
    184        /**
    185         * Draw a single data item.
    186         *
    187         * @param g2  the graphics device.
    188         * @param state  the renderer state.
    189         * @param dataArea  the data plot area.
    190         * @param plot  the plot.
    191         * @param domainAxis  the domain axis.
    192         * @param rangeAxis  the range axis.
    193         * @param dataset  the data.
    194         * @param row  the row index (zero-based).
    195         * @param column  the column index (zero-based).
    196         * @param pass  the pass index.
    197         */
    198        public void drawItem(Graphics2D g2,
    199                             CategoryItemRendererState state,
    200                             Rectangle2D dataArea,
    201                             CategoryPlot plot,
    202                             CategoryAxis domainAxis,
    203                             ValueAxis rangeAxis,
    204                             CategoryDataset dataset,
    205                             int row,
    206                             int column,
    207                             int pass) {
    208
    209            if (!isSeriesVisible(row)) {
    210                return;
    211            }
    212
    213            // setup for collecting optional entity info...
    214            Shape entityArea = null;
    215            EntityCollection entities = state.getEntityCollection();
    216
    217            double y1 = 0.0;
    218            Number n = dataset.getValue(row, column);
    219            if (n != null) {
    220                y1 = n.doubleValue();
    221                if (this.renderAsPercentages) {
    222                    double total = DataUtilities.calculateColumnTotal(dataset,
    223                            column, state.getVisibleSeriesArray());
    224                    y1 = y1 / total;
    225                }
    226            }
    227            double[] stack1 = getStackValues(dataset, row, column,
    228                    state.getVisibleSeriesArray());
    229
    230
    231            // leave the y values (y1, y0) untranslated as it is going to be be
    232            // stacked up later by previous series values, after this it will be
    233            // translated.
    234            double xx1 = domainAxis.getCategoryMiddle(column, getColumnCount(),
    235                    dataArea, plot.getDomainAxisEdge());
    236
    237
    238            // get the previous point and the next point so we can calculate a
    239            // "hot spot" for the area (used by the chart entity)...
    240            double y0 = 0.0;
    241            n = dataset.getValue(row, Math.max(column - 1, 0));
    242            if (n != null) {
    243                y0 = n.doubleValue();
    244                if (this.renderAsPercentages) {
    245                    double total = DataUtilities.calculateColumnTotal(dataset,
    246                            Math.max(column - 1, 0), state.getVisibleSeriesArray());
    247                    y0 = y0 / total;
    248                }
    249            }
    250            double[] stack0 = getStackValues(dataset, row, Math.max(column - 1, 0),
    251                    state.getVisibleSeriesArray());
    252
    253            // FIXME: calculate xx0
    254            double xx0 = domainAxis.getCategoryStart(column, getColumnCount(),
    255                    dataArea, plot.getDomainAxisEdge());
    256
    257            int itemCount = dataset.getColumnCount();
    258            double y2 = 0.0;
    259            n = dataset.getValue(row, Math.min(column + 1, itemCount - 1));
    260            if (n != null) {
    261                y2 = n.doubleValue();
    262                if (this.renderAsPercentages) {
    263                    double total = DataUtilities.calculateColumnTotal(dataset,
    264                            Math.min(column + 1, itemCount - 1),
    265                            state.getVisibleSeriesArray());
    266                    y2 = y2 / total;
    267                }
    268            }
    269            double[] stack2 = getStackValues(dataset, row, Math.min(column + 1,
    270                    itemCount - 1), state.getVisibleSeriesArray());
    271
    272            double xx2 = domainAxis.getCategoryEnd(column, getColumnCount(),
    273                    dataArea, plot.getDomainAxisEdge());
    274
    275            // FIXME: calculate xxLeft and xxRight
    276            double xxLeft = xx0;
    277            double xxRight = xx2;
    278
    279            double[] stackLeft = averageStackValues(stack0, stack1);
    280            double[] stackRight = averageStackValues(stack1, stack2);
    281            double[] adjStackLeft = adjustedStackValues(stack0, stack1);
    282            double[] adjStackRight = adjustedStackValues(stack1, stack2);
    283
    284            float transY1;
    285
    286            RectangleEdge edge1 = plot.getRangeAxisEdge();
    287
    288            GeneralPath left = new GeneralPath();
    289            GeneralPath right = new GeneralPath();
    290            if (y1 >= 0.0) {  // handle positive value
    291                transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1], dataArea,
    292                        edge1);
    293                float transStack1 = (float) rangeAxis.valueToJava2D(stack1[1],
    294                        dataArea, edge1);
    295                float transStackLeft = (float) rangeAxis.valueToJava2D(
    296                        adjStackLeft[1], dataArea, edge1);
    297
    298                // LEFT POLYGON
    299                if (y0 >= 0.0) {
    300                    double yleft = (y0 + y1) / 2.0 + stackLeft[1];
    301                    float transYLeft
    302                        = (float) rangeAxis.valueToJava2D(yleft, dataArea, edge1);
    303                    left.moveTo((float) xx1, transY1);
    304                    left.lineTo((float) xx1, transStack1);
    305                    left.lineTo((float) xxLeft, transStackLeft);
    306                    left.lineTo((float) xxLeft, transYLeft);
    307                    left.closePath();
    308                }
    309                else {
    310                    left.moveTo((float) xx1, transStack1);
    311                    left.lineTo((float) xx1, transY1);
    312                    left.lineTo((float) xxLeft, transStackLeft);
    313                    left.closePath();
    314                }
    315
    316                float transStackRight = (float) rangeAxis.valueToJava2D(
    317                        adjStackRight[1], dataArea, edge1);
    318                // RIGHT POLYGON
    319                if (y2 >= 0.0) {
    320                    double yright = (y1 + y2) / 2.0 + stackRight[1];
    321                    float transYRight
    322                        = (float) rangeAxis.valueToJava2D(yright, dataArea, edge1);
    323                    right.moveTo((float) xx1, transStack1);
    324                    right.lineTo((float) xx1, transY1);
    325                    right.lineTo((float) xxRight, transYRight);
    326                    right.lineTo((float) xxRight, transStackRight);
    327                    right.closePath();
    328                }
    329                else {
    330                    right.moveTo((float) xx1, transStack1);
    331                    right.lineTo((float) xx1, transY1);
    332                    right.lineTo((float) xxRight, transStackRight);
    333                    right.closePath();
    334                }
    335            }
    336            else {  // handle negative value
    337                transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0], dataArea,
    338                        edge1);
    339                float transStack1 = (float) rangeAxis.valueToJava2D(stack1[0],
    340                        dataArea, edge1);
    341                float transStackLeft = (float) rangeAxis.valueToJava2D(
    342                        adjStackLeft[0], dataArea, edge1);
    343
    344                // LEFT POLYGON
    345                if (y0 >= 0.0) {
    346                    left.moveTo((float) xx1, transStack1);
    347                    left.lineTo((float) xx1, transY1);
    348                    left.lineTo((float) xxLeft, transStackLeft);
    349                    left.clone();
    350                }
    351                else {
    352                    double yleft = (y0 + y1) / 2.0 + stackLeft[0];
    353                    float transYLeft = (float) rangeAxis.valueToJava2D(yleft,
    354                            dataArea, edge1);
    355                    left.moveTo((float) xx1, transY1);
    356                    left.lineTo((float) xx1, transStack1);
    357                    left.lineTo((float) xxLeft, transStackLeft);
    358                    left.lineTo((float) xxLeft, transYLeft);
    359                    left.closePath();
    360                }
    361                float transStackRight = (float) rangeAxis.valueToJava2D(
    362                        adjStackRight[0], dataArea, edge1);
    363
    364                // RIGHT POLYGON
    365                if (y2 >= 0.0) {
    366                    right.moveTo((float) xx1, transStack1);
    367                    right.lineTo((float) xx1, transY1);
    368                    right.lineTo((float) xxRight, transStackRight);
    369                    right.closePath();
    370                }
    371                else {
    372                    double yright = (y1 + y2) / 2.0 + stackRight[0];
    373                    float transYRight = (float) rangeAxis.valueToJava2D(yright,
    374                            dataArea, edge1);
    375                    right.moveTo((float) xx1, transStack1);
    376                    right.lineTo((float) xx1, transY1);
    377                    right.lineTo((float) xxRight, transYRight);
    378                    right.lineTo((float) xxRight, transStackRight);
    379                    right.closePath();
    380                }
    381            }
    382
    383            if (pass == 0) {
    384                Paint itemPaint = getItemPaint(row, column);
    385                g2.setPaint(itemPaint);
    386                g2.fill(left);
    387                g2.fill(right);
    388
    389                // add an entity for the item...
    390                if (entities != null) {
    391                    GeneralPath gp = new GeneralPath(left);
    392                    gp.append(right, false);
    393                    entityArea = gp;
    394                    addItemEntity(entities, dataset, row, column, entityArea);
    395                }
    396            }
    397            else if (pass == 1) {
    398                drawItemLabel(g2, plot.getOrientation(), dataset, row, column,
    399                        xx1, transY1, y1 < 0.0);
    400            }
    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, int[] validRows) {
    419            double[] result = new double[2];
    420            double total = 0.0;
    421            if (this.renderAsPercentages) {
    422                total = DataUtilities.calculateColumnTotal(dataset, index,
    423                        validRows);
    424            }
    425            for (int i = 0; i < series; i++) {
    426                if (isSeriesVisible(i)) {
    427                    double v = 0.0;
    428                    Number n = dataset.getValue(i, index);
    429                    if (n != null) {
    430                        v = n.doubleValue();
    431                        if (this.renderAsPercentages) {
    432                            v = v / total;
    433                        }
    434                    }
    435                    if (!Double.isNaN(v)) {
    436                        if (v >= 0.0) {
    437                            result[1] += v;
    438                        }
    439                        else {
    440                            result[0] += v;
    441                        }
    442                    }
    443                }
    444            }
    445            return result;
    446        }
    447
    448        /**
    449         * Returns a pair of "stack" values calculated as the mean of the two
    450         * specified stack value pairs.
    451         *
    452         * @param stack1  the first stack pair.
    453         * @param stack2  the second stack pair.
    454         *
    455         * @return A pair of average stack values.
    456         */
    457        private double[] averageStackValues(double[] stack1, double[] stack2) {
    458            double[] result = new double[2];
    459            result[0] = (stack1[0] + stack2[0]) / 2.0;
    460            result[1] = (stack1[1] + stack2[1]) / 2.0;
    461            return result;
    462        }
    463
    464        /**
    465         * Calculates adjusted stack values from the supplied values.  The value is
    466         * the mean of the supplied values, unless either of the supplied values
    467         * is zero, in which case the adjusted value is zero also.
    468         *
    469         * @param stack1  the first stack pair.
    470         * @param stack2  the second stack pair.
    471         *
    472         * @return A pair of average stack values.
    473         */
    474        private double[] adjustedStackValues(double[] stack1, double[] stack2) {
    475            double[] result = new double[2];
    476            if (stack1[0] == 0.0 || stack2[0] == 0.0) {
    477                result[0] = 0.0;
    478            }
    479            else {
    480                result[0] = (stack1[0] + stack2[0]) / 2.0;
    481            }
    482            if (stack1[1] == 0.0 || stack2[1] == 0.0) {
    483                result[1] = 0.0;
    484            }
    485            else {
    486                result[1] = (stack1[1] + stack2[1]) / 2.0;
    487            }
    488            return result;
    489        }
    490
    491        /**
    492         * Checks this instance for equality with an arbitrary object.
    493         *
    494         * @param obj  the object (<code>null</code> not permitted).
    495         *
    496         * @return A boolean.
    497         */
    498        public boolean equals(Object obj) {
    499            if (obj == this) {
    500                return true;
    501            }
    502            if (!(obj instanceof StackedAreaRenderer)) {
    503                return false;
    504            }
    505            StackedAreaRenderer that = (StackedAreaRenderer) obj;
    506            if (this.renderAsPercentages != that.renderAsPercentages) {
    507                return false;
    508            }
    509            return super.equals(obj);
    510        }
    511
    512        /**
    513         * Calculates the stacked value of the all series up to, but not including
    514         * <code>series</code> for the specified category, <code>category</code>.
    515         * It returns 0.0 if <code>series</code> is the first series, i.e. 0.
    516         *
    517         * @param dataset  the dataset (<code>null</code> not permitted).
    518         * @param series  the series.
    519         * @param category  the category.
    520         *
    521         * @return double returns a cumulative value for all series' values up to
    522         *         but excluding <code>series</code> for Object
    523         *         <code>category</code>.
    524         *
    525         * @deprecated As of 1.0.13, as the method is never used internally.
    526         */
    527        protected double getPreviousHeight(CategoryDataset dataset,
    528                int series, int category) {
    529
    530            double result = 0.0;
    531            Number n;
    532            double total = 0.0;
    533            if (this.renderAsPercentages) {
    534                total = DataUtilities.calculateColumnTotal(dataset, category);
    535            }
    536            for (int i = 0; i < series; i++) {
    537                n = dataset.getValue(i, category);
    538                if (n != null) {
    539                    double v = n.doubleValue();
    540                    if (this.renderAsPercentages) {
    541                        v = v / total;
    542                    }
    543                    result += v;
    544                }
    545            }
    546            return result;
    547
    548        }
    549
    550    }