001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2013, 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    @Override
317    public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea,
318            XYPlot plot, XYDataset data, PlotRenderingInfo info) {
319
320        XYItemRendererState state = super.initialise(g2, dataArea, plot, data,
321                info);
322        // disable visible items optimisation - it doesn't work for this
323        // renderer...
324        state.setProcessVisibleItemsOnly(false);
325        return state;
326
327    }
328
329
330    /**
331     * Draws the visual representation of a single data item.
332     *
333     * @param g2  the graphics device.
334     * @param state  the renderer state.
335     * @param dataArea  the area within which the data is being drawn.
336     * @param info  collects information about the drawing.
337     * @param plot  the plot (can be used to obtain standard color information
338     *              etc).
339     * @param domainAxis  the domain axis.
340     * @param rangeAxis  the range axis.
341     * @param dataset  the dataset.
342     * @param series  the series index (zero-based).
343     * @param item  the item index (zero-based).
344     * @param crosshairState  crosshair information for the plot
345     *                        (<code>null</code> permitted).
346     * @param pass  the pass index.
347     */
348    @Override
349    public void drawItem(Graphics2D g2, XYItemRendererState state, 
350            Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
351            ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset,
352            int series, int item, CrosshairState crosshairState, int pass) {
353
354        PlotOrientation orientation = plot.getOrientation();
355
356        // Get the item count for the series, so that we can know which is the
357        // end of the series.
358        int itemCount = dataset.getItemCount(series);
359
360        Paint paint = getItemPaint(series, item);
361        Stroke seriesStroke = getItemStroke(series, item);
362        g2.setPaint(paint);
363        g2.setStroke(seriesStroke);
364
365        // get the data point...
366        double x1 = dataset.getXValue(series, item);
367        double y1 = dataset.getYValue(series, item);
368        double x = x1;
369        double y = Double.isNaN(y1) ? getRangeBase() : y1;
370        double transX1 = domainAxis.valueToJava2D(x, dataArea,
371                plot.getDomainAxisEdge());
372        double transY1 = rangeAxis.valueToJava2D(y, dataArea,
373                plot.getRangeAxisEdge());
374
375        // avoid possible sun.dc.pr.PRException: endPath: bad path
376        transY1 = restrictValueToDataArea(transY1, plot, dataArea);
377
378        if (this.pArea == null && !Double.isNaN(y1)) {
379
380            // Create a new Area for the series
381            this.pArea = new Polygon();
382
383            // start from Y = rangeBase
384            double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea,
385                    plot.getRangeAxisEdge());
386
387            // avoid possible sun.dc.pr.PRException: endPath: bad path
388            transY2 = restrictValueToDataArea(transY2, plot, dataArea);
389
390            // The first point is (x, this.baseYValue)
391            if (orientation == PlotOrientation.VERTICAL) {
392                this.pArea.addPoint((int) transX1, (int) transY2);
393            }
394            else if (orientation == PlotOrientation.HORIZONTAL) {
395                this.pArea.addPoint((int) transY2, (int) transX1);
396            }
397        }
398
399        double transX0;
400        double transY0;
401
402        double x0;
403        double y0;
404        if (item > 0) {
405            // get the previous data point...
406            x0 = dataset.getXValue(series, item - 1);
407            y0 = Double.isNaN(y1) ? y1 : dataset.getYValue(series, item - 1);
408
409            x = x0;
410            y = Double.isNaN(y0) ? getRangeBase() : y0;
411            transX0 = domainAxis.valueToJava2D(x, dataArea,
412                    plot.getDomainAxisEdge());
413            transY0 = rangeAxis.valueToJava2D(y, dataArea,
414                    plot.getRangeAxisEdge());
415
416            // avoid possible sun.dc.pr.PRException: endPath: bad path
417            transY0 = restrictValueToDataArea(transY0, plot, dataArea);
418
419            if (Double.isNaN(y1)) {
420                // NULL value -> insert point on base line
421                // instead of 'step point'
422                transX1 = transX0;
423                transY0 = transY1;
424            }
425            if (transY0 != transY1) {
426                // not just a horizontal bar but need to perform a 'step'.
427                if (orientation == PlotOrientation.VERTICAL) {
428                    this.pArea.addPoint((int) transX1, (int) transY0);
429                }
430                else if (orientation == PlotOrientation.HORIZONTAL) {
431                    this.pArea.addPoint((int) transY0, (int) transX1);
432                }
433            }
434        }
435
436        Shape shape = null;
437        if (!Double.isNaN(y1)) {
438            // Add each point to Area (x, y)
439            if (orientation == PlotOrientation.VERTICAL) {
440                this.pArea.addPoint((int) transX1, (int) transY1);
441            }
442            else if (orientation == PlotOrientation.HORIZONTAL) {
443                this.pArea.addPoint((int) transY1, (int) transX1);
444            }
445
446            if (getShapesVisible()) {
447                shape = getItemShape(series, item);
448                if (orientation == PlotOrientation.VERTICAL) {
449                    shape = ShapeUtilities.createTranslatedShape(shape,
450                            transX1, transY1);
451                }
452                else if (orientation == PlotOrientation.HORIZONTAL) {
453                    shape = ShapeUtilities.createTranslatedShape(shape,
454                            transY1, transX1);
455                }
456                if (isShapesFilled()) {
457                    g2.fill(shape);
458                }
459                else {
460                    g2.draw(shape);
461                }
462            }
463            else {
464                if (orientation == PlotOrientation.VERTICAL) {
465                    shape = new Rectangle2D.Double(transX1 - 2, transY1 - 2,
466                            4.0, 4.0);
467                }
468                else if (orientation == PlotOrientation.HORIZONTAL) {
469                    shape = new Rectangle2D.Double(transY1 - 2, transX1 - 2,
470                            4.0, 4.0);
471                }
472            }
473        }
474
475        // Check if the item is the last item for the series or if it
476        // is a NULL value and number of items > 0.  We can't draw an area for
477        // a single point.
478        if (getPlotArea() && item > 0 && this.pArea != null
479                          && (item == (itemCount - 1) || Double.isNaN(y1))) {
480
481            double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea,
482                    plot.getRangeAxisEdge());
483
484            // avoid possible sun.dc.pr.PRException: endPath: bad path
485            transY2 = restrictValueToDataArea(transY2, plot, dataArea);
486
487            if (orientation == PlotOrientation.VERTICAL) {
488                // Add the last point (x,0)
489                this.pArea.addPoint((int) transX1, (int) transY2);
490            }
491            else if (orientation == PlotOrientation.HORIZONTAL) {
492                // Add the last point (x,0)
493                this.pArea.addPoint((int) transY2, (int) transX1);
494            }
495
496            // fill the polygon
497            g2.fill(this.pArea);
498
499            // draw an outline around the Area.
500            if (isOutline()) {
501                g2.setStroke(plot.getOutlineStroke());
502                g2.setPaint(plot.getOutlinePaint());
503                g2.draw(this.pArea);
504            }
505
506            // start new area when needed (see above)
507            this.pArea = null;
508        }
509
510        // do we need to update the crosshair values?
511        if (!Double.isNaN(y1)) {
512            int domainAxisIndex = plot.getDomainAxisIndex(domainAxis);
513            int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis);
514            updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex,
515                    rangeAxisIndex, transX1, transY1, orientation);
516        }
517
518        // collect entity and tool tip information...
519        EntityCollection entities = state.getEntityCollection();
520        if (entities != null) {
521            addEntity(entities, shape, dataset, series, item, transX1, transY1);
522        }
523    }
524
525    /**
526     * Tests this renderer for equality with an arbitrary object.
527     *
528     * @param obj  the object (<code>null</code> permitted).
529     *
530     * @return A boolean.
531     */
532    @Override
533    public boolean equals(Object obj) {
534        if (obj == this) {
535            return true;
536        }
537        if (!(obj instanceof XYStepAreaRenderer)) {
538            return false;
539        }
540        XYStepAreaRenderer that = (XYStepAreaRenderer) obj;
541        if (this.showOutline != that.showOutline) {
542            return false;
543        }
544        if (this.shapesVisible != that.shapesVisible) {
545            return false;
546        }
547        if (this.shapesFilled != that.shapesFilled) {
548            return false;
549        }
550        if (this.plotArea != that.plotArea) {
551            return false;
552        }
553        if (this.rangeBase != that.rangeBase) {
554            return false;
555        }
556        return super.equals(obj);
557    }
558
559    /**
560     * Returns a clone of the renderer.
561     *
562     * @return A clone.
563     *
564     * @throws CloneNotSupportedException  if the renderer cannot be cloned.
565     */
566    @Override
567    public Object clone() throws CloneNotSupportedException {
568        return super.clone();
569    }
570
571    /**
572     * Helper method which returns a value if it lies
573     * inside the visible dataArea and otherwise the corresponding
574     * coordinate on the border of the dataArea. The PlotOrientation
575     * is taken into account.
576     * Useful to avoid possible sun.dc.pr.PRException: endPath: bad path
577     * which occurs when trying to draw lines/shapes which in large part
578     * lie outside of the visible dataArea.
579     *
580     * @param value the value which shall be
581     * @param dataArea  the area within which the data is being drawn.
582     * @param plot  the plot (can be used to obtain standard color
583     *              information etc).
584     * @return <code>double</code> value inside the data area.
585     */
586    protected static double restrictValueToDataArea(double value,
587                                                    XYPlot plot,
588                                                    Rectangle2D dataArea) {
589        double min = 0;
590        double max = 0;
591        if (plot.getOrientation() == PlotOrientation.VERTICAL) {
592            min = dataArea.getMinY();
593            max = dataArea.getMaxY();
594        }
595        else if (plot.getOrientation() ==  PlotOrientation.HORIZONTAL) {
596            min = dataArea.getMinX();
597            max = dataArea.getMaxX();
598        }
599        if (value < min) {
600            value = min;
601        }
602        else if (value > max) {
603            value = max;
604        }
605        return value;
606    }
607
608}