1 /* ===========================================================
2  * JFreeChart : a free chart library for the Java(tm) platform
3  * ===========================================================
4  *
5  * (C) Copyright 2000-2013, by Object Refinery Limited and Contributors.
6  *
7  * Project Info:  http://www.jfree.org/jfreechart/index.html
8  *
9  * This library is free software; you can redistribute it and/or modify it
10  * under the terms of the GNU Lesser General Public License as published by
11  * the Free Software Foundation; either version 2.1 of the License, or
12  * (at your option) any later version.
13  *
14  * This library is distributed in the hope that it will be useful, but
15  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
17  * License for more details.
18  *
19  * You should have received a copy of the GNU Lesser General Public
20  * License along with this library; if not, write to the Free Software
21  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
22  * USA.
23  *
24  * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
25  * Other names may be trademarks of their respective owners.]
26  *
27  * -----------------------
28  * XYStepAreaRenderer.java
29  * -----------------------
30  * (C) Copyright 2003-2009, by Matthias Rose and Contributors.
31  *
32  * Original Author:  Matthias Rose (based on XYAreaRenderer.java);
33  * Contributor(s):   David Gilbert (for Object Refinery Limited);
34  *
35  * Changes:
36  * --------
37  * 07-Oct-2003 : Version 1, contributed by Matthias Rose (DG);
38  * 10-Feb-2004 : Added some getter and setter methods (DG);
39  * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState.  Renamed
40  *               XYToolTipGenerator --> XYItemLabelGenerator (DG);
41  * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
42  *               getYValue() (DG);
43  * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG);
44  * 06-Jul-2005 : Renamed get/setPlotShapes() --> get/setShapesVisible() (DG);
45  * ------------- JFREECHART 1.0.x ---------------------------------------------
46  * 06-Jul-2006 : Modified to call dataset methods that return double
47  *               primitives only (DG);
48  * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG);
49  * 14-Feb-2007 : Added equals() method override (DG);
50  * 04-May-2007 : Set processVisibleItemsOnly flag to false (DG);
51  * 14-May-2008 : Call addEntity() from within drawItem() (DG);
52  * 19-May-2009 : Fixed FindBugs warnings, patch by Michal Wozniak (DG);
53  *
54  */
55 
56 package org.jfree.chart.renderer.xy;
57 
58 import java.awt.Graphics2D;
59 import java.awt.Paint;
60 import java.awt.Polygon;
61 import java.awt.Shape;
62 import java.awt.Stroke;
63 import java.awt.geom.Rectangle2D;
64 import java.io.Serializable;
65 
66 import org.jfree.chart.axis.ValueAxis;
67 import org.jfree.chart.entity.EntityCollection;
68 import org.jfree.chart.event.RendererChangeEvent;
69 import org.jfree.chart.labels.XYToolTipGenerator;
70 import org.jfree.chart.plot.CrosshairState;
71 import org.jfree.chart.plot.PlotOrientation;
72 import org.jfree.chart.plot.PlotRenderingInfo;
73 import org.jfree.chart.plot.XYPlot;
74 import org.jfree.chart.urls.XYURLGenerator;
75 import org.jfree.data.xy.XYDataset;
76 import org.jfree.util.PublicCloneable;
77 import org.jfree.util.ShapeUtilities;
78 
79 /**
80  * A step chart renderer that fills the area between the step and the x-axis.
81  * The example shown here is generated by the
82  * <code>XYStepAreaRendererDemo1.java</code> program included in the JFreeChart
83  * demo collection:
84  * <br><br>
85  * <img src="../../../../../images/XYStepAreaRendererSample.png"
86  * alt="XYStepAreaRendererSample.png" />
87  */
88 public class XYStepAreaRenderer extends AbstractXYItemRenderer
89         implements XYItemRenderer, Cloneable, PublicCloneable, Serializable {
90 
91     /** For serialization. */
92     private static final long serialVersionUID = -7311560779702649635L;
93 
94     /** Useful constant for specifying the type of rendering (shapes only). */
95     public static final int SHAPES = 1;
96 
97     /** Useful constant for specifying the type of rendering (area only). */
98     public static final int AREA = 2;
99 
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      */
XYStepAreaRenderer()129     public XYStepAreaRenderer() {
130         this(AREA);
131     }
132 
133     /**
134      * Constructs a new renderer.
135      *
136      * @param type  the type of the renderer.
137      */
XYStepAreaRenderer(int type)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      */
XYStepAreaRenderer(int type, XYToolTipGenerator toolTipGenerator, XYURLGenerator urlGenerator)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      */
isOutline()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      */
setOutline(boolean show)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      */
getShapesVisible()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      */
setShapesVisible(boolean flag)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      */
isShapesFilled()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      */
setShapesFilled(boolean filled)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      */
getPlotArea()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      */
setPlotArea(boolean flag)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      */
getRangeBase()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      */
setRangeBase(double val)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
initialise(Graphics2D g2, Rectangle2D dataArea, XYPlot plot, XYDataset data, PlotRenderingInfo info)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
drawItem(Graphics2D g2, XYItemRendererState state, Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, int series, int item, CrosshairState crosshairState, int pass)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
equals(Object obj)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
clone()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      */
restrictValueToDataArea(double value, XYPlot plot, Rectangle2D dataArea)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 }
609