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