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