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