    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     * StackedBarRenderer.java
    029     * -----------------------
    030     * (C) Copyright 2000-2009, by Object Refinery Limited and Contributors.
    031     *
    032     * Original Author:  David Gilbert (for Object Refinery Limited);
    033     * Contributor(s):   Richard Atkinson;
    034     *                   Thierry Saura;
    035     *                   Christian W. Zuckschwerdt;
    036     *                   Peter Kolb (patch 2511330);
    037     *
    038     * Changes
    039     * -------
    040     * 19-Oct-2001 : Version 1 (DG);
    041     * 22-Oct-2001 : Renamed DataSource.java --> Dataset.java etc. (DG);
    042     * 23-Oct-2001 : Changed intro and trail gaps on bar plots to use percentage of
    043     *               available space rather than a fixed number of units (DG);
    044     * 15-Nov-2001 : Modified to allow for null data values (DG);
    045     * 22-Nov-2001 : Modified to allow for negative data values (DG);
    046     * 13-Dec-2001 : Added tooltips (DG);
    047     * 16-Jan-2002 : Fixed bug for single category datasets (DG);
    048     * 15-Feb-2002 : Added isStacked() method (DG);
    049     * 14-Mar-2002 : Modified to implement the CategoryItemRenderer interface (DG);
    050     * 24-May-2002 : Incorporated tooltips into chart entities (DG);
    051     * 11-Jun-2002 : Added check for (permitted) null info object, bug and fix
    052     *               reported by David Basten.  Also updated Javadocs. (DG);
    053     * 25-Jun-2002 : Removed redundant import (DG);
    054     * 26-Jun-2002 : Small change to entity (DG);
    055     * 05-Aug-2002 : Small modification to drawCategoryItem method to support URLs
    056     *               for HTML image maps (RA);
    057     * 08-Aug-2002 : Added optional linking lines, contributed by Thierry
    058     *               Saura (DG);
    059     * 26-Sep-2002 : Fixed errors reported by Checkstyle (DG);
    060     * 24-Oct-2002 : Amendments for changes in CategoryDataset interface and
    061     *               CategoryToolTipGenerator interface (DG);
    062     * 05-Nov-2002 : Replaced references to CategoryDataset with TableDataset (DG);
    063     * 26-Nov-2002 : Replaced isStacked() method with getRangeType() method (DG);
    064     * 17-Jan-2003 : Moved plot classes to a separate package (DG);
    065     * 25-Mar-2003 : Implemented Serializable (DG);
    066     * 12-May-2003 : Merged horizontal and vertical stacked bar renderers (DG);
    067     * 30-Jul-2003 : Modified entity constructor (CZ);
    068     * 08-Sep-2003 : Fixed bug 799668 (isBarOutlineDrawn() ignored) (DG);
    069     * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
    070     * 21-Oct-2003 : Moved bar width into renderer state (DG);
    071     * 26-Nov-2003 : Added code to respect maxBarWidth attribute (DG);
    072     * 05-Nov-2004 : Changed to a two-pass renderer so that item labels are not
    073     *               overwritten by other bars (DG);
    074     * 07-Jan-2005 : Renamed getRangeExtent() --> findRangeBounds() (DG);
    075     * 29-Mar-2005 : Modified drawItem() method so that a zero value is handled
    076     *               within the code for positive rather than negative values (DG);
    077     * 20-Apr-2005 : Renamed CategoryLabelGenerator
    078     *               --> CategoryItemLabelGenerator (DG);
    079     * 17-May-2005 : Added flag to allow rendering values as percentages - inspired
    080     *               by patch 1200886 submitted by John Xiao (DG);
    081     * 09-Jun-2005 : Added accessor methods for the renderAsPercentages flag,
    082     *               provided equals() method, and use addItemEntity from
    083     *               superclass (DG);
    084     * 09-Jun-2005 : Added support for GradientPaint - see bug report 1215670 (DG);
    085     * 22-Sep-2005 : Renamed getMaxBarWidth() --> getMaximumBarWidth() (DG);
    086     * 29-Sep-2005 : Use outline stroke in drawItem method - see bug report
    087     *               1304139 (DG);
    088     * ------------- JFREECHART 1.0.x ---------------------------------------------
    089     * 11-Oct-2006 : Source reformatting (DG);
    090     * 24-Jun-2008 : Added new barPainter mechanism (DG);
    091     * 04-Feb-2009 : Added support for hidden series (PK);
    092     *
    093     */
    094
    095    package org.jfree.chart.renderer.category;
    096
    097    import java.awt.Graphics2D;
    098    import java.awt.geom.Rectangle2D;
    099    import java.io.Serializable;
    100
    101    import org.jfree.chart.axis.CategoryAxis;
    102    import org.jfree.chart.axis.ValueAxis;
    103    import org.jfree.chart.entity.EntityCollection;
    104    import org.jfree.chart.event.RendererChangeEvent;
    105    import org.jfree.chart.labels.CategoryItemLabelGenerator;
    106    import org.jfree.chart.labels.ItemLabelAnchor;
    107    import org.jfree.chart.labels.ItemLabelPosition;
    108    import org.jfree.chart.plot.CategoryPlot;
    109    import org.jfree.chart.plot.PlotOrientation;
    110    import org.jfree.data.DataUtilities;
    111    import org.jfree.data.Range;
    112    import org.jfree.data.category.CategoryDataset;
    113    import org.jfree.data.general.DatasetUtilities;
    114    import org.jfree.ui.RectangleEdge;
    115    import org.jfree.ui.TextAnchor;
    116    import org.jfree.util.PublicCloneable;
    117
    118    /**
    119     * A stacked bar renderer for use with the {@link CategoryPlot} class.
    120     * The example shown here is generated by the
    121     * <code>StackedBarChartDemo1.java</code> program included in the
    122     * JFreeChart Demo Collection:
    123     * <br><br>
    124     * <img src="../../../../../images/StackedBarRendererSample.png"
    125     * alt="StackedBarRendererSample.png" />
    126     */
    127    public class StackedBarRenderer extends BarRenderer
    128            implements Cloneable, PublicCloneable, Serializable {
    129
    130        /** For serialization. */
    131        static final long serialVersionUID = 6402943811500067531L;
    132
    133        /** A flag that controls whether the bars display values or percentages. */
    134        private boolean renderAsPercentages;
    135
    136        /**
    137         * Creates a new renderer.  By default, the renderer has no tool tip
    138         * generator and no URL generator.  These defaults have been chosen to
    139         * minimise the processing required to generate a default chart.  If you
    140         * require tool tips or URLs, then you can easily add the required
    141         * generators.
    142         */
    143        public StackedBarRenderer() {
    144            this(false);
    145        }
    146
    147        /**
    148         * Creates a new renderer.
    149         *
    150         * @param renderAsPercentages  a flag that controls whether the data values
    151         *                             are rendered as percentages.
    152         */
    153        public StackedBarRenderer(boolean renderAsPercentages) {
    154            super();
    155            this.renderAsPercentages = renderAsPercentages;
    156
    157            // set the default item label positions, which will only be used if
    158            // the user requests visible item labels...
    159            ItemLabelPosition p = new ItemLabelPosition(ItemLabelAnchor.CENTER,
    160                    TextAnchor.CENTER);
    161            setBasePositiveItemLabelPosition(p);
    162            setBaseNegativeItemLabelPosition(p);
    163            setPositiveItemLabelPositionFallback(null);
    164            setNegativeItemLabelPositionFallback(null);
    165        }
    166
    167        /**
    168         * Returns <code>true</code> if the renderer displays each item value as
    169         * a percentage (so that the stacked bars add to 100%), and
    170         * <code>false</code> otherwise.
    171         *
    172         * @return A boolean.
    173         *
    174         * @see #setRenderAsPercentages(boolean)
    175         */
    176        public boolean getRenderAsPercentages() {
    177            return this.renderAsPercentages;
    178        }
    179
    180        /**
    181         * Sets the flag that controls whether the renderer displays each item
    182         * value as a percentage (so that the stacked bars add to 100%), and sends
    183         * a {@link RendererChangeEvent} to all registered listeners.
    184         *
    185         * @param asPercentages  the flag.
    186         *
    187         * @see #getRenderAsPercentages()
    188         */
    189        public void setRenderAsPercentages(boolean asPercentages) {
    190            this.renderAsPercentages = asPercentages;
    191            fireChangeEvent();
    192        }
    193
    194        /**
    195         * Returns the number of passes (<code>3</code>) required by this renderer.
    196         * The first pass is used to draw the bar shadows, the second pass is used
    197         * to draw the bars, and the third pass is used to draw the item labels
    198         * (if visible).
    199         *
    200         * @return The number of passes required by the renderer.
    201         */
    202        public int getPassCount() {
    203            return 3;
    204        }
    205
    206        /**
    207         * Returns the range of values the renderer requires to display all the
    208         * items from the specified dataset.
    209         *
    210         * @param dataset  the dataset (<code>null</code> permitted).
    211         *
    212         * @return The range (or <code>null</code> if the dataset is empty).
    213         */
    214        public Range findRangeBounds(CategoryDataset dataset) {
    215            if (dataset == null) {
    216                return null;
    217            }
    218            if (this.renderAsPercentages) {
    219                return new Range(0.0, 1.0);
    220            }
    221            else {
    222                return DatasetUtilities.findStackedRangeBounds(dataset, getBase());
    223            }
    224        }
    225
    226        /**
    227         * Calculates the bar width and stores it in the renderer state.
    228         *
    229         * @param plot  the plot.
    230         * @param dataArea  the data area.
    231         * @param rendererIndex  the renderer index.
    232         * @param state  the renderer state.
    233         */
    234        protected void calculateBarWidth(CategoryPlot plot,
    235                                         Rectangle2D dataArea,
    236                                         int rendererIndex,
    237                                         CategoryItemRendererState state) {
    238
    239            // calculate the bar width
    240            CategoryAxis xAxis = plot.getDomainAxisForDataset(rendererIndex);
    241            CategoryDataset data = plot.getDataset(rendererIndex);
    242            if (data != null) {
    243                PlotOrientation orientation = plot.getOrientation();
    244                double space = 0.0;
    245                if (orientation == PlotOrientation.HORIZONTAL) {
    246                    space = dataArea.getHeight();
    247                }
    248                else if (orientation == PlotOrientation.VERTICAL) {
    249                    space = dataArea.getWidth();
    250                }
    251                double maxWidth = space * getMaximumBarWidth();
    252                int columns = data.getColumnCount();
    253                double categoryMargin = 0.0;
    254                if (columns > 1) {
    255                    categoryMargin = xAxis.getCategoryMargin();
    256                }
    257
    258                double used = space * (1 - xAxis.getLowerMargin()
    259                                         - xAxis.getUpperMargin()
    260                                         - categoryMargin);
    261                if (columns > 0) {
    262                    state.setBarWidth(Math.min(used / columns, maxWidth));
    263                }
    264                else {
    265                    state.setBarWidth(Math.min(used, maxWidth));
    266                }
    267            }
    268
    269        }
    270
    271        /**
    272         * Draws a stacked bar for a specific item.
    273         *
    274         * @param g2  the graphics device.
    275         * @param state  the renderer state.
    276         * @param dataArea  the plot area.
    277         * @param plot  the plot.
    278         * @param domainAxis  the domain (category) axis.
    279         * @param rangeAxis  the range (value) axis.
    280         * @param dataset  the data.
    281         * @param row  the row index (zero-based).
    282         * @param column  the column index (zero-based).
    283         * @param pass  the pass index.
    284         */
    285        public void drawItem(Graphics2D g2,
    286                             CategoryItemRendererState state,
    287                             Rectangle2D dataArea,
    288                             CategoryPlot plot,
    289                             CategoryAxis domainAxis,
    290                             ValueAxis rangeAxis,
    291                             CategoryDataset dataset,
    292                             int row,
    293                             int column,
    294                             int pass) {
    295
    296            if (!isSeriesVisible(row)) {
    297                return;
    298            }
    299
    300            // nothing is drawn for null values...
    301            Number dataValue = dataset.getValue(row, column);
    302            if (dataValue == null) {
    303                return;
    304            }
    305
    306            double value = dataValue.doubleValue();
    307            double total = 0.0;  // only needed if calculating percentages
    308            if (this.renderAsPercentages) {
    309                total = DataUtilities.calculateColumnTotal(dataset, column,
    310                        state.getVisibleSeriesArray());
    311                value = value / total;
    312            }
    313
    314            PlotOrientation orientation = plot.getOrientation();
    315            double barW0 = domainAxis.getCategoryMiddle(column, getColumnCount(),
    316                    dataArea, plot.getDomainAxisEdge())
    317                    - state.getBarWidth() / 2.0;
    318
    319            double positiveBase = getBase();
    320            double negativeBase = positiveBase;
    321
    322            for (int i = 0; i < row; i++) {
    323                Number v = dataset.getValue(i, column);
    324                if (v != null && isSeriesVisible(i)) {
    325                    double d = v.doubleValue();
    326                    if (this.renderAsPercentages) {
    327                        d = d / total;
    328                    }
    329                    if (d > 0) {
    330                        positiveBase = positiveBase + d;
    331                    }
    332                    else {
    333                        negativeBase = negativeBase + d;
    334                    }
    335                }
    336            }
    337
    338            double translatedBase;
    339            double translatedValue;
    340            boolean positive = (value > 0.0);
    341            boolean inverted = rangeAxis.isInverted();
    342            RectangleEdge barBase;
    343            if (orientation == PlotOrientation.HORIZONTAL) {
    344                if (positive && inverted || !positive && !inverted) {
    345                    barBase = RectangleEdge.RIGHT;
    346                }
    347                else {
    348                    barBase = RectangleEdge.LEFT;
    349                }
    350            }
    351            else {
    352                if (positive && !inverted || !positive && inverted) {
    353                    barBase = RectangleEdge.BOTTOM;
    354                }
    355                else {
    356                    barBase = RectangleEdge.TOP;
    357                }
    358            }
    359
    360            RectangleEdge location = plot.getRangeAxisEdge();
    361            if (positive) {
    362                translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea,
    363                        location);
    364                translatedValue = rangeAxis.valueToJava2D(positiveBase + value,
    365                        dataArea, location);
    366            }
    367            else {
    368                translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea,
    369                        location);
    370                translatedValue = rangeAxis.valueToJava2D(negativeBase + value,
    371                        dataArea, location);
    372            }
    373            double barL0 = Math.min(translatedBase, translatedValue);
    374            double barLength = Math.max(Math.abs(translatedValue - translatedBase),
    375                    getMinimumBarLength());
    376
    377            Rectangle2D bar = null;
    378            if (orientation == PlotOrientation.HORIZONTAL) {
    379                bar = new Rectangle2D.Double(barL0, barW0, barLength,
    380                        state.getBarWidth());
    381            }
    382            else {
    383                bar = new Rectangle2D.Double(barW0, barL0, state.getBarWidth(),
    384                        barLength);
    385            }
    386            if (pass == 0) {
    387                if (getShadowsVisible()) {
    388                    boolean pegToBase = (positive && (positiveBase == getBase()))
    389                            || (!positive && (negativeBase == getBase()));
    390                    getBarPainter().paintBarShadow(g2, this, row, column, bar,
    391                            barBase, pegToBase);
    392                }
    393            }
    394            else if (pass == 1) {
    395                getBarPainter().paintBar(g2, this, row, column, bar, barBase);
    396
    397                // add an item entity, if this information is being collected
    398                EntityCollection entities = state.getEntityCollection();
    399                if (entities != null) {
    400                    addItemEntity(entities, dataset, row, column, bar);
    401                }
    402            }
    403            else if (pass == 2) {
    404                CategoryItemLabelGenerator generator = getItemLabelGenerator(row,
    405                        column);
    406                if (generator != null && isItemLabelVisible(row, column)) {
    407                    drawItemLabel(g2, dataset, row, column, plot, generator, bar,
    408                            (value < 0.0));
    409                }
    410            }
    411        }
    412
    413        /**
    414         * Tests this renderer for equality with an arbitrary object.
    415         *
    416         * @param obj  the object (<code>null</code> permitted).
    417         *
    418         * @return A boolean.
    419         */
    420        public boolean equals(Object obj) {
    421            if (obj == this) {
    422                return true;
    423            }
    424            if (!(obj instanceof StackedBarRenderer)) {
    425                return false;
    426            }
    427            StackedBarRenderer that = (StackedBarRenderer) obj;
    428            if (this.renderAsPercentages != that.renderAsPercentages) {
    429                return false;
    430            }
    431            return super.equals(obj);
    432        }
    433
    434    }