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