    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     * StackedXYAreaRenderer2.java
    029     * ---------------------------
    030     * (C) Copyright 2004-2008, by Object Refinery Limited and Contributors.
    031     *
    032     * Original Author:  David Gilbert (for Object Refinery Limited), based on
    033     *                   the StackedXYAreaRenderer class by Richard Atkinson;
    034     * Contributor(s):   -;
    035     *
    036     * Changes:
    037     * --------
    038     * 30-Apr-2004 : Version 1 (DG);
    039     * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
    040     *               getYValue() (DG);
    041     * 10-Sep-2004 : Removed getRangeType() method (DG);
    042     * 06-Jan-2004 : Renamed getRangeExtent() --> findRangeBounds (DG);
    043     * 28-Mar-2005 : Use getXValue() and getYValue() from dataset (DG);
    044     * 03-Oct-2005 : Add entity generation to drawItem() method (DG);
    045     * ------------- JFREECHART 1.0.x ---------------------------------------------
    046     * 22-Aug-2006 : Handle null and empty datasets correctly in the
    047     *               findRangeBounds() method (DG);
    048     * 22-Sep-2006 : Added a flag to allow rounding of x-coordinates (after
    049     *               translation to Java2D space) in order to avoid the striping
    050     *               that can result from anti-aliasing (thanks to Doug
    051     *               Clayton) (DG);
    052     * 30-Nov-2006 : Added accessor methods for the roundXCoordinates flag (DG);
    053     * 02-Jun-2008 : Fixed bug with PlotOrientation.HORIZONTAL (DG);
    054     *
    055     */
    056
    057    package org.jfree.chart.renderer.xy;
    058
    059    import java.awt.Graphics2D;
    060    import java.awt.Paint;
    061    import java.awt.Shape;
    062    import java.awt.geom.GeneralPath;
    063    import java.awt.geom.Rectangle2D;
    064    import java.io.Serializable;
    065
    066    import org.jfree.chart.axis.ValueAxis;
    067    import org.jfree.chart.entity.EntityCollection;
    068    import org.jfree.chart.event.RendererChangeEvent;
    069    import org.jfree.chart.labels.XYToolTipGenerator;
    070    import org.jfree.chart.plot.CrosshairState;
    071    import org.jfree.chart.plot.PlotOrientation;
    072    import org.jfree.chart.plot.PlotRenderingInfo;
    073    import org.jfree.chart.plot.XYPlot;
    074    import org.jfree.chart.urls.XYURLGenerator;
    075    import org.jfree.data.Range;
    076    import org.jfree.data.xy.TableXYDataset;
    077    import org.jfree.data.xy.XYDataset;
    078    import org.jfree.ui.RectangleEdge;
    079    import org.jfree.util.PublicCloneable;
    080
    081    /**
    082     * A stacked area renderer for the {@link XYPlot} class.
    083     */
    084    public class StackedXYAreaRenderer2 extends XYAreaRenderer2
    085            implements Cloneable, PublicCloneable, Serializable {
    086
    087        /** For serialization. */
    088        private static final long serialVersionUID = 7752676509764539182L;
    089
    090        /**
    091         * This flag controls whether or not the x-coordinates (in Java2D space)
    092         * are rounded to integers.  When set to true, this can avoid the vertical
    093         * striping that anti-aliasing can generate.  However, the rounding may not
    094         * be appropriate for output in high resolution formats (for example,
    095         * vector graphics formats such as SVG and PDF).
    096         *
    097         * @since 1.0.3
    098         */
    099        private boolean roundXCoordinates;
    100
    101        /**
    102         * Creates a new renderer.
    103         */
    104        public StackedXYAreaRenderer2() {
    105            this(null, null);
    106        }
    107
    108        /**
    109         * Constructs a new renderer.
    110         *
    111         * @param labelGenerator  the tool tip generator to use.  <code>null</code>
    112         *                        is none.
    113         * @param urlGenerator  the URL generator (<code>null</code> permitted).
    114         */
    115        public StackedXYAreaRenderer2(XYToolTipGenerator labelGenerator,
    116                                      XYURLGenerator urlGenerator) {
    117            super(labelGenerator, urlGenerator);
    118            this.roundXCoordinates = true;
    119        }
    120
    121        /**
    122         * Returns the flag that controls whether or not the x-coordinates (in
    123         * Java2D space) are rounded to integer values.
    124         *
    125         * @return The flag.
    126         *
    127         * @since 1.0.4
    128         *
    129         * @see #setRoundXCoordinates(boolean)
    130         */
    131        public boolean getRoundXCoordinates() {
    132            return this.roundXCoordinates;
    133        }
    134
    135        /**
    136         * Sets the flag that controls whether or not the x-coordinates (in
    137         * Java2D space) are rounded to integer values, and sends a
    138         * {@link RendererChangeEvent} to all registered listeners.
    139         *
    140         * @param round  the new flag value.
    141         *
    142         * @since 1.0.4
    143         *
    144         * @see #getRoundXCoordinates()
    145         */
    146        public void setRoundXCoordinates(boolean round) {
    147            this.roundXCoordinates = round;
    148            fireChangeEvent();
    149        }
    150
    151        /**
    152         * Returns the range of values the renderer requires to display all the
    153         * items from the specified dataset.
    154         *
    155         * @param dataset  the dataset (<code>null</code> permitted).
    156         *
    157         * @return The range (or <code>null</code> if the dataset is
    158         *         <code>null</code> or empty).
    159         */
    160        public Range findRangeBounds(XYDataset dataset) {
    161            if (dataset == null) {
    162                return null;
    163            }
    164            double min = Double.POSITIVE_INFINITY;
    165            double max = Double.NEGATIVE_INFINITY;
    166            TableXYDataset d = (TableXYDataset) dataset;
    167            int itemCount = d.getItemCount();
    168            for (int i = 0; i < itemCount; i++) {
    169                double[] stackValues = getStackValues((TableXYDataset) dataset,
    170                        d.getSeriesCount(), i);
    171                min = Math.min(min, stackValues[0]);
    172                max = Math.max(max, stackValues[1]);
    173            }
    174            if (min == Double.POSITIVE_INFINITY) {
    175                return null;
    176            }
    177            return new Range(min, max);
    178        }
    179
    180        /**
    181         * Returns the number of passes required by the renderer.
    182         *
    183         * @return 1.
    184         */
    185        public int getPassCount() {
    186            return 1;
    187        }
    188
    189        /**
    190         * Draws the visual representation of a single data item.
    191         *
    192         * @param g2  the graphics device.
    193         * @param state  the renderer state.
    194         * @param dataArea  the area within which the data is being drawn.
    195         * @param info  collects information about the drawing.
    196         * @param plot  the plot (can be used to obtain standard color information
    197         *              etc).
    198         * @param domainAxis  the domain axis.
    199         * @param rangeAxis  the range axis.
    200         * @param dataset  the dataset.
    201         * @param series  the series index (zero-based).
    202         * @param item  the item index (zero-based).
    203         * @param crosshairState  information about crosshairs on a plot.
    204         * @param pass  the pass index.
    205         */
    206        public void drawItem(Graphics2D g2,
    207                             XYItemRendererState state,
    208                             Rectangle2D dataArea,
    209                             PlotRenderingInfo info,
    210                             XYPlot plot,
    211                             ValueAxis domainAxis,
    212                             ValueAxis rangeAxis,
    213                             XYDataset dataset,
    214                             int series,
    215                             int item,
    216                             CrosshairState crosshairState,
    217                             int pass) {
    218
    219            // setup for collecting optional entity info...
    220            Shape entityArea = null;
    221            EntityCollection entities = null;
    222            if (info != null) {
    223                entities = info.getOwner().getEntityCollection();
    224            }
    225
    226            TableXYDataset tdataset = (TableXYDataset) dataset;
    227            PlotOrientation orientation = plot.getOrientation();
    228
    229            // get the data point...
    230            double x1 = dataset.getXValue(series, item);
    231            double y1 = dataset.getYValue(series, item);
    232            if (Double.isNaN(y1)) {
    233                y1 = 0.0;
    234            }
    235            double[] stack1 = getStackValues(tdataset, series, item);
    236
    237            // get the previous point and the next point so we can calculate a
    238            // "hot spot" for the area (used by the chart entity)...
    239            double x0 = dataset.getXValue(series, Math.max(item - 1, 0));
    240            double y0 = dataset.getYValue(series, Math.max(item - 1, 0));
    241            if (Double.isNaN(y0)) {
    242                y0 = 0.0;
    243            }
    244            double[] stack0 = getStackValues(tdataset, series, Math.max(item - 1,
    245                    0));
    246
    247            int itemCount = dataset.getItemCount(series);
    248            double x2 = dataset.getXValue(series, Math.min(item + 1,
    249                    itemCount - 1));
    250            double y2 = dataset.getYValue(series, Math.min(item + 1,
    251                    itemCount - 1));
    252            if (Double.isNaN(y2)) {
    253                y2 = 0.0;
    254            }
    255            double[] stack2 = getStackValues(tdataset, series, Math.min(item + 1,
    256                    itemCount - 1));
    257
    258            double xleft = (x0 + x1) / 2.0;
    259            double xright = (x1 + x2) / 2.0;
    260            double[] stackLeft = averageStackValues(stack0, stack1);
    261            double[] stackRight = averageStackValues(stack1, stack2);
    262            double[] adjStackLeft = adjustedStackValues(stack0, stack1);
    263            double[] adjStackRight = adjustedStackValues(stack1, stack2);
    264
    265            RectangleEdge edge0 = plot.getDomainAxisEdge();
    266
    267            float transX1 = (float) domainAxis.valueToJava2D(x1, dataArea, edge0);
    268            float transXLeft = (float) domainAxis.valueToJava2D(xleft, dataArea,
    269                    edge0);
    270            float transXRight = (float) domainAxis.valueToJava2D(xright, dataArea,
    271                    edge0);
    272
    273            if (this.roundXCoordinates) {
    274                transX1 = Math.round(transX1);
    275                transXLeft = Math.round(transXLeft);
    276                transXRight = Math.round(transXRight);
    277            }
    278            float transY1;
    279
    280            RectangleEdge edge1 = plot.getRangeAxisEdge();
    281
    282            GeneralPath left = new GeneralPath();
    283            GeneralPath right = new GeneralPath();
    284            if (y1 >= 0.0) {  // handle positive value
    285                transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1], dataArea,
    286                        edge1);
    287                float transStack1 = (float) rangeAxis.valueToJava2D(stack1[1],
    288                        dataArea, edge1);
    289                float transStackLeft = (float) rangeAxis.valueToJava2D(
    290                        adjStackLeft[1], dataArea, edge1);
    291
    292                // LEFT POLYGON
    293                if (y0 >= 0.0) {
    294                    double yleft = (y0 + y1) / 2.0 + stackLeft[1];
    295                    float transYLeft
    296                        = (float) rangeAxis.valueToJava2D(yleft, dataArea, edge1);
    297                    if (orientation == PlotOrientation.VERTICAL) {
    298                        left.moveTo(transX1, transY1);
    299                        left.lineTo(transX1, transStack1);
    300                        left.lineTo(transXLeft, transStackLeft);
    301                        left.lineTo(transXLeft, transYLeft);
    302                    }
    303                    else {
    304                            left.moveTo(transY1, transX1);
    305                            left.lineTo(transStack1, transX1);
    306                            left.lineTo(transStackLeft, transXLeft);
    307                            left.lineTo(transYLeft, transXLeft);
    308                    }
    309                    left.closePath();
    310                }
    311                else {
    312                    if (orientation == PlotOrientation.VERTICAL) {
    313                        left.moveTo(transX1, transStack1);
    314                        left.lineTo(transX1, transY1);
    315                        left.lineTo(transXLeft, transStackLeft);
    316                    }
    317                    else {
    318                        left.moveTo(transStack1, transX1);
    319                        left.lineTo(transY1, transX1);
    320                        left.lineTo(transStackLeft, transXLeft);
    321                    }
    322                    left.closePath();
    323                }
    324
    325                float transStackRight = (float) rangeAxis.valueToJava2D(
    326                        adjStackRight[1], dataArea, edge1);
    327                // RIGHT POLYGON
    328                if (y2 >= 0.0) {
    329                    double yright = (y1 + y2) / 2.0 + stackRight[1];
    330                    float transYRight
    331                        = (float) rangeAxis.valueToJava2D(yright, dataArea, edge1);
    332                    if (orientation == PlotOrientation.VERTICAL) {
    333                        right.moveTo(transX1, transStack1);
    334                        right.lineTo(transX1, transY1);
    335                        right.lineTo(transXRight, transYRight);
    336                        right.lineTo(transXRight, transStackRight);
    337                    }
    338                    else {
    339                        right.moveTo(transStack1, transX1);
    340                        right.lineTo(transY1, transX1);
    341                        right.lineTo(transYRight, transXRight);
    342                        right.lineTo(transStackRight, transXRight);
    343                    }
    344                    right.closePath();
    345                }
    346                else {
    347                    if (orientation == PlotOrientation.VERTICAL) {
    348                        right.moveTo(transX1, transStack1);
    349                        right.lineTo(transX1, transY1);
    350                        right.lineTo(transXRight, transStackRight);
    351                    }
    352                    else {
    353                        right.moveTo(transStack1, transX1);
    354                        right.lineTo(transY1, transX1);
    355                        right.lineTo(transStackRight, transXRight);
    356                    }
    357                    right.closePath();
    358                }
    359            }
    360            else {  // handle negative value
    361                transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0], dataArea,
    362                        edge1);
    363                float transStack1 = (float) rangeAxis.valueToJava2D(stack1[0],
    364                        dataArea, edge1);
    365                float transStackLeft = (float) rangeAxis.valueToJava2D(
    366                        adjStackLeft[0], dataArea, edge1);
    367
    368                // LEFT POLYGON
    369                if (y0 >= 0.0) {
    370                    if (orientation == PlotOrientation.VERTICAL) {
    371                        left.moveTo(transX1, transStack1);
    372                        left.lineTo(transX1, transY1);
    373                        left.lineTo(transXLeft, transStackLeft);
    374                    }
    375                    else {
    376                        left.moveTo(transStack1, transX1);
    377                        left.lineTo(transY1, transX1);
    378                        left.lineTo(transStackLeft, transXLeft);
    379                    }
    380                    left.clone();
    381                }
    382                else {
    383                    double yleft = (y0 + y1) / 2.0 + stackLeft[0];
    384                    float transYLeft = (float) rangeAxis.valueToJava2D(yleft,
    385                            dataArea, edge1);
    386                    if (orientation == PlotOrientation.VERTICAL) {
    387                        left.moveTo(transX1, transY1);
    388                        left.lineTo(transX1, transStack1);
    389                        left.lineTo(transXLeft, transStackLeft);
    390                        left.lineTo(transXLeft, transYLeft);
    391                    }
    392                    else {
    393                        left.moveTo(transY1, transX1);
    394                        left.lineTo(transStack1, transX1);
    395                        left.lineTo(transStackLeft, transXLeft);
    396                        left.lineTo(transYLeft, transXLeft);
    397                    }
    398                    left.closePath();
    399                }
    400                float transStackRight = (float) rangeAxis.valueToJava2D(
    401                        adjStackRight[0], dataArea, edge1);
    402
    403                // RIGHT POLYGON
    404                if (y2 >= 0.0) {
    405                    if (orientation == PlotOrientation.VERTICAL) {
    406                        right.moveTo(transX1, transStack1);
    407                        right.lineTo(transX1, transY1);
    408                        right.lineTo(transXRight, transStackRight);
    409                    }
    410                    else {
    411                        right.moveTo(transStack1, transX1);
    412                        right.lineTo(transY1, transX1);
    413                        right.lineTo(transStackRight, transXRight);
    414                    }
    415                    right.closePath();
    416                }
    417                else {
    418                    double yright = (y1 + y2) / 2.0 + stackRight[0];
    419                    float transYRight = (float) rangeAxis.valueToJava2D(yright,
    420                            dataArea, edge1);
    421                    if (orientation == PlotOrientation.VERTICAL) {
    422                        right.moveTo(transX1, transStack1);
    423                        right.lineTo(transX1, transY1);
    424                        right.lineTo(transXRight, transYRight);
    425                        right.lineTo(transXRight, transStackRight);
    426                    }
    427                    else {
    428                        right.moveTo(transStack1, transX1);
    429                        right.lineTo(transY1, transX1);
    430                        right.lineTo(transYRight, transXRight);
    431                        right.lineTo(transStackRight, transXRight);
    432                    }
    433                    right.closePath();
    434                }
    435            }
    436
    437            //  Get series Paint and Stroke
    438            Paint itemPaint = getItemPaint(series, item);
    439            if (pass == 0) {
    440                g2.setPaint(itemPaint);
    441                g2.fill(left);
    442                g2.fill(right);
    443            }
    444
    445            // add an entity for the item...
    446            if (entities != null) {
    447                GeneralPath gp = new GeneralPath(left);
    448                gp.append(right, false);
    449                entityArea = gp;
    450                addEntity(entities, entityArea, dataset, series, item,
    451                        transX1, transY1);
    452            }
    453
    454        }
    455
    456        /**
    457         * Calculates the stacked values (one positive and one negative) of all
    458         * series up to, but not including, <code>series</code> for the specified
    459         * item. It returns [0.0, 0.0] if <code>series</code> is the first series.
    460         *
    461         * @param dataset  the dataset (<code>null</code> not permitted).
    462         * @param series  the series index.
    463         * @param index  the item index.
    464         *
    465         * @return An array containing the cumulative negative and positive values
    466         *     for all series values up to but excluding <code>series</code>
    467         *     for <code>index</code>.
    468         */
    469        private double[] getStackValues(TableXYDataset dataset,
    470                                        int series, int index) {
    471            double[] result = new double[2];
    472            for (int i = 0; i < series; i++) {
    473                double v = dataset.getYValue(i, index);
    474                if (!Double.isNaN(v)) {
    475                    if (v >= 0.0) {
    476                        result[1] += v;
    477                    }
    478                    else {
    479                        result[0] += v;
    480                    }
    481                }
    482            }
    483            return result;
    484        }
    485
    486        /**
    487         * Returns a pair of "stack" values calculated as the mean of the two
    488         * specified stack value pairs.
    489         *
    490         * @param stack1  the first stack pair.
    491         * @param stack2  the second stack pair.
    492         *
    493         * @return A pair of average stack values.
    494         */
    495        private double[] averageStackValues(double[] stack1, double[] stack2) {
    496            double[] result = new double[2];
    497            result[0] = (stack1[0] + stack2[0]) / 2.0;
    498            result[1] = (stack1[1] + stack2[1]) / 2.0;
    499            return result;
    500        }
    501
    502        /**
    503         * Calculates adjusted stack values from the supplied values.  The value is
    504         * the mean of the supplied values, unless either of the supplied values
    505         * is zero, in which case the adjusted value is zero also.
    506         *
    507         * @param stack1  the first stack pair.
    508         * @param stack2  the second stack pair.
    509         *
    510         * @return A pair of average stack values.
    511         */
    512        private double[] adjustedStackValues(double[] stack1, double[] stack2) {
    513            double[] result = new double[2];
    514            if (stack1[0] == 0.0 || stack2[0] == 0.0) {
    515                result[0] = 0.0;
    516            }
    517            else {
    518                result[0] = (stack1[0] + stack2[0]) / 2.0;
    519            }
    520            if (stack1[1] == 0.0 || stack2[1] == 0.0) {
    521                result[1] = 0.0;
    522            }
    523            else {
    524                result[1] = (stack1[1] + stack2[1]) / 2.0;
    525            }
    526            return result;
    527        }
    528
    529        /**
    530         * Tests this renderer for equality with an arbitrary object.
    531         *
    532         * @param obj  the object (<code>null</code> permitted).
    533         *
    534         * @return A boolean.
    535         */
    536        public boolean equals(Object obj) {
    537            if (obj == this) {
    538                return true;
    539            }
    540            if (!(obj instanceof StackedXYAreaRenderer2)) {
    541                return false;
    542            }
    543            StackedXYAreaRenderer2 that = (StackedXYAreaRenderer2) obj;
    544            if (this.roundXCoordinates != that.roundXCoordinates) {
    545                return false;
    546            }
    547            return super.equals(obj);
    548        }
    549
    550        /**
    551         * Returns a clone of the renderer.
    552         *
    553         * @return A clone.
    554         *
    555         * @throws CloneNotSupportedException  if the renderer cannot be cloned.
    556         */
    557        public Object clone() throws CloneNotSupportedException {
    558            return super.clone();
    559        }
    560
    561    }