    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     * StackedBarRenderer.java
    029     * -----------------------
    030     * (C) Copyright 2000-2005, 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.7 2005/10/25 20:54:16 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     *
    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,
    124                                               Serializable {
    125
    126        /** For serialization. */
    127        static final long serialVersionUID = 6402943811500067531L;
    128
    129        /** A flag that controls whether the bars display values or percentages. */
    130        private boolean renderAsPercentages;
    131
    132        /**
    133         * Creates a new renderer.  By default, the renderer has no tool tip
    134         * generator and no URL generator.  These defaults have been chosen to
    135         * minimise the processing required to generate a default chart.  If you
    136         * require tool tips or URLs, then you can easily add the required
    137         * generators.
    138         */
    139        public StackedBarRenderer() {
    140            this(false);
    141        }
    142
    143        /**
    144         * Creates a new renderer.
    145         *
    146         * @param renderAsPercentages  a flag that controls whether the data values
    147         *                             are rendered as percentages.
    148         */
    149        public StackedBarRenderer(boolean renderAsPercentages) {
    150            super();
    151            this.renderAsPercentages = renderAsPercentages;
    152
    153            // set the default item label positions, which will only be used if
    154            // the user requests visible item labels...
    155            ItemLabelPosition p = new ItemLabelPosition(
    156                ItemLabelAnchor.CENTER, TextAnchor.CENTER
    157            );
    158            setBasePositiveItemLabelPosition(p);
    159            setBaseNegativeItemLabelPosition(p);
    160            setPositiveItemLabelPositionFallback(null);
    161            setNegativeItemLabelPositionFallback(null);
    162        }
    163
    164        /**
    165         * Returns <code>true</code> if the renderer displays each item value as
    166         * a percentage (so that the stacked bars add to 100%), and
    167         * <code>false</code> otherwise.
    168         *
    169         * @return A 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        public void setRenderAsPercentages(boolean asPercentages) {
    183            this.renderAsPercentages = asPercentages;
    184            notifyListeners(new RendererChangeEvent(this));
    185        }
    186
    187        /**
    188         * Returns the number of passes (<code>2</code>) required by this renderer.
    189         * The first pass is used to draw the bars, the second pass is used to
    190         * draw the item labels (if visible).
    191         *
    192         * @return The number of passes required by the renderer.
    193         */
    194        public int getPassCount() {
    195            return 2;
    196        }
    197
    198        /**
    199         * Returns the range of values the renderer requires to display all the
    200         * items from the specified dataset.
    201         *
    202         * @param dataset  the dataset (<code>null</code> permitted).
    203         *
    204         * @return The range (or <code>null</code> if the dataset is empty).
    205         */
    206        public Range findRangeBounds(CategoryDataset dataset) {
    207            if (this.renderAsPercentages) {
    208                return new Range(0.0, 1.0);
    209            }
    210            else {
    211                return DatasetUtilities.findStackedRangeBounds(dataset, getBase());
    212            }
    213        }
    214
    215        /**
    216         * Calculates the bar width and stores it in the renderer state.
    217         *
    218         * @param plot  the plot.
    219         * @param dataArea  the data area.
    220         * @param rendererIndex  the renderer index.
    221         * @param state  the renderer state.
    222         */
    223        protected void calculateBarWidth(CategoryPlot plot,
    224                                         Rectangle2D dataArea,
    225                                         int rendererIndex,
    226                                         CategoryItemRendererState state) {
    227
    228            // calculate the bar width
    229            CategoryAxis xAxis = plot.getDomainAxisForDataset(rendererIndex);
    230            CategoryDataset data = plot.getDataset(rendererIndex);
    231            if (data != null) {
    232                PlotOrientation orientation = plot.getOrientation();
    233                double space = 0.0;
    234                if (orientation == PlotOrientation.HORIZONTAL) {
    235                    space = dataArea.getHeight();
    236                }
    237                else if (orientation == PlotOrientation.VERTICAL) {
    238                    space = dataArea.getWidth();
    239                }
    240                double maxWidth = space * getMaximumBarWidth();
    241                int columns = data.getColumnCount();
    242                double categoryMargin = 0.0;
    243                if (columns > 1) {
    244                    categoryMargin = xAxis.getCategoryMargin();
    245                }
    246
    247                double used = space * (1 - xAxis.getLowerMargin()
    248                                         - xAxis.getUpperMargin()
    249                                         - categoryMargin);
    250                if (columns > 0) {
    251                    state.setBarWidth(Math.min(used / columns, maxWidth));
    252                }
    253                else {
    254                    state.setBarWidth(Math.min(used, maxWidth));
    255                }
    256            }
    257
    258        }
    259
    260        /**
    261         * Draws a stacked bar for a specific item.
    262         *
    263         * @param g2  the graphics device.
    264         * @param state  the renderer state.
    265         * @param dataArea  the plot area.
    266         * @param plot  the plot.
    267         * @param domainAxis  the domain (category) axis.
    268         * @param rangeAxis  the range (value) axis.
    269         * @param dataset  the data.
    270         * @param row  the row index (zero-based).
    271         * @param column  the column index (zero-based).
    272         * @param pass  the pass index.
    273         */
    274        public void drawItem(Graphics2D g2,
    275                             CategoryItemRendererState state,
    276                             Rectangle2D dataArea,
    277                             CategoryPlot plot,
    278                             CategoryAxis domainAxis,
    279                             ValueAxis rangeAxis,
    280                             CategoryDataset dataset,
    281                             int row,
    282                             int column,
    283                             int pass) {
    284
    285            // nothing is drawn for null values...
    286            Number dataValue = dataset.getValue(row, column);
    287            if (dataValue == null) {
    288                return;
    289            }
    290
    291            double value = dataValue.doubleValue();
    292            double total = 0.0;  // only needed if calculating percentages
    293            if (this.renderAsPercentages) {
    294                total = DataUtilities.calculateColumnTotal(dataset, column);
    295                value = value / total;
    296            }
    297
    298            PlotOrientation orientation = plot.getOrientation();
    299            double barW0 = domainAxis.getCategoryMiddle(
    300                column, getColumnCount(), dataArea, plot.getDomainAxisEdge()
    301            ) - state.getBarWidth() / 2.0;
    302
    303            double positiveBase = getBase();
    304            double negativeBase = positiveBase;
    305
    306            for (int i = 0; i < row; i++) {
    307                Number v = dataset.getValue(i, column);
    308                if (v != null) {
    309                    double d = v.doubleValue();
    310                    if (this.renderAsPercentages) {
    311                        d = d / total;
    312                    }
    313                    if (d > 0) {
    314                        positiveBase = positiveBase + d;
    315                    }
    316                    else {
    317                        negativeBase = negativeBase + d;
    318                    }
    319                }
    320            }
    321
    322            double translatedBase;
    323            double translatedValue;
    324            RectangleEdge location = plot.getRangeAxisEdge();
    325            if (value >= 0.0) {
    326                translatedBase = rangeAxis.valueToJava2D(
    327                    positiveBase, dataArea, location
    328                );
    329                translatedValue = rangeAxis.valueToJava2D(
    330                    positiveBase + value, dataArea, location
    331                );
    332            }
    333            else {
    334                translatedBase = rangeAxis.valueToJava2D(
    335                    negativeBase, dataArea, location
    336                );
    337                translatedValue = rangeAxis.valueToJava2D(
    338                    negativeBase + value, dataArea, location
    339                );
    340            }
    341            double barL0 = Math.min(translatedBase, translatedValue);
    342            double barLength = Math.max(
    343                Math.abs(translatedValue - translatedBase), getMinimumBarLength()
    344            );
    345
    346            Rectangle2D bar = null;
    347            if (orientation == PlotOrientation.HORIZONTAL) {
    348                bar = new Rectangle2D.Double(
    349                    barL0, barW0, barLength, state.getBarWidth());
    350            }
    351            else {
    352                bar = new Rectangle2D.Double(
    353                    barW0, barL0, state.getBarWidth(), barLength
    354                );
    355            }
    356            if (pass == 0) {
    357                Paint itemPaint = getItemPaint(row, column);
    358                GradientPaintTransformer t = getGradientPaintTransformer();
    359                if (t != null && itemPaint instanceof GradientPaint) {
    360                    itemPaint = t.transform((GradientPaint) itemPaint, bar);
    361                }
    362                g2.setPaint(itemPaint);
    363                g2.fill(bar);
    364                if (isDrawBarOutline()
    365                        && state.getBarWidth() > BAR_OUTLINE_WIDTH_THRESHOLD) {
    366                    g2.setStroke(getItemOutlineStroke(row, column));
    367                    g2.setPaint(getItemOutlinePaint(row, column));
    368                    g2.draw(bar);
    369                }
    370
    371                // add an item entity, if this information is being collected
    372                EntityCollection entities = state.getEntityCollection();
    373                if (entities != null) {
    374                    addItemEntity(entities, dataset, row, column, bar);
    375                }
    376            }
    377            else if (pass == 1) {
    378                CategoryItemLabelGenerator generator
    379                    = getItemLabelGenerator(row, column);
    380                if (generator != null && isItemLabelVisible(row, column)) {
    381                    drawItemLabel(
    382                        g2, dataset, row, column, plot, generator, bar,
    383                        (value < 0.0)
    384                    );
    385                }
    386            }
    387        }
    388
    389        /**
    390         * Tests this renderer for equality with an arbitrary object.
    391         *
    392         * @param obj  the object (<code>null</code> permitted).
    393         *
    394         * @return A boolean.
    395         */
    396        public boolean equals(Object obj) {
    397            if (obj == this) {
    398                return true;
    399            }
    400            if (!(obj instanceof StackedBarRenderer)) {
    401                return false;
    402            }
    403            if (!super.equals(obj)) {
    404                return false;
    405            }
    406            StackedBarRenderer that = (StackedBarRenderer) obj;
    407            if (this.renderAsPercentages != that.renderAsPercentages) {
    408                return false;
    409            }
    410            return true;
    411        }
    412
    413    }