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