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