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