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