    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2005, 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-2006, 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     * $Id: StackedAreaRenderer.java,v 1.6.2.3 2006/10/11 16:25:50 mungady Exp $
    039     *
    040     * Changes:
    041     * --------
    042     * 20-Sep-2002 : Version 1, contributed by Dan Rivett;
    043     * 24-Oct-2002 : Amendments for changes in CategoryDataset interface and
    044     *               CategoryToolTipGenerator interface (DG);
    045     * 01-Nov-2002 : Added tooltips (DG);
    046     * 06-Nov-2002 : Renamed drawCategoryItem() --> drawItem() and now using axis
    047     *               for category spacing. Renamed StackedAreaCategoryItemRenderer
    048     *               --> StackedAreaRenderer (DG);
    049     * 26-Nov-2002 : Switched CategoryDataset --> TableDataset (DG);
    050     * 26-Nov-2002 : Replaced isStacked() method with getRangeType() method (DG);
    051     * 17-Jan-2003 : Moved plot classes to a separate package (DG);
    052     * 25-Mar-2003 : Implemented Serializable (DG);
    053     * 13-May-2003 : Modified to take into account the plot orientation (DG);
    054     * 30-Jul-2003 : Modified entity constructor (CZ);
    055     * 07-Oct-2003 : Added renderer state (DG);
    056     * 29-Apr-2004 : Added getRangeExtent() override (DG);
    057     * 05-Nov-2004 : Modified drawItem() signature (DG);
    058     * 07-Jan-2005 : Renamed getRangeExtent() --> findRangeBounds() (DG);
    059     * ------------- JFREECHART 1.0.x ---------------------------------------------
    060     * 11-Oct-2006 : Added support for rendering data values as percentages,
    061     *               and added a second pass for drawing item labels (DG);
    062     *
    063     */
    064
    065    package org.jfree.chart.renderer.category;
    066
    067    import java.awt.Graphics2D;
    068    import java.awt.Polygon;
    069    import java.awt.Shape;
    070    import java.awt.geom.Rectangle2D;
    071    import java.io.Serializable;
    072
    073    import org.jfree.chart.axis.CategoryAxis;
    074    import org.jfree.chart.axis.ValueAxis;
    075    import org.jfree.chart.entity.EntityCollection;
    076    import org.jfree.chart.event.RendererChangeEvent;
    077    import org.jfree.chart.plot.CategoryPlot;
    078    import org.jfree.chart.plot.PlotOrientation;
    079    import org.jfree.data.DataUtilities;
    080    import org.jfree.data.Range;
    081    import org.jfree.data.category.CategoryDataset;
    082    import org.jfree.data.general.DatasetUtilities;
    083    import org.jfree.ui.RectangleEdge;
    084    import org.jfree.util.PublicCloneable;
    085
    086    /**
    087     * A renderer that draws stacked area charts for a
    088     * {@link org.jfree.chart.plot.CategoryPlot}.
    089     */
    090    public class StackedAreaRenderer extends AreaRenderer
    091                                     implements Cloneable, PublicCloneable,
    092                                                Serializable {
    093
    094        /** For serialization. */
    095        private static final long serialVersionUID = -3595635038460823663L;
    096
    097        /** A flag that controls whether the areas display values or percentages. */
    098        private boolean renderAsPercentages;
    099
    100        /**
    101         * Creates a new renderer.
    102         */
    103        public StackedAreaRenderer() {
    104            this(false);
    105        }
    106
    107        /**
    108         * Creates a new renderer.
    109         *
    110         * @param renderAsPercentages  a flag that controls whether the data values
    111         *                             are rendered as percentages.
    112         */
    113        public StackedAreaRenderer(boolean renderAsPercentages) {
    114            super();
    115            this.renderAsPercentages = renderAsPercentages;
    116        }
    117
    118        /**
    119         * Returns <code>true</code> if the renderer displays each item value as
    120         * a percentage (so that the stacked areas add to 100%), and
    121         * <code>false</code> otherwise.
    122         *
    123         * @return A boolean.
    124         *
    125         * @since 1.0.3
    126         */
    127        public boolean getRenderAsPercentages() {
    128            return this.renderAsPercentages;
    129        }
    130
    131        /**
    132         * Sets the flag that controls whether the renderer displays each item
    133         * value as a percentage (so that the stacked areas add to 100%), and sends
    134         * a {@link RendererChangeEvent} to all registered listeners.
    135         *
    136         * @param asPercentages  the flag.
    137         *
    138         * @since 1.0.3
    139         */
    140        public void setRenderAsPercentages(boolean asPercentages) {
    141            this.renderAsPercentages = asPercentages;
    142            notifyListeners(new RendererChangeEvent(this));
    143        }
    144
    145        /**
    146         * Returns the number of passes (<code>2</code>) required by this renderer.
    147         * The first pass is used to draw the bars, the second pass is used to
    148         * draw the item labels (if visible).
    149         *
    150         * @return The number of passes required by the renderer.
    151         */
    152        public int getPassCount() {
    153            return 2;
    154        }
    155
    156        /**
    157         * Returns the range of values the renderer requires to display all the
    158         * items from the specified dataset.
    159         *
    160         * @param dataset  the dataset (<code>null</code> not permitted).
    161         *
    162         * @return The range (or <code>null</code> if the dataset is empty).
    163         */
    164        public Range findRangeBounds(CategoryDataset dataset) {
    165            if (this.renderAsPercentages) {
    166                return new Range(0.0, 1.0);
    167            }
    168            else {
    169                return DatasetUtilities.findStackedRangeBounds(dataset);
    170            }
    171        }
    172
    173        /**
    174         * Draw a single data item.
    175         *
    176         * @param g2  the graphics device.
    177         * @param state  the renderer state.
    178         * @param dataArea  the data plot area.
    179         * @param plot  the plot.
    180         * @param domainAxis  the domain axis.
    181         * @param rangeAxis  the range axis.
    182         * @param dataset  the data.
    183         * @param row  the row index (zero-based).
    184         * @param column  the column index (zero-based).
    185         * @param pass  the pass index.
    186         */
    187        public void drawItem(Graphics2D g2,
    188                             CategoryItemRendererState state,
    189                             Rectangle2D dataArea,
    190                             CategoryPlot plot,
    191                             CategoryAxis domainAxis,
    192                             ValueAxis rangeAxis,
    193                             CategoryDataset dataset,
    194                             int row,
    195                             int column,
    196                             int pass) {
    197
    198            // plot non-null values...
    199            Number dataValue = dataset.getValue(row, column);
    200            if (dataValue == null) {
    201                return;
    202            }
    203
    204            double value = dataValue.doubleValue();
    205            double total = 0.0;  // only needed if calculating percentages
    206            if (this.renderAsPercentages) {
    207                total = DataUtilities.calculateColumnTotal(dataset, column);
    208                value = value / total;
    209            }
    210
    211            // leave the y values (y1, y0) untranslated as it is going to be be
    212            // stacked up later by previous series values, after this it will be
    213            // translated.
    214            double xx1 = domainAxis.getCategoryMiddle(column, getColumnCount(),
    215                    dataArea, plot.getDomainAxisEdge());
    216
    217            double previousHeightx1 = getPreviousHeight(dataset, row, column);
    218            double y1 = value + previousHeightx1;
    219            RectangleEdge location = plot.getRangeAxisEdge();
    220            double yy1 = rangeAxis.valueToJava2D(y1, dataArea, location);
    221
    222            g2.setPaint(getItemPaint(row, column));
    223            g2.setStroke(getItemStroke(row, column));
    224
    225            // in column zero, the only job to do is draw any visible item labels
    226            // and this is done in the second pass...
    227            if (column == 0) {
    228                if (pass == 1) {
    229                    // draw item labels, if visible
    230                    if (isItemLabelVisible(row, column)) {
    231                        drawItemLabel(g2, plot.getOrientation(), dataset, row, column,
    232                                xx1, yy1, (y1 < 0.0));
    233                    }
    234                }
    235            }
    236            else {
    237                Number previousValue = dataset.getValue(row, column - 1);
    238                if (previousValue != null) {
    239
    240                    double xx0 = domainAxis.getCategoryMiddle(column - 1,
    241                            getColumnCount(), dataArea, plot.getDomainAxisEdge());
    242                    double y0 = previousValue.doubleValue();
    243                    if (this.renderAsPercentages) {
    244                        total = DataUtilities.calculateColumnTotal(dataset,
    245                                column - 1);
    246                        y0 = y0 / total;
    247                    }
    248
    249
    250                    // Get the previous height, but this will be different for both
    251                    // y0 and y1 as the previous series values could differ.
    252                    double previousHeightx0 = getPreviousHeight(dataset, row,
    253                            column - 1);
    254
    255                    // Now stack the current y values on top of the previous values.
    256                    y0 += previousHeightx0;
    257
    258                    // Now translate the previous heights
    259                    double previousHeightxx0 = rangeAxis.valueToJava2D(
    260                            previousHeightx0, dataArea, location);
    261                    double previousHeightxx1 = rangeAxis.valueToJava2D(
    262                            previousHeightx1, dataArea, location);
    263
    264                    // Now translate the current y values.
    265                    double yy0 = rangeAxis.valueToJava2D(y0, dataArea, location);
    266
    267                    if (pass == 0) {
    268                        Polygon p = null;
    269                        PlotOrientation orientation = plot.getOrientation();
    270                        if (orientation == PlotOrientation.HORIZONTAL) {
    271                            p = new Polygon();
    272                            p.addPoint((int) yy0, (int) xx0);
    273                            p.addPoint((int) yy1, (int) xx1);
    274                            p.addPoint((int) previousHeightxx1, (int) xx1);
    275                            p.addPoint((int) previousHeightxx0, (int) xx0);
    276                        }
    277                        else if (orientation == PlotOrientation.VERTICAL) {
    278                            p = new Polygon();
    279                            p.addPoint((int) xx0, (int) yy0);
    280                            p.addPoint((int) xx1, (int) yy1);
    281                            p.addPoint((int) xx1, (int) previousHeightxx1);
    282                            p.addPoint((int) xx0, (int) previousHeightxx0);
    283                        }
    284                        g2.setPaint(getItemPaint(row, column));
    285                        g2.setStroke(getItemStroke(row, column));
    286                        g2.fill(p);
    287                    }
    288                    else {
    289                        if (isItemLabelVisible(row, column)) {
    290                            drawItemLabel(g2, plot.getOrientation(), dataset, row,
    291                                    column, xx1, yy1, (y1 < 0.0));
    292                        }
    293                    }
    294                }
    295
    296
    297            }
    298
    299
    300            // add an item entity, if this information is being collected
    301            EntityCollection entities = state.getEntityCollection();
    302            if (entities != null) {
    303                Shape shape = new Rectangle2D.Double(xx1 - 3.0, yy1 - 3.0, 6.0, 6.0);
    304                addItemEntity(entities, dataset, row, column, shape);
    305            }
    306
    307        }
    308
    309        /**
    310         * Calculates the stacked value of the all series up to, but not including
    311         * <code>series</code> for the specified category, <code>category</code>.
    312         * It returns 0.0 if <code>series</code> is the first series, i.e. 0.
    313         *
    314         * @param dataset  the dataset (<code>null</code> not permitted).
    315         * @param series  the series.
    316         * @param category  the category.
    317         *
    318         * @return double returns a cumulative value for all series' values up to
    319         *         but excluding <code>series</code> for Object
    320         *         <code>category</code>.
    321         */
    322        protected double getPreviousHeight(CategoryDataset dataset,
    323                                           int series, int category) {
    324
    325            double result = 0.0;
    326            Number n;
    327            double total = 0.0;
    328            if (this.renderAsPercentages) {
    329                total = DataUtilities.calculateColumnTotal(dataset, category);
    330            }
    331            for (int i = 0; i < series; i++) {
    332                n = dataset.getValue(i, category);
    333                if (n != null) {
    334                    double v = n.doubleValue();
    335                    if (this.renderAsPercentages) {
    336                        v = v / total;
    337                    }
    338                    result += v;
    339                }
    340            }
    341            return result;
    342
    343        }
    344
    345        /**
    346         * Checks this instance for equality with an arbitrary object.
    347         *
    348         * @param obj  the object (<code>null</code> not permitted).
    349         *
    350         * @return A boolean.
    351         */
    352        public boolean equals(Object obj) {
    353            if (obj == this) {
    354                return true;
    355            }
    356            if (! (obj instanceof StackedAreaRenderer)) {
    357                return false;
    358            }
    359            StackedAreaRenderer that = (StackedAreaRenderer) obj;
    360            if (this.renderAsPercentages != that.renderAsPercentages) {
    361                return false;
    362            }
    363            return super.equals(obj);
    364        }
    365    }