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