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