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