    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
    056    package org.jfree.chart.renderer.xy;
    057
    058    import java.awt.Graphics2D;
    059    import java.awt.geom.Rectangle2D;
    060
    061    import org.jfree.chart.axis.ValueAxis;
    062    import org.jfree.chart.entity.EntityCollection;
    063    import org.jfree.chart.event.RendererChangeEvent;
    064    import org.jfree.chart.labels.ItemLabelAnchor;
    065    import org.jfree.chart.labels.ItemLabelPosition;
    066    import org.jfree.chart.labels.XYItemLabelGenerator;
    067    import org.jfree.chart.plot.CrosshairState;
    068    import org.jfree.chart.plot.PlotOrientation;
    069    import org.jfree.chart.plot.PlotRenderingInfo;
    070    import org.jfree.chart.plot.XYPlot;
    071    import org.jfree.data.Range;
    072    import org.jfree.data.general.DatasetUtilities;
    073    import org.jfree.data.xy.IntervalXYDataset;
    074    import org.jfree.data.xy.TableXYDataset;
    075    import org.jfree.data.xy.XYDataset;
    076    import org.jfree.ui.RectangleEdge;
    077    import 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     */
    095    public 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    }