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