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