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