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