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