    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     * XYStepAreaRenderer.java
    029     * -----------------------
    030     * (C) Copyright 2003-2008, by Matthias Rose and Contributors.
    031     *
    032     * Original Author:  Matthias Rose (based on XYAreaRenderer.java);
    033     * Contributor(s):   David Gilbert (for Object Refinery Limited);
    034     *
    035     * Changes:
    036     * --------
    037     * 07-Oct-2003 : Version 1, contributed by Matthias Rose (DG);
    038     * 10-Feb-2004 : Added some getter and setter methods (DG);
    039     * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState.  Renamed
    040     *               XYToolTipGenerator --> XYItemLabelGenerator (DG);
    041     * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
    042     *               getYValue() (DG);
    043     * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG);
    044     * 06-Jul-2005 : Renamed get/setPlotShapes() --> get/setShapesVisible() (DG);
    045     * ------------- JFREECHART 1.0.x ---------------------------------------------
    046     * 06-Jul-2006 : Modified to call dataset methods that return double
    047     *               primitives only (DG);
    048     * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG);
    049     * 14-Feb-2007 : Added equals() method override (DG);
    050     * 04-May-2007 : Set processVisibleItemsOnly flag to false (DG);
    051     * 14-May-2008 : Call addEntity() from within drawItem() (DG);
    052     *
    053     */
    054
    055    package org.jfree.chart.renderer.xy;
    056
    057    import java.awt.Graphics2D;
    058    import java.awt.Paint;
    059    import java.awt.Polygon;
    060    import java.awt.Shape;
    061    import java.awt.Stroke;
    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.PlotOrientation;
    071    import org.jfree.chart.plot.PlotRenderingInfo;
    072    import org.jfree.chart.plot.XYPlot;
    073    import org.jfree.chart.urls.XYURLGenerator;
    074    import org.jfree.data.xy.XYDataset;
    075    import org.jfree.util.PublicCloneable;
    076    import org.jfree.util.ShapeUtilities;
    077
    078    /**
    079     * A step chart renderer that fills the area between the step and the x-axis.
    080     */
    081    public class XYStepAreaRenderer extends AbstractXYItemRenderer
    082            implements XYItemRenderer, Cloneable, PublicCloneable, Serializable {
    083
    084        /** For serialization. */
    085        private static final long serialVersionUID = -7311560779702649635L;
    086
    087        /** Useful constant for specifying the type of rendering (shapes only). */
    088        public static final int SHAPES = 1;
    089
    090        /** Useful constant for specifying the type of rendering (area only). */
    091        public static final int AREA = 2;
    092
    093        /**
    094         * Useful constant for specifying the type of rendering (area and shapes).
    095         */
    096        public static final int AREA_AND_SHAPES = 3;
    097
    098        /** A flag indicating whether or not shapes are drawn at each XY point. */
    099        private boolean shapesVisible;
    100
    101        /** A flag that controls whether or not shapes are filled for ALL series. */
    102        private boolean shapesFilled;
    103
    104        /** A flag indicating whether or not Area are drawn at each XY point. */
    105        private boolean plotArea;
    106
    107        /** A flag that controls whether or not the outline is shown. */
    108        private boolean showOutline;
    109
    110        /** Area of the complete series */
    111        protected transient Polygon pArea = null;
    112
    113        /**
    114         * The value on the range axis which defines the 'lower' border of the
    115         * area.
    116         */
    117        private double rangeBase;
    118
    119        /**
    120         * Constructs a new renderer.
    121         */
    122        public XYStepAreaRenderer() {
    123            this(AREA);
    124        }
    125
    126        /**
    127         * Constructs a new renderer.
    128         *
    129         * @param type  the type of the renderer.
    130         */
    131        public XYStepAreaRenderer(int type) {
    132            this(type, null, null);
    133        }
    134
    135        /**
    136         * Constructs a new renderer.
    137         * <p>
    138         * To specify the type of renderer, use one of the constants:
    139         * AREA, SHAPES or AREA_AND_SHAPES.
    140         *
    141         * @param type  the type of renderer.
    142         * @param toolTipGenerator  the tool tip generator to use
    143         *                          (<code>null</code> permitted).
    144         * @param urlGenerator  the URL generator (<code>null</code> permitted).
    145         */
    146        public XYStepAreaRenderer(int type,
    147                                  XYToolTipGenerator toolTipGenerator,
    148                                  XYURLGenerator urlGenerator) {
    149
    150            super();
    151            setBaseToolTipGenerator(toolTipGenerator);
    152            setURLGenerator(urlGenerator);
    153
    154            if (type == AREA) {
    155                this.plotArea = true;
    156            }
    157            else if (type == SHAPES) {
    158                this.shapesVisible = true;
    159            }
    160            else if (type == AREA_AND_SHAPES) {
    161                this.plotArea = true;
    162                this.shapesVisible = true;
    163            }
    164            this.showOutline = false;
    165        }
    166
    167        /**
    168         * Returns a flag that controls whether or not outlines of the areas are
    169         * drawn.
    170         *
    171         * @return The flag.
    172         *
    173         * @see #setOutline(boolean)
    174         */
    175        public boolean isOutline() {
    176            return this.showOutline;
    177        }
    178
    179        /**
    180         * Sets a flag that controls whether or not outlines of the areas are
    181         * drawn, and sends a {@link RendererChangeEvent} to all registered
    182         * listeners.
    183         *
    184         * @param show  the flag.
    185         *
    186         * @see #isOutline()
    187         */
    188        public void setOutline(boolean show) {
    189            this.showOutline = show;
    190            fireChangeEvent();
    191        }
    192
    193        /**
    194         * Returns true if shapes are being plotted by the renderer.
    195         *
    196         * @return <code>true</code> if shapes are being plotted by the renderer.
    197         *
    198         * @see #setShapesVisible(boolean)
    199         */
    200        public boolean getShapesVisible() {
    201            return this.shapesVisible;
    202        }
    203
    204        /**
    205         * Sets the flag that controls whether or not shapes are displayed for each
    206         * data item, and sends a {@link RendererChangeEvent} to all registered
    207         * listeners.
    208         *
    209         * @param flag  the flag.
    210         *
    211         * @see #getShapesVisible()
    212         */
    213        public void setShapesVisible(boolean flag) {
    214            this.shapesVisible = flag;
    215            fireChangeEvent();
    216        }
    217
    218        /**
    219         * Returns the flag that controls whether or not the shapes are filled.
    220         *
    221         * @return A boolean.
    222         *
    223         * @see #setShapesFilled(boolean)
    224         */
    225        public boolean isShapesFilled() {
    226            return this.shapesFilled;
    227        }
    228
    229        /**
    230         * Sets the 'shapes filled' for ALL series and sends a
    231         * {@link RendererChangeEvent} to all registered listeners.
    232         *
    233         * @param filled  the flag.
    234         *
    235         * @see #isShapesFilled()
    236         */
    237        public void setShapesFilled(boolean filled) {
    238            this.shapesFilled = filled;
    239            fireChangeEvent();
    240        }
    241
    242        /**
    243         * Returns true if Area is being plotted by the renderer.
    244         *
    245         * @return <code>true</code> if Area is being plotted by the renderer.
    246         *
    247         * @see #setPlotArea(boolean)
    248         */
    249        public boolean getPlotArea() {
    250            return this.plotArea;
    251        }
    252
    253        /**
    254         * Sets a flag that controls whether or not areas are drawn for each data
    255         * item and sends a {@link RendererChangeEvent} to all registered
    256         * listeners.
    257         *
    258         * @param flag  the flag.
    259         *
    260         * @see #getPlotArea()
    261         */
    262        public void setPlotArea(boolean flag) {
    263            this.plotArea = flag;
    264            fireChangeEvent();
    265        }
    266
    267        /**
    268         * Returns the value on the range axis which defines the 'lower' border of
    269         * the area.
    270         *
    271         * @return <code>double</code> the value on the range axis which defines
    272         *         the 'lower' border of the area.
    273         *
    274         * @see #setRangeBase(double)
    275         */
    276        public double getRangeBase() {
    277            return this.rangeBase;
    278        }
    279
    280        /**
    281         * Sets the value on the range axis which defines the default border of the
    282         * area, and sends a {@link RendererChangeEvent} to all registered
    283         * listeners.  E.g. setRangeBase(Double.NEGATIVE_INFINITY) lets areas always
    284         * reach the lower border of the plotArea.
    285         *
    286         * @param val  the value on the range axis which defines the default border
    287         *             of the area.
    288         *
    289         * @see #getRangeBase()
    290         */
    291        public void setRangeBase(double val) {
    292            this.rangeBase = val;
    293            fireChangeEvent();
    294        }
    295
    296        /**
    297         * Initialises the renderer.  Here we calculate the Java2D y-coordinate for
    298         * zero, since all the bars have their bases fixed at zero.
    299         *
    300         * @param g2  the graphics device.
    301         * @param dataArea  the area inside the axes.
    302         * @param plot  the plot.
    303         * @param data  the data.
    304         * @param info  an optional info collection object to return data back to
    305         *              the caller.
    306         *
    307         * @return The number of passes required by the renderer.
    308         */
    309        public XYItemRendererState initialise(Graphics2D g2,
    310                                              Rectangle2D dataArea,
    311                                              XYPlot plot,
    312                                              XYDataset data,
    313                                              PlotRenderingInfo info) {
    314
    315
    316            XYItemRendererState state = super.initialise(g2, dataArea, plot, data,
    317                    info);
    318            // disable visible items optimisation - it doesn't work for this
    319            // renderer...
    320            state.setProcessVisibleItemsOnly(false);
    321            return state;
    322
    323        }
    324
    325
    326        /**
    327         * Draws the visual representation of a single data item.
    328         *
    329         * @param g2  the graphics device.
    330         * @param state  the renderer state.
    331         * @param dataArea  the area within which the data is being drawn.
    332         * @param info  collects information about the drawing.
    333         * @param plot  the plot (can be used to obtain standard color information
    334         *              etc).
    335         * @param domainAxis  the domain axis.
    336         * @param rangeAxis  the range axis.
    337         * @param dataset  the dataset.
    338         * @param series  the series index (zero-based).
    339         * @param item  the item index (zero-based).
    340         * @param crosshairState  crosshair information for the plot
    341         *                        (<code>null</code> permitted).
    342         * @param pass  the pass index.
    343         */
    344        public void drawItem(Graphics2D g2,
    345                             XYItemRendererState state,
    346                             Rectangle2D dataArea,
    347                             PlotRenderingInfo info,
    348                             XYPlot plot,
    349                             ValueAxis domainAxis,
    350                             ValueAxis rangeAxis,
    351                             XYDataset dataset,
    352                             int series,
    353                             int item,
    354                             CrosshairState crosshairState,
    355                             int pass) {
    356
    357            PlotOrientation orientation = plot.getOrientation();
    358
    359            // Get the item count for the series, so that we can know which is the
    360            // end of the series.
    361            int itemCount = dataset.getItemCount(series);
    362
    363            Paint paint = getItemPaint(series, item);
    364            Stroke seriesStroke = getItemStroke(series, item);
    365            g2.setPaint(paint);
    366            g2.setStroke(seriesStroke);
    367
    368            // get the data point...
    369            double x1 = dataset.getXValue(series, item);
    370            double y1 = dataset.getYValue(series, item);
    371            double x = x1;
    372            double y = Double.isNaN(y1) ? getRangeBase() : y1;
    373            double transX1 = domainAxis.valueToJava2D(x, dataArea,
    374                    plot.getDomainAxisEdge());
    375            double transY1 = rangeAxis.valueToJava2D(y, dataArea,
    376                    plot.getRangeAxisEdge());
    377
    378            // avoid possible sun.dc.pr.PRException: endPath: bad path
    379            transY1 = restrictValueToDataArea(transY1, plot, dataArea);
    380
    381            if (this.pArea == null && !Double.isNaN(y1)) {
    382
    383                // Create a new Area for the series
    384                this.pArea = new Polygon();
    385
    386                // start from Y = rangeBase
    387                double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea,
    388                        plot.getRangeAxisEdge());
    389
    390                // avoid possible sun.dc.pr.PRException: endPath: bad path
    391                transY2 = restrictValueToDataArea(transY2, plot, dataArea);
    392
    393                // The first point is (x, this.baseYValue)
    394                if (orientation == PlotOrientation.VERTICAL) {
    395                    this.pArea.addPoint((int) transX1, (int) transY2);
    396                }
    397                else if (orientation == PlotOrientation.HORIZONTAL) {
    398                    this.pArea.addPoint((int) transY2, (int) transX1);
    399                }
    400            }
    401
    402            double transX0 = 0;
    403            double transY0 = restrictValueToDataArea(getRangeBase(), plot,
    404                    dataArea);
    405
    406            double x0;
    407            double y0;
    408            if (item > 0) {
    409                // get the previous data point...
    410                x0 = dataset.getXValue(series, item - 1);
    411                y0 = Double.isNaN(y1) ? y1 : dataset.getYValue(series, item - 1);
    412
    413                x = x0;
    414                y = Double.isNaN(y0) ? getRangeBase() : y0;
    415                transX0 = domainAxis.valueToJava2D(x, dataArea,
    416                        plot.getDomainAxisEdge());
    417                transY0 = rangeAxis.valueToJava2D(y, dataArea,
    418                        plot.getRangeAxisEdge());
    419
    420                // avoid possible sun.dc.pr.PRException: endPath: bad path
    421                transY0 = restrictValueToDataArea(transY0, plot, dataArea);
    422
    423                if (Double.isNaN(y1)) {
    424                    // NULL value -> insert point on base line
    425                    // instead of 'step point'
    426                    transX1 = transX0;
    427                    transY0 = transY1;
    428                }
    429                if (transY0 != transY1) {
    430                    // not just a horizontal bar but need to perform a 'step'.
    431                    if (orientation == PlotOrientation.VERTICAL) {
    432                        this.pArea.addPoint((int) transX1, (int) transY0);
    433                    }
    434                    else if (orientation == PlotOrientation.HORIZONTAL) {
    435                        this.pArea.addPoint((int) transY0, (int) transX1);
    436                    }
    437                }
    438            }
    439
    440            Shape shape = null;
    441            if (!Double.isNaN(y1)) {
    442                // Add each point to Area (x, y)
    443                if (orientation == PlotOrientation.VERTICAL) {
    444                    this.pArea.addPoint((int) transX1, (int) transY1);
    445                }
    446                else if (orientation == PlotOrientation.HORIZONTAL) {
    447                    this.pArea.addPoint((int) transY1, (int) transX1);
    448                }
    449
    450                if (getShapesVisible()) {
    451                    shape = getItemShape(series, item);
    452                    if (orientation == PlotOrientation.VERTICAL) {
    453                        shape = ShapeUtilities.createTranslatedShape(shape,
    454                                transX1, transY1);
    455                    }
    456                    else if (orientation == PlotOrientation.HORIZONTAL) {
    457                        shape = ShapeUtilities.createTranslatedShape(shape,
    458                                transY1, transX1);
    459                    }
    460                    if (isShapesFilled()) {
    461                        g2.fill(shape);
    462                    }
    463                    else {
    464                        g2.draw(shape);
    465                    }
    466                }
    467                else {
    468                    if (orientation == PlotOrientation.VERTICAL) {
    469                        shape = new Rectangle2D.Double(transX1 - 2, transY1 - 2,
    470                                4.0, 4.0);
    471                    }
    472                    else if (orientation == PlotOrientation.HORIZONTAL) {
    473                        shape = new Rectangle2D.Double(transY1 - 2, transX1 - 2,
    474                                4.0, 4.0);
    475                    }
    476                }
    477            }
    478
    479            // Check if the item is the last item for the series or if it
    480            // is a NULL value and number of items > 0.  We can't draw an area for
    481            // a single point.
    482            if (getPlotArea() && item > 0 && this.pArea != null
    483                              && (item == (itemCount - 1) || Double.isNaN(y1))) {
    484
    485                double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea,
    486                        plot.getRangeAxisEdge());
    487
    488                // avoid possible sun.dc.pr.PRException: endPath: bad path
    489                transY2 = restrictValueToDataArea(transY2, plot, dataArea);
    490
    491                if (orientation == PlotOrientation.VERTICAL) {
    492                    // Add the last point (x,0)
    493                    this.pArea.addPoint((int) transX1, (int) transY2);
    494                }
    495                else if (orientation == PlotOrientation.HORIZONTAL) {
    496                    // Add the last point (x,0)
    497                    this.pArea.addPoint((int) transY2, (int) transX1);
    498                }
    499
    500                // fill the polygon
    501                g2.fill(this.pArea);
    502
    503                // draw an outline around the Area.
    504                if (isOutline()) {
    505                    g2.setStroke(plot.getOutlineStroke());
    506                    g2.setPaint(plot.getOutlinePaint());
    507                    g2.draw(this.pArea);
    508                }
    509
    510                // start new area when needed (see above)
    511                this.pArea = null;
    512            }
    513
    514            // do we need to update the crosshair values?
    515            if (!Double.isNaN(y1)) {
    516                int domainAxisIndex = plot.getDomainAxisIndex(domainAxis);
    517                int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis);
    518                updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex,
    519                        rangeAxisIndex, transX1, transY1, orientation);
    520            }
    521
    522            // collect entity and tool tip information...
    523            EntityCollection entities = state.getEntityCollection();
    524            if (entities != null) {
    525                addEntity(entities, shape, dataset, series, item, transX1, transY1);
    526            }
    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 XYStepAreaRenderer)) {
    541                return false;
    542            }
    543            XYStepAreaRenderer that = (XYStepAreaRenderer) obj;
    544            if (this.showOutline != that.showOutline) {
    545                return false;
    546            }
    547            if (this.shapesVisible != that.shapesVisible) {
    548                return false;
    549            }
    550            if (this.shapesFilled != that.shapesFilled) {
    551                return false;
    552            }
    553            if (this.plotArea != that.plotArea) {
    554                return false;
    555            }
    556            if (this.rangeBase != that.rangeBase) {
    557                return false;
    558            }
    559            return super.equals(obj);
    560        }
    561
    562        /**
    563         * Returns a clone of the renderer.
    564         *
    565         * @return A clone.
    566         *
    567         * @throws CloneNotSupportedException  if the renderer cannot be cloned.
    568         */
    569        public Object clone() throws CloneNotSupportedException {
    570            return super.clone();
    571        }
    572
    573        /**
    574         * Helper method which returns a value if it lies
    575         * inside the visible dataArea and otherwise the corresponding
    576         * coordinate on the border of the dataArea. The PlotOrientation
    577         * is taken into account.
    578         * Useful to avoid possible sun.dc.pr.PRException: endPath: bad path
    579         * which occurs when trying to draw lines/shapes which in large part
    580         * lie outside of the visible dataArea.
    581         *
    582         * @param value the value which shall be
    583         * @param dataArea  the area within which the data is being drawn.
    584         * @param plot  the plot (can be used to obtain standard color
    585         *              information etc).
    586         * @return <code>double</code> value inside the data area.
    587         */
    588        protected static double restrictValueToDataArea(double value,
    589                                                        XYPlot plot,
    590                                                        Rectangle2D dataArea) {
    591            double min = 0;
    592            double max = 0;
    593            if (plot.getOrientation() == PlotOrientation.VERTICAL) {
    594                min = dataArea.getMinY();
    595                max = dataArea.getMaxY();
    596            }
    597            else if (plot.getOrientation() ==  PlotOrientation.HORIZONTAL) {
    598                min = dataArea.getMinX();
    599                max = dataArea.getMaxX();
    600            }
    601            if (value < min) {
    602                value = min;
    603            }
    604            else if (value > max) {
    605                value = max;
    606            }
    607            return value;
    608        }
    609
    610    }