    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     * 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,
    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(ItemLabelAnchor.CENTER,
    156                    TextAnchor.CENTER);
    157            setBasePositiveItemLabelPosition(p);
    158            setBaseNegativeItemLabelPosition(p);
    159            setPositiveItemLabelPositionFallback(null);
    160            setNegativeItemLabelPositionFallback(null);
    161        }
    162
    163        /**
    164         * Returns <code>true</code> if the renderer displays each item value as
    165         * a percentage (so that the stacked bars add to 100%), and
    166         * <code>false</code> otherwise.
    167         *
    168         * @return A boolean.
    169         *
    170         * @see #setRenderAsPercentages(boolean)
    171         */
    172        public boolean getRenderAsPercentages() {
    173            return this.renderAsPercentages;
    174        }
    175
    176        /**
    177         * Sets the flag that controls whether the renderer displays each item
    178         * value as a percentage (so that the stacked bars add to 100%), and sends
    179         * a {@link RendererChangeEvent} to all registered listeners.
    180         *
    181         * @param asPercentages  the flag.
    182         *
    183         * @see #getRenderAsPercentages()
    184         */
    185        public void setRenderAsPercentages(boolean asPercentages) {
    186            this.renderAsPercentages = asPercentages;
    187            fireChangeEvent();
    188        }
    189
    190        /**
    191         * Returns the number of passes (<code>2</code>) required by this renderer.
    192         * The first pass is used to draw the bars, the second pass is used to
    193         * draw the item labels (if visible).
    194         *
    195         * @return The number of passes required by the renderer.
    196         */
    197        public int getPassCount() {
    198            return 2;
    199        }
    200
    201        /**
    202         * Returns the range of values the renderer requires to display all the
    203         * items from the specified dataset.
    204         *
    205         * @param dataset  the dataset (<code>null</code> permitted).
    206         *
    207         * @return The range (or <code>null</code> if the dataset is empty).
    208         */
    209        public Range findRangeBounds(CategoryDataset dataset) {
    210            if (this.renderAsPercentages) {
    211                return new Range(0.0, 1.0);
    212            }
    213            else {
    214                return DatasetUtilities.findStackedRangeBounds(dataset, getBase());
    215            }
    216        }
    217
    218        /**
    219         * Calculates the bar width and stores it in the renderer state.
    220         *
    221         * @param plot  the plot.
    222         * @param dataArea  the data area.
    223         * @param rendererIndex  the renderer index.
    224         * @param state  the renderer state.
    225         */
    226        protected void calculateBarWidth(CategoryPlot plot,
    227                                         Rectangle2D dataArea,
    228                                         int rendererIndex,
    229                                         CategoryItemRendererState state) {
    230
    231            // calculate the bar width
    232            CategoryAxis xAxis = plot.getDomainAxisForDataset(rendererIndex);
    233            CategoryDataset data = plot.getDataset(rendererIndex);
    234            if (data != null) {
    235                PlotOrientation orientation = plot.getOrientation();
    236                double space = 0.0;
    237                if (orientation == PlotOrientation.HORIZONTAL) {
    238                    space = dataArea.getHeight();
    239                }
    240                else if (orientation == PlotOrientation.VERTICAL) {
    241                    space = dataArea.getWidth();
    242                }
    243                double maxWidth = space * getMaximumBarWidth();
    244                int columns = data.getColumnCount();
    245                double categoryMargin = 0.0;
    246                if (columns > 1) {
    247                    categoryMargin = xAxis.getCategoryMargin();
    248                }
    249
    250                double used = space * (1 - xAxis.getLowerMargin()
    251                                         - xAxis.getUpperMargin()
    252                                         - categoryMargin);
    253                if (columns > 0) {
    254                    state.setBarWidth(Math.min(used / columns, maxWidth));
    255                }
    256                else {
    257                    state.setBarWidth(Math.min(used, maxWidth));
    258                }
    259            }
    260
    261        }
    262
    263        /**
    264         * Draws a stacked bar for a specific item.
    265         *
    266         * @param g2  the graphics device.
    267         * @param state  the renderer state.
    268         * @param dataArea  the plot area.
    269         * @param plot  the plot.
    270         * @param domainAxis  the domain (category) axis.
    271         * @param rangeAxis  the range (value) axis.
    272         * @param dataset  the data.
    273         * @param row  the row index (zero-based).
    274         * @param column  the column index (zero-based).
    275         * @param pass  the pass index.
    276         */
    277        public void drawItem(Graphics2D g2,
    278                             CategoryItemRendererState state,
    279                             Rectangle2D dataArea,
    280                             CategoryPlot plot,
    281                             CategoryAxis domainAxis,
    282                             ValueAxis rangeAxis,
    283                             CategoryDataset dataset,
    284                             int row,
    285                             int column,
    286                             int pass) {
    287
    288            // nothing is drawn for null values...
    289            Number dataValue = dataset.getValue(row, column);
    290            if (dataValue == null) {
    291                return;
    292            }
    293
    294            double value = dataValue.doubleValue();
    295            double total = 0.0;  // only needed if calculating percentages
    296            if (this.renderAsPercentages) {
    297                total = DataUtilities.calculateColumnTotal(dataset, column);
    298                value = value / total;
    299            }
    300
    301            PlotOrientation orientation = plot.getOrientation();
    302            double barW0 = domainAxis.getCategoryMiddle(column, getColumnCount(),
    303                    dataArea, plot.getDomainAxisEdge())
    304                    - state.getBarWidth() / 2.0;
    305
    306            double positiveBase = getBase();
    307            double negativeBase = positiveBase;
    308
    309            for (int i = 0; i < row; i++) {
    310                Number v = dataset.getValue(i, column);
    311                if (v != null) {
    312                    double d = v.doubleValue();
    313                    if (this.renderAsPercentages) {
    314                        d = d / total;
    315                    }
    316                    if (d > 0) {
    317                        positiveBase = positiveBase + d;
    318                    }
    319                    else {
    320                        negativeBase = negativeBase + d;
    321                    }
    322                }
    323            }
    324
    325            double translatedBase;
    326            double translatedValue;
    327            RectangleEdge location = plot.getRangeAxisEdge();
    328            if (value >= 0.0) {
    329                translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea,
    330                        location);
    331                translatedValue = rangeAxis.valueToJava2D(positiveBase + value,
    332                        dataArea, location);
    333            }
    334            else {
    335                translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea,
    336                        location);
    337                translatedValue = rangeAxis.valueToJava2D(negativeBase + value,
    338                        dataArea, location);
    339            }
    340            double barL0 = Math.min(translatedBase, translatedValue);
    341            double barLength = Math.max(Math.abs(translatedValue - translatedBase),
    342                    getMinimumBarLength());
    343
    344            Rectangle2D bar = null;
    345            if (orientation == PlotOrientation.HORIZONTAL) {
    346                bar = new Rectangle2D.Double(barL0, barW0, barLength,
    347                        state.getBarWidth());
    348            }
    349            else {
    350                bar = new Rectangle2D.Double(barW0, barL0, state.getBarWidth(),
    351                        barLength);
    352            }
    353            if (pass == 0) {
    354                Paint itemPaint = getItemPaint(row, column);
    355                GradientPaintTransformer t = getGradientPaintTransformer();
    356                if (t != null && itemPaint instanceof GradientPaint) {
    357                    itemPaint = t.transform((GradientPaint) itemPaint, bar);
    358                }
    359                g2.setPaint(itemPaint);
    360                g2.fill(bar);
    361                if (isDrawBarOutline()
    362                        && state.getBarWidth() > BAR_OUTLINE_WIDTH_THRESHOLD) {
    363                    g2.setStroke(getItemOutlineStroke(row, column));
    364                    g2.setPaint(getItemOutlinePaint(row, column));
    365                    g2.draw(bar);
    366                }
    367
    368                // add an item entity, if this information is being collected
    369                EntityCollection entities = state.getEntityCollection();
    370                if (entities != null) {
    371                    addItemEntity(entities, dataset, row, column, bar);
    372                }
    373            }
    374            else if (pass == 1) {
    375                CategoryItemLabelGenerator generator = getItemLabelGenerator(row,
    376                        column);
    377                if (generator != null && isItemLabelVisible(row, column)) {
    378                    drawItemLabel(g2, dataset, row, column, plot, generator, bar,
    379                            (value < 0.0));
    380                }
    381            }
    382        }
    383
    384        /**
    385         * Tests this renderer for equality with an arbitrary object.
    386         *
    387         * @param obj  the object (<code>null</code> permitted).
    388         *
    389         * @return A boolean.
    390         */
    391        public boolean equals(Object obj) {
    392            if (obj == this) {
    393                return true;
    394            }
    395            if (!(obj instanceof StackedBarRenderer)) {
    396                return false;
    397            }
    398            StackedBarRenderer that = (StackedBarRenderer) obj;
    399            if (this.renderAsPercentages != that.renderAsPercentages) {
    400                return false;
    401            }
    402            return super.equals(obj);
    403        }
    404
    405    }