    001/* ===========================================================
    002 * JFreeChart : a free chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C) Copyright 2000-2011, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
    025 * Other names may be trademarks of their respective owners.]
    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
    095package org.jfree.chart.renderer.category;
    096
    097import java.awt.Graphics2D;
    098import java.awt.geom.Rectangle2D;
    099import java.io.Serializable;
    100
    101import org.jfree.chart.axis.CategoryAxis;
    102import org.jfree.chart.axis.ValueAxis;
    103import org.jfree.chart.entity.EntityCollection;
    104import org.jfree.chart.event.RendererChangeEvent;
    105import org.jfree.chart.labels.CategoryItemLabelGenerator;
    106import org.jfree.chart.labels.ItemLabelAnchor;
    107import org.jfree.chart.labels.ItemLabelPosition;
    108import org.jfree.chart.plot.CategoryPlot;
    109import org.jfree.chart.plot.PlotOrientation;
    110import org.jfree.data.DataUtilities;
    111import org.jfree.data.Range;
    112import org.jfree.data.category.CategoryDataset;
    113import org.jfree.data.general.DatasetUtilities;
    114import org.jfree.ui.RectangleEdge;
    115import org.jfree.ui.TextAnchor;
    116import 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 */
    127public 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}