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