    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 * StackedXYBarRenderer.java
    029 * -------------------------
    030 * (C) Copyright 2004-2011, by Andreas Schroeder and Contributors.
    031 *
    032 * Original Author:  Andreas Schroeder;
    033 * Contributor(s):   David Gilbert (for Object Refinery Limited);
    034 *
    035 * Changes
    036 * -------
    037 * 01-Apr-2004 : Version 1 (AS);
    038 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
    039 *               getYValue() (DG);
    040 * 15-Aug-2004 : Added drawBarOutline to control draw/don't-draw bar
    041 *               outlines (BN);
    042 * 10-Sep-2004 : drawBarOutline attribute is now inherited from XYBarRenderer
    043 *               and double primitives are retrieved from the dataset rather
    044 *               than Number objects (DG);
    045 * 07-Jan-2005 : Updated for method name change in DatasetUtilities (DG);
    046 * 25-Jan-2005 : Modified to handle negative values correctly (DG);
    047 * ------------- JFREECHART 1.0.x ---------------------------------------------
    048 * 06-Dec-2006 : Added support for GradientPaint (DG);
    049 * 15-Mar-2007 : Added renderAsPercentages option (DG);
    050 * 24-Jun-2008 : Added new barPainter mechanism (DG);
    051 * 23-Sep-2008 : Check shadow visibility before drawing shadow (DG);
    052 * 28-May-2009 : Fixed bar positioning with inverted domain axis (DG);
    053 * 07-Act-2011 : Fix for Bug #3035289: Patch #3035325 (MH);
    054 */
    055
    056package org.jfree.chart.renderer.xy;
    057
    058import java.awt.Graphics2D;
    059import java.awt.geom.Rectangle2D;
    060
    061import org.jfree.chart.axis.ValueAxis;
    062import org.jfree.chart.entity.EntityCollection;
    063import org.jfree.chart.event.RendererChangeEvent;
    064import org.jfree.chart.labels.ItemLabelAnchor;
    065import org.jfree.chart.labels.ItemLabelPosition;
    066import org.jfree.chart.labels.XYItemLabelGenerator;
    067import org.jfree.chart.plot.CrosshairState;
    068import org.jfree.chart.plot.PlotOrientation;
    069import org.jfree.chart.plot.PlotRenderingInfo;
    070import org.jfree.chart.plot.XYPlot;
    071import org.jfree.data.Range;
    072import org.jfree.data.general.DatasetUtilities;
    073import org.jfree.data.xy.IntervalXYDataset;
    074import org.jfree.data.xy.TableXYDataset;
    075import org.jfree.data.xy.XYDataset;
    076import org.jfree.ui.RectangleEdge;
    077import org.jfree.ui.TextAnchor;
    078
    079/**
    080 * A bar renderer that displays the series items stacked.
    081 * The dataset used together with this renderer must be a
    082 * {@link org.jfree.data.xy.IntervalXYDataset} and a
    083 * {@link org.jfree.data.xy.TableXYDataset}. For example, the
    084 * dataset class {@link org.jfree.data.xy.CategoryTableXYDataset}
    085 * implements both interfaces.
    086 *
    087 * The example shown here is generated by the
    088 * <code>StackedXYBarChartDemo2.java</code> program included in the
    089 * JFreeChart demo collection:
    090 * <br><br>
    091 * <img src="../../../../../images/StackedXYBarRendererSample.png"
    092 * alt="StackedXYBarRendererSample.png" />
    093
    094 */
    095public class StackedXYBarRenderer extends XYBarRenderer {
    096
    097    /** For serialization. */
    098    private static final long serialVersionUID = -7049101055533436444L;
    099
    100    /** A flag that controls whether the bars display values or percentages. */
    101    private boolean renderAsPercentages;
    102
    103    /**
    104     * Creates a new renderer.
    105     */
    106    public StackedXYBarRenderer() {
    107        this(0.0);
    108    }
    109
    110    /**
    111     * Creates a new renderer.
    112     *
    113     * @param margin  the percentual amount of the bars that are cut away.
    114     */
    115    public StackedXYBarRenderer(double margin) {
    116        super(margin);
    117        this.renderAsPercentages = false;
    118
    119        // set the default item label positions, which will only be used if
    120        // the user requests visible item labels...
    121        ItemLabelPosition p = new ItemLabelPosition(ItemLabelAnchor.CENTER,
    122                TextAnchor.CENTER);
    123        setBasePositiveItemLabelPosition(p);
    124        setBaseNegativeItemLabelPosition(p);
    125        setPositiveItemLabelPositionFallback(null);
    126        setNegativeItemLabelPositionFallback(null);
    127    }
    128
    129    /**
    130     * Returns <code>true</code> if the renderer displays each item value as
    131     * a percentage (so that the stacked bars add to 100%), and
    132     * <code>false</code> otherwise.
    133     *
    134     * @return A boolean.
    135     *
    136     * @see #setRenderAsPercentages(boolean)
    137     *
    138     * @since 1.0.5
    139     */
    140    public boolean getRenderAsPercentages() {
    141        return this.renderAsPercentages;
    142    }
    143
    144    /**
    145     * Sets the flag that controls whether the renderer displays each item
    146     * value as a percentage (so that the stacked bars add to 100%), and sends
    147     * a {@link RendererChangeEvent} to all registered listeners.
    148     *
    149     * @param asPercentages  the flag.
    150     *
    151     * @see #getRenderAsPercentages()
    152     *
    153     * @since 1.0.5
    154     */
    155    public void setRenderAsPercentages(boolean asPercentages) {
    156        this.renderAsPercentages = asPercentages;
    157        fireChangeEvent();
    158    }
    159
    160    /**
    161     * Returns <code>3</code> to indicate that this renderer requires three
    162     * passes for drawing (shadows are drawn in the first pass, the bars in the
    163     * second, and item labels are drawn in the third pass so that
    164     * they always appear in front of all the bars).
    165     *
    166     * @return <code>2</code>.
    167     */
    168    public int getPassCount() {
    169        return 3;
    170    }
    171
    172    /**
    173     * Initialises the renderer and returns a state object that should be
    174     * passed to all subsequent calls to the drawItem() method. Here there is
    175     * nothing to do.
    176     *
    177     * @param g2  the graphics device.
    178     * @param dataArea  the area inside the axes.
    179     * @param plot  the plot.
    180     * @param data  the data.
    181     * @param info  an optional info collection object to return data back to
    182     *              the caller.
    183     *
    184     * @return A state object.
    185     */
    186    public XYItemRendererState initialise(Graphics2D g2,
    187                                          Rectangle2D dataArea,
    188                                          XYPlot plot,
    189                                          XYDataset data,
    190                                          PlotRenderingInfo info) {
    191        return new XYBarRendererState(info);
    192    }
    193
    194    /**
    195     * Returns the range of values the renderer requires to display all the
    196     * items from the specified dataset.
    197     *
    198     * @param dataset  the dataset (<code>null</code> permitted).
    199     *
    200     * @return The range (<code>null</code> if the dataset is <code>null</code>
    201     *         or empty).
    202     */
    203    public Range findRangeBounds(XYDataset dataset) {
    204        if (dataset != null) {
    205            if (this.renderAsPercentages) {
    206                return new Range(0.0, 1.0);
    207            }
    208            else {
    209                return DatasetUtilities.findStackedRangeBounds(
    210                        (TableXYDataset) dataset);
    211            }
    212        }
    213        else {
    214            return null;
    215        }
    216    }
    217
    218    /**
    219     * Draws the visual representation of a single data item.
    220     *
    221     * @param g2  the graphics device.
    222     * @param state  the renderer state.
    223     * @param dataArea  the area within which the plot is being drawn.
    224     * @param info  collects information about the drawing.
    225     * @param plot  the plot (can be used to obtain standard color information
    226     *              etc).
    227     * @param domainAxis  the domain axis.
    228     * @param rangeAxis  the range axis.
    229     * @param dataset  the dataset.
    230     * @param series  the series index (zero-based).
    231     * @param item  the item index (zero-based).
    232     * @param crosshairState  crosshair information for the plot
    233     *                        (<code>null</code> permitted).
    234     * @param pass  the pass index.
    235     */
    236    public void drawItem(Graphics2D g2,
    237                         XYItemRendererState state,
    238                         Rectangle2D dataArea,
    239                         PlotRenderingInfo info,
    240                         XYPlot plot,
    241                         ValueAxis domainAxis,
    242                         ValueAxis rangeAxis,
    243                         XYDataset dataset,
    244                         int series,
    245                         int item,
    246                         CrosshairState crosshairState,
    247                         int pass) {
    248
    249        if (!getItemVisible(series, item)) {
    250            return;
    251        }
    252
    253        if (!(dataset instanceof IntervalXYDataset
    254                && dataset instanceof TableXYDataset)) {
    255            String message = "dataset (type " + dataset.getClass().getName()
    256                + ") has wrong type:";
    257            boolean and = false;
    258            if (!IntervalXYDataset.class.isAssignableFrom(dataset.getClass())) {
    259                message += " it is no IntervalXYDataset";
    260                and = true;
    261            }
    262            if (!TableXYDataset.class.isAssignableFrom(dataset.getClass())) {
    263                if (and) {
    264                    message += " and";
    265                }
    266                message += " it is no TableXYDataset";
    267            }
    268
    269            throw new IllegalArgumentException(message);
    270        }
    271
    272        IntervalXYDataset intervalDataset = (IntervalXYDataset) dataset;
    273        double value = intervalDataset.getYValue(series, item);
    274        if (Double.isNaN(value)) {
    275            return;
    276        }
    277
    278        // if we are rendering the values as percentages, we need to calculate
    279        // the total for the current item.  Unfortunately here we end up
    280        // repeating the calculation more times than is strictly necessary -
    281        // hopefully I'll come back to this and find a way to add the
    282        // total(s) to the renderer state.  The other problem is we implicitly
    283        // assume the dataset has no negative values...perhaps that can be
    284        // fixed too.
    285        double total = 0.0;
    286        if (this.renderAsPercentages) {
    287            total = DatasetUtilities.calculateStackTotal(
    288                    (TableXYDataset) dataset, item);
    289            value = value / total;
    290        }
    291
    292        double positiveBase = 0.0;
    293        double negativeBase = 0.0;
    294
    295        for (int i = 0; i < series; i++) {
    296            double v = dataset.getYValue(i, item);
    297            if (!Double.isNaN(v) && isSeriesVisible(i)) {
    298                if (this.renderAsPercentages) {
    299                    v = v / total;
    300                }
    301                if (v > 0) {
    302                    positiveBase = positiveBase + v;
    303                }
    304                else {
    305                    negativeBase = negativeBase + v;
    306                }
    307            }
    308        }
    309
    310        double translatedBase;
    311        double translatedValue;
    312        RectangleEdge edgeR = plot.getRangeAxisEdge();
    313        if (value > 0.0) {
    314            translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea,
    315                    edgeR);
    316            translatedValue = rangeAxis.valueToJava2D(positiveBase + value,
    317                    dataArea, edgeR);
    318        }
    319        else {
    320            translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea,
    321                    edgeR);
    322            translatedValue = rangeAxis.valueToJava2D(negativeBase + value,
    323                    dataArea, edgeR);
    324        }
    325
    326        RectangleEdge edgeD = plot.getDomainAxisEdge();
    327        double startX = intervalDataset.getStartXValue(series, item);
    328        if (Double.isNaN(startX)) {
    329            return;
    330        }
    331        double translatedStartX = domainAxis.valueToJava2D(startX, dataArea,
    332                edgeD);
    333
    334        double endX = intervalDataset.getEndXValue(series, item);
    335        if (Double.isNaN(endX)) {
    336            return;
    337        }
    338        double translatedEndX = domainAxis.valueToJava2D(endX, dataArea, edgeD);
    339
    340        double translatedWidth = Math.max(1, Math.abs(translatedEndX
    341                - translatedStartX));
    342        double translatedHeight = Math.abs(translatedValue - translatedBase);
    343        if (getMargin() > 0.0) {
    344            double cut = translatedWidth * getMargin();
    345            translatedWidth = translatedWidth - cut;
    346            translatedStartX = translatedStartX + cut / 2;
    347        }
    348
    349        Rectangle2D bar = null;
    350        PlotOrientation orientation = plot.getOrientation();
    351        if (orientation == PlotOrientation.HORIZONTAL) {
    352            bar = new Rectangle2D.Double(Math.min(translatedBase,
    353                    translatedValue), Math.min(translatedEndX,
    354                    translatedStartX), translatedHeight, translatedWidth);
    355        }
    356        else if (orientation == PlotOrientation.VERTICAL) {
    357            bar = new Rectangle2D.Double(Math.min(translatedStartX,
    358                    translatedEndX), Math.min(translatedBase, translatedValue),
    359                    translatedWidth, translatedHeight);
    360        }
    361        boolean positive = (value > 0.0);
    362        boolean inverted = rangeAxis.isInverted();
    363        RectangleEdge barBase;
    364        if (orientation == PlotOrientation.HORIZONTAL) {
    365            if (positive && inverted || !positive && !inverted) {
    366                barBase = RectangleEdge.RIGHT;
    367            }
    368            else {
    369                barBase = RectangleEdge.LEFT;
    370            }
    371        }
    372        else {
    373            if (positive && !inverted || !positive && inverted) {
    374                barBase = RectangleEdge.BOTTOM;
    375            }
    376            else {
    377                barBase = RectangleEdge.TOP;
    378            }
    379        }
    380
    381        if (pass == 0) {
    382            if (getShadowsVisible()) {
    383                getBarPainter().paintBarShadow(g2, this, series, item, bar,
    384                        barBase, false);
    385            }
    386        }
    387        else if (pass == 1) {
    388            getBarPainter().paintBar(g2, this, series, item, bar, barBase);
    389
    390            // add an entity for the item...
    391            if (info != null) {
    392                EntityCollection entities = info.getOwner()
    393                        .getEntityCollection();
    394                if (entities != null) {
    395                    addEntity(entities, bar, dataset, series, item,
    396                            bar.getCenterX(), bar.getCenterY());
    397                }
    398            }
    399        }
    400        else if (pass == 2) {
    401            // handle item label drawing, now that we know all the bars have
    402            // been drawn...
    403            if (isItemLabelVisible(series, item)) {
    404                XYItemLabelGenerator generator = getItemLabelGenerator(series,
    405                        item);
    406                drawItemLabel(g2, dataset, series, item, plot, generator, bar,
    407                        value < 0.0);
    408            }
    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 StackedXYBarRenderer)) {
    425            return false;
    426        }
    427        StackedXYBarRenderer that = (StackedXYBarRenderer) obj;
    428        if (this.renderAsPercentages != that.renderAsPercentages) {
    429            return false;
    430        }
    431        return super.equals(obj);
    432    }
    433
    434    /**
    435     * Returns a hash code for this instance.
    436     *
    437     * @return A hash code.
    438     */
    439    public int hashCode() {
    440        int result = super.hashCode();
    441        result = result * 37 + (this.renderAsPercentages ? 1 : 0);
    442        return result;
    443    }
    444
    445}