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