    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     * StackedXYAreaRenderer2.java
    029     * ---------------------------
    030     * (C) Copyright 2004-2007, 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     *
    054     */
    055
    056    package org.jfree.chart.renderer.xy;
    057
    058    import java.awt.Graphics2D;
    059    import java.awt.Paint;
    060    import java.awt.Shape;
    061    import java.awt.geom.GeneralPath;
    062    import java.awt.geom.Rectangle2D;
    063    import java.io.Serializable;
    064
    065    import org.jfree.chart.axis.ValueAxis;
    066    import org.jfree.chart.entity.EntityCollection;
    067    import org.jfree.chart.event.RendererChangeEvent;
    068    import org.jfree.chart.labels.XYToolTipGenerator;
    069    import org.jfree.chart.plot.CrosshairState;
    070    import org.jfree.chart.plot.PlotRenderingInfo;
    071    import org.jfree.chart.plot.XYPlot;
    072    import org.jfree.chart.urls.XYURLGenerator;
    073    import org.jfree.data.Range;
    074    import org.jfree.data.xy.TableXYDataset;
    075    import org.jfree.data.xy.XYDataset;
    076    import org.jfree.ui.RectangleEdge;
    077    import org.jfree.util.PublicCloneable;
    078
    079    /**
    080     * A stacked area renderer for the {@link XYPlot} class.
    081     */
    082    public class StackedXYAreaRenderer2 extends XYAreaRenderer2
    083                                        implements Cloneable,
    084                                                   PublicCloneable,
    085                                                   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
    228            // get the data point...
    229            double x1 = dataset.getXValue(series, item);
    230            double y1 = dataset.getYValue(series, item);
    231            if (Double.isNaN(y1)) {
    232                y1 = 0.0;
    233            }
    234            double[] stack1 = getStackValues(tdataset, series, item);
    235
    236            // get the previous point and the next point so we can calculate a
    237            // "hot spot" for the area (used by the chart entity)...
    238            double x0 = dataset.getXValue(series, Math.max(item - 1, 0));
    239            double y0 = dataset.getYValue(series, Math.max(item - 1, 0));
    240            if (Double.isNaN(y0)) {
    241                y0 = 0.0;
    242            }
    243            double[] stack0 = getStackValues(tdataset, series, Math.max(item - 1,
    244                    0));
    245
    246            int itemCount = dataset.getItemCount(series);
    247            double x2 = dataset.getXValue(series, Math.min(item + 1,
    248                    itemCount - 1));
    249            double y2 = dataset.getYValue(series, Math.min(item + 1,
    250                    itemCount - 1));
    251            if (Double.isNaN(y2)) {
    252                y2 = 0.0;
    253            }
    254            double[] stack2 = getStackValues(tdataset, series, Math.min(item + 1,
    255                    itemCount - 1));
    256
    257            double xleft = (x0 + x1) / 2.0;
    258            double xright = (x1 + x2) / 2.0;
    259            double[] stackLeft = averageStackValues(stack0, stack1);
    260            double[] stackRight = averageStackValues(stack1, stack2);
    261            double[] adjStackLeft = adjustedStackValues(stack0, stack1);
    262            double[] adjStackRight = adjustedStackValues(stack1, stack2);
    263
    264            RectangleEdge edge0 = plot.getDomainAxisEdge();
    265
    266            float transX1 = (float) domainAxis.valueToJava2D(x1, dataArea, edge0);
    267            float transXLeft = (float) domainAxis.valueToJava2D(xleft, dataArea,
    268                    edge0);
    269            float transXRight = (float) domainAxis.valueToJava2D(xright, dataArea,
    270                    edge0);
    271
    272            if (this.roundXCoordinates) {
    273                transX1 = Math.round(transX1);
    274                transXLeft = Math.round(transXLeft);
    275                transXRight = Math.round(transXRight);
    276            }
    277            float transY1;
    278
    279            RectangleEdge edge1 = plot.getRangeAxisEdge();
    280
    281            GeneralPath left = new GeneralPath();
    282            GeneralPath right = new GeneralPath();
    283            if (y1 >= 0.0) {  // handle positive value
    284                transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1], dataArea,
    285                        edge1);
    286                float transStack1 = (float) rangeAxis.valueToJava2D(stack1[1],
    287                        dataArea, edge1);
    288                float transStackLeft = (float) rangeAxis.valueToJava2D(
    289                        adjStackLeft[1], dataArea, edge1);
    290
    291                // LEFT POLYGON
    292                if (y0 >= 0.0) {
    293                    double yleft = (y0 + y1) / 2.0 + stackLeft[1];
    294                    float transYLeft
    295                        = (float) rangeAxis.valueToJava2D(yleft, dataArea, edge1);
    296                    left.moveTo(transX1, transY1);
    297                    left.lineTo(transX1, transStack1);
    298                    left.lineTo(transXLeft, transStackLeft);
    299                    left.lineTo(transXLeft, transYLeft);
    300                    left.closePath();
    301                }
    302                else {
    303                    left.moveTo(transX1, transStack1);
    304                    left.lineTo(transX1, transY1);
    305                    left.lineTo(transXLeft, transStackLeft);
    306                    left.closePath();
    307                }
    308
    309                float transStackRight = (float) rangeAxis.valueToJava2D(
    310                        adjStackRight[1], dataArea, edge1);
    311                // RIGHT POLYGON
    312                if (y2 >= 0.0) {
    313                    double yright = (y1 + y2) / 2.0 + stackRight[1];
    314                    float transYRight
    315                        = (float) rangeAxis.valueToJava2D(yright, dataArea, edge1);
    316                    right.moveTo(transX1, transStack1);
    317                    right.lineTo(transX1, transY1);
    318                    right.lineTo(transXRight, transYRight);
    319                    right.lineTo(transXRight, transStackRight);
    320                    right.closePath();
    321                }
    322                else {
    323                    right.moveTo(transX1, transStack1);
    324                    right.lineTo(transX1, transY1);
    325                    right.lineTo(transXRight, transStackRight);
    326                    right.closePath();
    327                }
    328            }
    329            else {  // handle negative value
    330                transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0], dataArea,
    331                        edge1);
    332                float transStack1 = (float) rangeAxis.valueToJava2D(stack1[0],
    333                        dataArea, edge1);
    334                float transStackLeft = (float) rangeAxis.valueToJava2D(
    335                        adjStackLeft[0], dataArea, edge1);
    336
    337                // LEFT POLYGON
    338                if (y0 >= 0.0) {
    339                    left.moveTo(transX1, transStack1);
    340                    left.lineTo(transX1, transY1);
    341                    left.lineTo(transXLeft, transStackLeft);
    342                    left.clone();
    343                }
    344                else {
    345                    double yleft = (y0 + y1) / 2.0 + stackLeft[0];
    346                    float transYLeft = (float) rangeAxis.valueToJava2D(yleft,
    347                            dataArea, edge1);
    348                    left.moveTo(transX1, transY1);
    349                    left.lineTo(transX1, transStack1);
    350                    left.lineTo(transXLeft, transStackLeft);
    351                    left.lineTo(transXLeft, transYLeft);
    352                    left.closePath();
    353                }
    354                float transStackRight = (float) rangeAxis.valueToJava2D(
    355                        adjStackRight[0], dataArea, edge1);
    356
    357                // RIGHT POLYGON
    358                if (y2 >= 0.0) {
    359                    right.moveTo(transX1, transStack1);
    360                    right.lineTo(transX1, transY1);
    361                    right.lineTo(transXRight, transStackRight);
    362                    right.closePath();
    363                }
    364                else {
    365                    double yright = (y1 + y2) / 2.0 + stackRight[0];
    366                    float transYRight = (float) rangeAxis.valueToJava2D(yright,
    367                            dataArea, edge1);
    368                    right.moveTo(transX1, transStack1);
    369                    right.lineTo(transX1, transY1);
    370                    right.lineTo(transXRight, transYRight);
    371                    right.lineTo(transXRight, transStackRight);
    372                    right.closePath();
    373                }
    374            }
    375
    376            //  Get series Paint and Stroke
    377            Paint itemPaint = getItemPaint(series, item);
    378            if (pass == 0) {
    379                g2.setPaint(itemPaint);
    380                g2.fill(left);
    381                g2.fill(right);
    382            }
    383
    384            // add an entity for the item...
    385            if (entities != null) {
    386                GeneralPath gp = new GeneralPath(left);
    387                gp.append(right, false);
    388                entityArea = gp;
    389                addEntity(entities, entityArea, dataset, series, item,
    390                        transX1, transY1);
    391            }
    392
    393        }
    394
    395        /**
    396         * Calculates the stacked values (one positive and one negative) of all
    397         * series up to, but not including, <code>series</code> for the specified
    398         * item. It returns [0.0, 0.0] if <code>series</code> is the first series.
    399         *
    400         * @param dataset  the dataset (<code>null</code> not permitted).
    401         * @param series  the series index.
    402         * @param index  the item index.
    403         *
    404         * @return An array containing the cumulative negative and positive values
    405         *     for all series values up to but excluding <code>series</code>
    406         *     for <code>index</code>.
    407         */
    408        private double[] getStackValues(TableXYDataset dataset,
    409                                        int series, int index) {
    410            double[] result = new double[2];
    411            for (int i = 0; i < series; i++) {
    412                double v = dataset.getYValue(i, index);
    413                if (!Double.isNaN(v)) {
    414                    if (v >= 0.0) {
    415                        result[1] += v;
    416                    }
    417                    else {
    418                        result[0] += v;
    419                    }
    420                }
    421            }
    422            return result;
    423        }
    424
    425        /**
    426         * Returns a pair of "stack" values calculated as the mean of the two
    427         * specified stack value pairs.
    428         *
    429         * @param stack1  the first stack pair.
    430         * @param stack2  the second stack pair.
    431         *
    432         * @return A pair of average stack values.
    433         */
    434        private double[] averageStackValues(double[] stack1, double[] stack2) {
    435            double[] result = new double[2];
    436            result[0] = (stack1[0] + stack2[0]) / 2.0;
    437            result[1] = (stack1[1] + stack2[1]) / 2.0;
    438            return result;
    439        }
    440
    441        /**
    442         * Calculates adjusted stack values from the supplied values.  The value is
    443         * the mean of the supplied values, unless either of the supplied values
    444         * is zero, in which case the adjusted value is zero also.
    445         *
    446         * @param stack1  the first stack pair.
    447         * @param stack2  the second stack pair.
    448         *
    449         * @return A pair of average stack values.
    450         */
    451        private double[] adjustedStackValues(double[] stack1, double[] stack2) {
    452            double[] result = new double[2];
    453            if (stack1[0] == 0.0 || stack2[0] == 0.0) {
    454                result[0] = 0.0;
    455            }
    456            else {
    457                result[0] = (stack1[0] + stack2[0]) / 2.0;
    458            }
    459            if (stack1[1] == 0.0 || stack2[1] == 0.0) {
    460                result[1] = 0.0;
    461            }
    462            else {
    463                result[1] = (stack1[1] + stack2[1]) / 2.0;
    464            }
    465            return result;
    466        }
    467
    468        /**
    469         * Tests this renderer for equality with an arbitrary object.
    470         *
    471         * @param obj  the object (<code>null</code> permitted).
    472         *
    473         * @return A boolean.
    474         */
    475        public boolean equals(Object obj) {
    476            if (obj == this) {
    477                return true;
    478            }
    479            if (!(obj instanceof StackedXYAreaRenderer2)) {
    480                return false;
    481            }
    482            StackedXYAreaRenderer2 that = (StackedXYAreaRenderer2) obj;
    483            if (this.roundXCoordinates != that.roundXCoordinates) {
    484                return false;
    485            }
    486            return super.equals(obj);
    487        }
    488
    489        /**
    490         * Returns a clone of the renderer.
    491         *
    492         * @return A clone.
    493         *
    494         * @throws CloneNotSupportedException  if the renderer cannot be cloned.
    495         */
    496        public Object clone() throws CloneNotSupportedException {
    497            return super.clone();
    498        }
    499
    500    }