    001/* ===========================================================
    002 * JFreeChart : a free chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C) Copyright 2000-2011, by Object Refinery Limited and Contributors.
    006 *
    007 * Project Info:  http://www.jfree.org/jfreechart/index.html
    008 *
    009 * This library is free software; you can redistribute it and/or modify it
    010 * under the terms of the GNU Lesser General Public License as published by
    011 * the Free Software Foundation; either version 2.1 of the License, or
    012 * (at your option) any later version.
    013 *
    014 * This library is distributed in the hope that it will be useful, but
    015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
    016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
    017 * License for more details.
    018 *
    019 * You should have received a copy of the GNU Lesser General Public
    020 * License along with this library; if not, write to the Free Software
    021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
    022 * USA.
    023 *
    024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
    025 * Other names may be trademarks of their respective owners.]
    026 *
    027 * ------------------------
    028 * 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
    069package org.jfree.chart.renderer.category;
    070
    071import java.awt.Graphics2D;
    072import java.awt.Paint;
    073import java.awt.Shape;
    074import java.awt.geom.GeneralPath;
    075import java.awt.geom.Rectangle2D;
    076import java.io.Serializable;
    077
    078import org.jfree.chart.axis.CategoryAxis;
    079import org.jfree.chart.axis.ValueAxis;
    080import org.jfree.chart.entity.EntityCollection;
    081import org.jfree.chart.event.RendererChangeEvent;
    082import org.jfree.chart.plot.CategoryPlot;
    083import org.jfree.data.DataUtilities;
    084import org.jfree.data.Range;
    085import org.jfree.data.category.CategoryDataset;
    086import org.jfree.data.general.DatasetUtilities;
    087import org.jfree.ui.RectangleEdge;
    088import 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 */
    099public 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}