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.5 2007/02/06 16:29:11 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     *
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        public boolean isOutline() {
177            return this.showOutline;
178        }
179    
180        /**
181         * Sets a flag that controls whether or not outlines of the areas are 
182         * drawn, and sends a {@link RendererChangeEvent} to all registered 
183         * listeners.
184         *
185         * @param show  the flag.
186         */
187        public void setOutline(boolean show) {
188            this.showOutline = show;
189            notifyListeners(new RendererChangeEvent(this));
190        }
191    
192        /**
193         * Returns true if shapes are being plotted by the renderer.
194         *
195         * @return <code>true</code> if shapes are being plotted by the renderer.
196         */
197        public boolean getShapesVisible() {
198            return this.shapesVisible;
199        }
200        
201        /**
202         * Sets the flag that controls whether or not shapes are displayed for each 
203         * data item, and sends a {@link RendererChangeEvent} to all registered
204         * listeners.
205         * 
206         * @param flag  the flag.
207         */
208        public void setShapesVisible(boolean flag) {
209            this.shapesVisible = flag;
210            notifyListeners(new RendererChangeEvent(this));
211        }
212    
213        /**
214         * Returns the flag that controls whether or not the shapes are filled.
215         * 
216         * @return A boolean.
217         */
218        public boolean isShapesFilled() {
219            return this.shapesFilled;
220        }
221        
222        /**
223         * Sets the 'shapes filled' for ALL series.
224         *
225         * @param filled  the flag.
226         */
227        public void setShapesFilled(boolean filled) {
228            this.shapesFilled = filled;
229            notifyListeners(new RendererChangeEvent(this));
230        }
231    
232        /**
233         * Returns true if Area is being plotted by the renderer.
234         *
235         * @return <code>true</code> if Area is being plotted by the renderer.
236         */
237        public boolean getPlotArea() {
238            return this.plotArea;
239        }
240    
241        /**
242         * Sets a flag that controls whether or not areas are drawn for each data 
243         * item.
244         * 
245         * @param flag  the flag.
246         */
247        public void setPlotArea(boolean flag) {
248            this.plotArea = flag;
249            notifyListeners(new RendererChangeEvent(this));
250        }
251        
252        /**
253         * Returns the value on the range axis which defines the 'lower' border of
254         * the area.
255         *
256         * @return <code>double</code> the value on the range axis which defines 
257         *         the 'lower' border of the area.
258         */
259        public double getRangeBase() {
260            return this.rangeBase;
261        }
262    
263        /**
264         * Sets the value on the range axis which defines the default border of the 
265         * area.  E.g. setRangeBase(Double.NEGATIVE_INFINITY) lets areas always 
266         * reach the lower border of the plotArea. 
267         * 
268         * @param val  the value on the range axis which defines the default border
269         *             of the area.
270         */
271        public void setRangeBase(double val) {
272            this.rangeBase = val;
273            notifyListeners(new RendererChangeEvent(this));
274        }
275    
276        /**
277         * Initialises the renderer.  Here we calculate the Java2D y-coordinate for
278         * zero, since all the bars have their bases fixed at zero.
279         *
280         * @param g2  the graphics device.
281         * @param dataArea  the area inside the axes.
282         * @param plot  the plot.
283         * @param data  the data.
284         * @param info  an optional info collection object to return data back to 
285         *              the caller.
286         *
287         * @return The number of passes required by the renderer.
288         */
289        public XYItemRendererState initialise(Graphics2D g2,
290                                              Rectangle2D dataArea,
291                                              XYPlot plot,
292                                              XYDataset data,
293                                              PlotRenderingInfo info) {
294    
295            return super.initialise(g2, dataArea, plot, data, info);
296    
297        }
298    
299    
300        /**
301         * Draws the visual representation of a single data item.
302         *
303         * @param g2  the graphics device.
304         * @param state  the renderer state.
305         * @param dataArea  the area within which the data is being drawn.
306         * @param info  collects information about the drawing.
307         * @param plot  the plot (can be used to obtain standard color information 
308         *              etc).
309         * @param domainAxis  the domain axis.
310         * @param rangeAxis  the range axis.
311         * @param dataset  the dataset.
312         * @param series  the series index (zero-based).
313         * @param item  the item index (zero-based).
314         * @param crosshairState  crosshair information for the plot 
315         *                        (<code>null</code> permitted).
316         * @param pass  the pass index.
317         */
318        public void drawItem(Graphics2D g2,
319                             XYItemRendererState state,
320                             Rectangle2D dataArea,
321                             PlotRenderingInfo info,
322                             XYPlot plot,
323                             ValueAxis domainAxis,
324                             ValueAxis rangeAxis,
325                             XYDataset dataset,
326                             int series,
327                             int item,
328                             CrosshairState crosshairState,
329                             int pass) {
330                                 
331            PlotOrientation orientation = plot.getOrientation();
332            
333            // Get the item count for the series, so that we can know which is the 
334            // end of the series.
335            int itemCount = dataset.getItemCount(series);
336    
337            Paint paint = getItemPaint(series, item);
338            Stroke seriesStroke = getItemStroke(series, item);
339            g2.setPaint(paint);
340            g2.setStroke(seriesStroke);
341    
342            // get the data point...
343            double x1 = dataset.getXValue(series, item);
344            double y1 = dataset.getYValue(series, item);
345            double x = x1;
346            double y = Double.isNaN(y1) ? getRangeBase() : y1;
347            double transX1 = domainAxis.valueToJava2D(x, dataArea, 
348                    plot.getDomainAxisEdge());
349            double transY1 = rangeAxis.valueToJava2D(y, dataArea, 
350                    plot.getRangeAxisEdge());
351                                                              
352            // avoid possible sun.dc.pr.PRException: endPath: bad path
353            transY1 = restrictValueToDataArea(transY1, plot, dataArea);         
354    
355            if (this.pArea == null && !Double.isNaN(y1)) {
356    
357                // Create a new Area for the series
358                this.pArea = new Polygon();
359            
360                // start from Y = rangeBase
361                double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea,
362                        plot.getRangeAxisEdge());
363            
364                // avoid possible sun.dc.pr.PRException: endPath: bad path
365                transY2 = restrictValueToDataArea(transY2, plot, dataArea);         
366            
367                // The first point is (x, this.baseYValue)
368                if (orientation == PlotOrientation.VERTICAL) {
369                    this.pArea.addPoint((int) transX1, (int) transY2);
370                }
371                else if (orientation == PlotOrientation.HORIZONTAL) {
372                    this.pArea.addPoint((int) transY2, (int) transX1);
373                }
374            }
375    
376            double transX0 = 0;
377            double transY0 = restrictValueToDataArea(
378                getRangeBase(), plot, dataArea
379            );           
380            
381            double x0;
382            double y0;
383            if (item > 0) {
384                // get the previous data point...
385                x0 = dataset.getXValue(series, item - 1);
386                y0 = Double.isNaN(y1) ? y1 : dataset.getYValue(series, item - 1);
387    
388                x = x0;
389                y = Double.isNaN(y0) ? getRangeBase() : y0;
390                transX0 = domainAxis.valueToJava2D(x, dataArea, 
391                        plot.getDomainAxisEdge());
392                transY0 = rangeAxis.valueToJava2D(y, dataArea, 
393                        plot.getRangeAxisEdge());
394    
395                // avoid possible sun.dc.pr.PRException: endPath: bad path
396                transY0 = restrictValueToDataArea(transY0, plot, dataArea);
397                            
398                if (Double.isNaN(y1)) {
399                    // NULL value -> insert point on base line
400                    // instead of 'step point'
401                    transX1 = transX0;
402                    transY0 = transY1;          
403                }
404                if (transY0 != transY1) {
405                    // not just a horizontal bar but need to perform a 'step'.
406                    if (orientation == PlotOrientation.VERTICAL) {
407                        this.pArea.addPoint((int) transX1, (int) transY0);
408                    }
409                    else if (orientation == PlotOrientation.HORIZONTAL) {
410                        this.pArea.addPoint((int) transY0, (int) transX1);
411                    }
412                }
413            }           
414    
415            Shape shape = null;
416            if (!Double.isNaN(y1)) {
417                // Add each point to Area (x, y)
418                if (orientation == PlotOrientation.VERTICAL) {
419                    this.pArea.addPoint((int) transX1, (int) transY1);
420                }
421                else if (orientation == PlotOrientation.HORIZONTAL) {
422                    this.pArea.addPoint((int) transY1, (int) transX1);
423                }
424    
425                if (getShapesVisible()) {
426                    shape = getItemShape(series, item);
427                    if (orientation == PlotOrientation.VERTICAL) {
428                        shape = ShapeUtilities.createTranslatedShape(shape, 
429                                transX1, transY1);
430                    }
431                    else if (orientation == PlotOrientation.HORIZONTAL) {
432                        shape = ShapeUtilities.createTranslatedShape(shape, 
433                                transY1, transX1);
434                    }
435                    if (isShapesFilled()) {
436                        g2.fill(shape);
437                    }   
438                    else {
439                        g2.draw(shape);
440                    }   
441                }
442                else {
443                    if (orientation == PlotOrientation.VERTICAL) {
444                        shape = new Rectangle2D.Double(transX1 - 2, transY1 - 2, 
445                                4.0, 4.0);
446                    }
447                    else if (orientation == PlotOrientation.HORIZONTAL) {
448                        shape = new Rectangle2D.Double(transY1 - 2, transX1 - 2, 
449                                4.0, 4.0);
450                    }
451                }
452            }
453    
454            // Check if the item is the last item for the series or if it
455            // is a NULL value and number of items > 0.  We can't draw an area for 
456            // a single point.
457            if (getPlotArea() && item > 0 && this.pArea != null 
458                              && (item == (itemCount - 1) || Double.isNaN(y1))) {
459    
460                double transY2 = rangeAxis.valueToJava2D(getRangeBase(), dataArea, 
461                        plot.getRangeAxisEdge());
462    
463                // avoid possible sun.dc.pr.PRException: endPath: bad path
464                transY2 = restrictValueToDataArea(transY2, plot, dataArea);         
465    
466                if (orientation == PlotOrientation.VERTICAL) {
467                    // Add the last point (x,0)
468                    this.pArea.addPoint((int) transX1, (int) transY2);
469                }
470                else if (orientation == PlotOrientation.HORIZONTAL) {
471                    // Add the last point (x,0)
472                    this.pArea.addPoint((int) transY2, (int) transX1);
473                }
474    
475                // fill the polygon
476                g2.fill(this.pArea);
477    
478                // draw an outline around the Area.
479                if (isOutline()) {
480                    g2.setStroke(plot.getOutlineStroke());
481                    g2.setPaint(plot.getOutlinePaint());
482                    g2.draw(this.pArea);
483                }
484    
485                // start new area when needed (see above)
486                this.pArea = null;
487            }
488    
489            // do we need to update the crosshair values?
490            if (!Double.isNaN(y1)) {
491                int domainAxisIndex = plot.getDomainAxisIndex(domainAxis);
492                int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis);
493                updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex, 
494                        rangeAxisIndex, transX1, transY1, orientation);
495            }
496    
497            // collect entity and tool tip information...
498            if (state.getInfo() != null) {
499                EntityCollection entities = state.getEntityCollection();
500                if (entities != null && shape != null) {
501                    String tip = null;
502                    XYToolTipGenerator generator 
503                        = getToolTipGenerator(series, item);
504                    if (generator != null) {
505                        tip = generator.generateToolTip(dataset, series, item);
506                    }
507                    String url = null;
508                    if (getURLGenerator() != null) {
509                        url = getURLGenerator().generateURL(dataset, series, item);
510                    }
511                    XYItemEntity entity = new XYItemEntity(shape, dataset, series, 
512                            item, tip, url);
513                    entities.add(entity);
514                }
515            }
516        }
517    
518        /**
519         * Returns a clone of the renderer.
520         * 
521         * @return A clone.
522         * 
523         * @throws CloneNotSupportedException  if the renderer cannot be cloned.
524         */
525        public Object clone() throws CloneNotSupportedException {
526            return super.clone();
527        }
528        
529        /**
530         * Helper method which returns a value if it lies
531         * inside the visible dataArea and otherwise the corresponding
532         * coordinate on the border of the dataArea. The PlotOrientation
533         * is taken into account. 
534         * Useful to avoid possible sun.dc.pr.PRException: endPath: bad path
535         * which occurs when trying to draw lines/shapes which in large part
536         * lie outside of the visible dataArea.
537         * 
538         * @param value the value which shall be 
539         * @param dataArea  the area within which the data is being drawn.
540         * @param plot  the plot (can be used to obtain standard color 
541         *              information etc).
542         * @return <code>double</code> value inside the data area.
543         */
544        protected static double restrictValueToDataArea(double value, 
545                                                        XYPlot plot, 
546                                                        Rectangle2D dataArea) {
547            double min = 0;
548            double max = 0;
549            if (plot.getOrientation() == PlotOrientation.VERTICAL) {
550                min = dataArea.getMinY();
551                max = dataArea.getMaxY();
552            } 
553            else if (plot.getOrientation() ==  PlotOrientation.HORIZONTAL) {
554                min = dataArea.getMinX();
555                max = dataArea.getMaxX();
556            }       
557            if (value < min) {
558                value = min;
559            }
560            else if (value > max) {
561                value = max;
562            }
563            return value;
564        }
565    
566    }