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 * StackedXYAreaRenderer.java
029 * --------------------------
030 * (C) Copyright 2003-2008, by Richard Atkinson and Contributors.
031 *
032 * Original Author:  Richard Atkinson;
033 * Contributor(s):   Christian W. Zuckschwerdt;
034 *                   David Gilbert (for Object Refinery Limited);
035 *
036 * Changes:
037 * --------
038 * 27-Jul-2003 : Initial version (RA);
039 * 30-Jul-2003 : Modified entity constructor (CZ);
040 * 18-Aug-2003 : Now handles null values (RA);
041 * 20-Aug-2003 : Implemented Cloneable, PublicCloneable and Serializable (DG);
042 * 22-Sep-2003 : Changed to be a two pass renderer with optional shape Paint
043 *               and Stroke (RA);
044 * 07-Oct-2003 : Added renderer state (DG);
045 * 10-Feb-2004 : Updated state object and changed drawItem() method to make
046 *               overriding easier (DG);
047 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState.  Renamed
048 *               XYToolTipGenerator --> XYItemLabelGenerator (DG);
049 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
050 *               getYValue() (DG);
051 * 10-Sep-2004 : Removed getRangeType() method (DG);
052 * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG);
053 * 06-Jan-2005 : Override equals() (DG);
054 * 07-Jan-2005 : Update for method name changes in DatasetUtilities (DG);
055 * 28-Mar-2005 : Use getXValue() and getYValue() from dataset (DG);
056 * 06-Jun-2005 : Fixed null pointer exception, plus problems with equals() and
057 *               serialization (DG);
058 * ------------- JFREECHART 1.0.x ---------------------------------------------
059 * 10-Nov-2006 : Fixed bug 1593156, NullPointerException with line
060 *               plotting (DG);
061 * 02-Feb-2007 : Fixed bug 1649686, crosshairs don't stack y-values (DG);
062 * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG);
063 * 22-Mar-2007 : Fire change events in setShapePaint() and setShapeStroke()
064 *               methods (DG);
065 * 20-Apr-2007 : Updated getLegendItem() for renderer change (DG);
066 *
067 */
068
069package org.jfree.chart.renderer.xy;
070
071import java.awt.Graphics2D;
072import java.awt.Paint;
073import java.awt.Point;
074import java.awt.Polygon;
075import java.awt.Shape;
076import java.awt.Stroke;
077import java.awt.geom.Line2D;
078import java.awt.geom.Rectangle2D;
079import java.io.IOException;
080import java.io.ObjectInputStream;
081import java.io.ObjectOutputStream;
082import java.io.Serializable;
083import java.util.Stack;
084
085import org.jfree.chart.axis.ValueAxis;
086import org.jfree.chart.entity.EntityCollection;
087import org.jfree.chart.entity.XYItemEntity;
088import org.jfree.chart.event.RendererChangeEvent;
089import org.jfree.chart.labels.XYToolTipGenerator;
090import org.jfree.chart.plot.CrosshairState;
091import org.jfree.chart.plot.PlotOrientation;
092import org.jfree.chart.plot.PlotRenderingInfo;
093import org.jfree.chart.plot.XYPlot;
094import org.jfree.chart.urls.XYURLGenerator;
095import org.jfree.data.Range;
096import org.jfree.data.general.DatasetUtilities;
097import org.jfree.data.xy.TableXYDataset;
098import org.jfree.data.xy.XYDataset;
099import org.jfree.io.SerialUtilities;
100import org.jfree.util.ObjectUtilities;
101import org.jfree.util.PaintUtilities;
102import org.jfree.util.PublicCloneable;
103import org.jfree.util.ShapeUtilities;
104
105/**
106 * A stacked area renderer for the {@link XYPlot} class.
107 * <br><br>
108 * The example shown here is generated by the
109 * <code>StackedXYAreaRendererDemo1.java</code> program included in the
110 * JFreeChart demo collection:
111 * <br><br>
112 * <img src="../../../../../images/StackedXYAreaRendererSample.png"
113 * alt="StackedXYAreaRendererSample.png" />
114 * <br><br>
115 * SPECIAL NOTE:  This renderer does not currently handle negative data values
116 * correctly.  This should get fixed at some point, but the current workaround
117 * is to use the {@link StackedXYAreaRenderer2} class instead.
118 */
119public class StackedXYAreaRenderer extends XYAreaRenderer
120        implements Cloneable, PublicCloneable, Serializable {
121
122    /** For serialization. */
123    private static final long serialVersionUID = 5217394318178570889L;
124
125     /**
126     * A state object for use by this renderer.
127     */
128    static class StackedXYAreaRendererState extends XYItemRendererState {
129
130        /** The area for the current series. */
131        private Polygon seriesArea;
132
133        /** The line. */
134        private Line2D line;
135
136        /** The points from the last series. */
137        private Stack lastSeriesPoints;
138
139        /** The points for the current series. */
140        private Stack currentSeriesPoints;
141
142        /**
143         * Creates a new state for the renderer.
144         *
145         * @param info  the plot rendering info.
146         */
147        public StackedXYAreaRendererState(PlotRenderingInfo info) {
148            super(info);
149            this.seriesArea = null;
150            this.line = new Line2D.Double();
151            this.lastSeriesPoints = new Stack();
152            this.currentSeriesPoints = new Stack();
153        }
154
155        /**
156         * Returns the series area.
157         *
158         * @return The series area.
159         */
160        public Polygon getSeriesArea() {
161            return this.seriesArea;
162        }
163
164        /**
165         * Sets the series area.
166         *
167         * @param area  the area.
168         */
169        public void setSeriesArea(Polygon area) {
170            this.seriesArea = area;
171        }
172
173        /**
174         * Returns the working line.
175         *
176         * @return The working line.
177         */
178        public Line2D getLine() {
179            return this.line;
180        }
181
182        /**
183         * Returns the current series points.
184         *
185         * @return The current series points.
186         */
187        public Stack getCurrentSeriesPoints() {
188            return this.currentSeriesPoints;
189        }
190
191        /**
192         * Sets the current series points.
193         *
194         * @param points  the points.
195         */
196        public void setCurrentSeriesPoints(Stack points) {
197            this.currentSeriesPoints = points;
198        }
199
200        /**
201         * Returns the last series points.
202         *
203         * @return The last series points.
204         */
205        public Stack getLastSeriesPoints() {
206            return this.lastSeriesPoints;
207        }
208
209        /**
210         * Sets the last series points.
211         *
212         * @param points  the points.
213         */
214        public void setLastSeriesPoints(Stack points) {
215            this.lastSeriesPoints = points;
216        }
217
218    }
219
220    /**
221     * Custom Paint for drawing all shapes, if null defaults to series shapes
222     */
223    private transient Paint shapePaint = null;
224
225    /**
226     * Custom Stroke for drawing all shapes, if null defaults to series
227     * strokes.
228     */
229    private transient Stroke shapeStroke = null;
230
231    /**
232     * Creates a new renderer.
233     */
234    public StackedXYAreaRenderer() {
235        this(AREA);
236    }
237
238    /**
239     * Constructs a new renderer.
240     *
241     * @param type  the type of the renderer.
242     */
243    public StackedXYAreaRenderer(int type) {
244        this(type, null, null);
245    }
246
247    /**
248     * Constructs a new renderer.  To specify the type of renderer, use one of
249     * the constants: <code>SHAPES</code>, <code>LINES</code>,
250     * <code>SHAPES_AND_LINES</code>, <code>AREA</code> or
251     * <code>AREA_AND_SHAPES</code>.
252     *
253     * @param type  the type of renderer.
254     * @param labelGenerator  the tool tip generator to use (<code>null</code>
255     *                        is none).
256     * @param urlGenerator  the URL generator (<code>null</code> permitted).
257     */
258    public StackedXYAreaRenderer(int type,
259                                 XYToolTipGenerator labelGenerator,
260                                 XYURLGenerator urlGenerator) {
261
262        super(type, labelGenerator, urlGenerator);
263    }
264
265    /**
266     * Returns the paint used for rendering shapes, or <code>null</code> if
267     * using series paints.
268     *
269     * @return The paint (possibly <code>null</code>).
270     *
271     * @see #setShapePaint(Paint)
272     */
273    public Paint getShapePaint() {
274        return this.shapePaint;
275    }
276
277    /**
278     * Sets the paint for rendering shapes and sends a
279     * {@link RendererChangeEvent} to all registered listeners.
280     *
281     * @param shapePaint  the paint (<code>null</code> permitted).
282     *
283     * @see #getShapePaint()
284     */
285    public void setShapePaint(Paint shapePaint) {
286        this.shapePaint = shapePaint;
287        fireChangeEvent();
288    }
289
290    /**
291     * Returns the stroke used for rendering shapes, or <code>null</code> if
292     * using series strokes.
293     *
294     * @return The stroke (possibly <code>null</code>).
295     *
296     * @see #setShapeStroke(Stroke)
297     */
298    public Stroke getShapeStroke() {
299        return this.shapeStroke;
300    }
301
302    /**
303     * Sets the stroke for rendering shapes and sends a
304     * {@link RendererChangeEvent} to all registered listeners.
305     *
306     * @param shapeStroke  the stroke (<code>null</code> permitted).
307     *
308     * @see #getShapeStroke()
309     */
310    public void setShapeStroke(Stroke shapeStroke) {
311        this.shapeStroke = shapeStroke;
312        fireChangeEvent();
313    }
314
315    /**
316     * Initialises the renderer. This method will be called before the first
317     * item is rendered, giving the renderer an opportunity to initialise any
318     * state information it wants to maintain.
319     *
320     * @param g2  the graphics device.
321     * @param dataArea  the area inside the axes.
322     * @param plot  the plot.
323     * @param data  the data.
324     * @param info  an optional info collection object to return data back to
325     *              the caller.
326     *
327     * @return A state object that should be passed to subsequent calls to the
328     *         drawItem() method.
329     */
330    public XYItemRendererState initialise(Graphics2D g2,
331                                          Rectangle2D dataArea,
332                                          XYPlot plot,
333                                          XYDataset data,
334                                          PlotRenderingInfo info) {
335
336        XYItemRendererState state = new StackedXYAreaRendererState(info);
337        // in the rendering process, there is special handling for item
338        // zero, so we can't support processing of visible data items only
339        state.setProcessVisibleItemsOnly(false);
340        return state;
341    }
342
343    /**
344     * Returns the number of passes required by the renderer.
345     *
346     * @return 2.
347     */
348    public int getPassCount() {
349        return 2;
350    }
351
352    /**
353     * Returns the range of values the renderer requires to display all the
354     * items from the specified dataset.
355     *
356     * @param dataset  the dataset (<code>null</code> permitted).
357     *
358     * @return The range ([0.0, 0.0] if the dataset contains no values, and
359     *         <code>null</code> if the dataset is <code>null</code>).
360     *
361     * @throws ClassCastException if <code>dataset</code> is not an instance
362     *         of {@link TableXYDataset}.
363     */
364    public Range findRangeBounds(XYDataset dataset) {
365        if (dataset != null) {
366            return DatasetUtilities.findStackedRangeBounds(
367                (TableXYDataset) dataset);
368        }
369        else {
370            return null;
371        }
372    }
373
374    /**
375     * Draws the visual representation of a single data item.
376     *
377     * @param g2  the graphics device.
378     * @param state  the renderer state.
379     * @param dataArea  the area within which the data is being drawn.
380     * @param info  collects information about the drawing.
381     * @param plot  the plot (can be used to obtain standard color information
382     *              etc).
383     * @param domainAxis  the domain axis.
384     * @param rangeAxis  the range axis.
385     * @param dataset  the dataset.
386     * @param series  the series index (zero-based).
387     * @param item  the item index (zero-based).
388     * @param crosshairState  information about crosshairs on a plot.
389     * @param pass  the pass index.
390     *
391     * @throws ClassCastException if <code>state</code> is not an instance of
392     *         <code>StackedXYAreaRendererState</code> or <code>dataset</code>
393     *         is not an instance of {@link TableXYDataset}.
394     */
395    public void drawItem(Graphics2D g2,
396                         XYItemRendererState state,
397                         Rectangle2D dataArea,
398                         PlotRenderingInfo info,
399                         XYPlot plot,
400                         ValueAxis domainAxis,
401                         ValueAxis rangeAxis,
402                         XYDataset dataset,
403                         int series,
404                         int item,
405                         CrosshairState crosshairState,
406                         int pass) {
407
408        PlotOrientation orientation = plot.getOrientation();
409        StackedXYAreaRendererState areaState
410            = (StackedXYAreaRendererState) state;
411        // Get the item count for the series, so that we can know which is the
412        // end of the series.
413        TableXYDataset tdataset = (TableXYDataset) dataset;
414        int itemCount = tdataset.getItemCount();
415
416        // get the data point...
417        double x1 = dataset.getXValue(series, item);
418        double y1 = dataset.getYValue(series, item);
419        boolean nullPoint = false;
420        if (Double.isNaN(y1)) {
421            y1 = 0.0;
422            nullPoint = true;
423        }
424
425        //  Get height adjustment based on stack and translate to Java2D values
426        double ph1 = getPreviousHeight(tdataset, series, item);
427        double transX1 = domainAxis.valueToJava2D(x1, dataArea,
428                plot.getDomainAxisEdge());
429        double transY1 = rangeAxis.valueToJava2D(y1 + ph1, dataArea,
430                plot.getRangeAxisEdge());
431
432        //  Get series Paint and Stroke
433        Paint seriesPaint = getItemPaint(series, item);
434        Stroke seriesStroke = getItemStroke(series, item);
435
436        if (pass == 0) {
437            //  On first pass render the areas, line and outlines
438
439            if (item == 0) {
440                // Create a new Area for the series
441                areaState.setSeriesArea(new Polygon());
442                areaState.setLastSeriesPoints(
443                        areaState.getCurrentSeriesPoints());
444                areaState.setCurrentSeriesPoints(new Stack());
445
446                // start from previous height (ph1)
447                double transY2 = rangeAxis.valueToJava2D(ph1, dataArea,
448                        plot.getRangeAxisEdge());
449
450                // The first point is (x, 0)
451                if (orientation == PlotOrientation.VERTICAL) {
452                    areaState.getSeriesArea().addPoint((int) transX1,
453                            (int) transY2);
454                }
455                else if (orientation == PlotOrientation.HORIZONTAL) {
456                    areaState.getSeriesArea().addPoint((int) transY2,
457                            (int) transX1);
458                }
459            }
460
461            // Add each point to Area (x, y)
462            if (orientation == PlotOrientation.VERTICAL) {
463                Point point = new Point((int) transX1, (int) transY1);
464                areaState.getSeriesArea().addPoint((int) point.getX(),
465                        (int) point.getY());
466                areaState.getCurrentSeriesPoints().push(point);
467            }
468            else if (orientation == PlotOrientation.HORIZONTAL) {
469                areaState.getSeriesArea().addPoint((int) transY1,
470                        (int) transX1);
471            }
472
473            if (getPlotLines()) {
474                if (item > 0) {
475                    // get the previous data point...
476                    double x0 = dataset.getXValue(series, item - 1);
477                    double y0 = dataset.getYValue(series, item - 1);
478                    double ph0 = getPreviousHeight(tdataset, series, item - 1);
479                    double transX0 = domainAxis.valueToJava2D(x0, dataArea,
480                            plot.getDomainAxisEdge());
481                    double transY0 = rangeAxis.valueToJava2D(y0 + ph0,
482                            dataArea, plot.getRangeAxisEdge());
483
484                    if (orientation == PlotOrientation.VERTICAL) {
485                        areaState.getLine().setLine(transX0, transY0, transX1,
486                                transY1);
487                    }
488                    else if (orientation == PlotOrientation.HORIZONTAL) {
489                        areaState.getLine().setLine(transY0, transX0, transY1,
490                                transX1);
491                    }
492                    g2.draw(areaState.getLine());
493                }
494            }
495
496            // Check if the item is the last item for the series and number of
497            // items > 0.  We can't draw an area for a single point.
498            if (getPlotArea() && item > 0 && item == (itemCount - 1)) {
499
500                double transY2 = rangeAxis.valueToJava2D(ph1, dataArea,
501                        plot.getRangeAxisEdge());
502
503                if (orientation == PlotOrientation.VERTICAL) {
504                    // Add the last point (x,0)
505                    areaState.getSeriesArea().addPoint((int) transX1,
506                            (int) transY2);
507                }
508                else if (orientation == PlotOrientation.HORIZONTAL) {
509                    // Add the last point (x,0)
510                    areaState.getSeriesArea().addPoint((int) transY2,
511                            (int) transX1);
512                }
513
514                // Add points from last series to complete the base of the
515                // polygon
516                if (series != 0) {
517                    Stack points = areaState.getLastSeriesPoints();
518                    while (!points.empty()) {
519                        Point point = (Point) points.pop();
520                        areaState.getSeriesArea().addPoint((int) point.getX(),
521                                (int) point.getY());
522                    }
523                }
524
525                //  Fill the polygon
526                g2.setPaint(seriesPaint);
527                g2.setStroke(seriesStroke);
528                g2.fill(areaState.getSeriesArea());
529
530                //  Draw an outline around the Area.
531                if (isOutline()) {
532                    g2.setStroke(lookupSeriesOutlineStroke(series));
533                    g2.setPaint(lookupSeriesOutlinePaint(series));
534                    g2.draw(areaState.getSeriesArea());
535                }
536            }
537
538            int domainAxisIndex = plot.getDomainAxisIndex(domainAxis);
539            int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis);
540            updateCrosshairValues(crosshairState, x1, ph1 + y1, domainAxisIndex,
541                    rangeAxisIndex, transX1, transY1, orientation);
542
543        }
544        else if (pass == 1) {
545            // On second pass render shapes and collect entity and tooltip
546            // information
547
548            Shape shape = null;
549            if (getPlotShapes()) {
550                shape = getItemShape(series, item);
551                if (plot.getOrientation() == PlotOrientation.VERTICAL) {
552                    shape = ShapeUtilities.createTranslatedShape(shape,
553                            transX1, transY1);
554                }
555                else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
556                    shape = ShapeUtilities.createTranslatedShape(shape,
557                            transY1, transX1);
558                }
559                if (!nullPoint) {
560                    if (getShapePaint() != null) {
561                        g2.setPaint(getShapePaint());
562                    }
563                    else {
564                        g2.setPaint(seriesPaint);
565                    }
566                    if (getShapeStroke() != null) {
567                        g2.setStroke(getShapeStroke());
568                    }
569                    else {
570                        g2.setStroke(seriesStroke);
571                    }
572                    g2.draw(shape);
573                }
574            }
575            else {
576                if (plot.getOrientation() == PlotOrientation.VERTICAL) {
577                    shape = new Rectangle2D.Double(transX1 - 3, transY1 - 3,
578                            6.0, 6.0);
579                }
580                else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
581                    shape = new Rectangle2D.Double(transY1 - 3, transX1 - 3,
582                            6.0, 6.0);
583                }
584            }
585
586            // collect entity and tool tip information...
587            if (state.getInfo() != null) {
588                EntityCollection entities = state.getEntityCollection();
589                if (entities != null && shape != null && !nullPoint) {
590                    String tip = null;
591                    XYToolTipGenerator generator
592                        = getToolTipGenerator(series, item);
593                    if (generator != null) {
594                        tip = generator.generateToolTip(dataset, series, item);
595                    }
596                    String url = null;
597                    if (getURLGenerator() != null) {
598                        url = getURLGenerator().generateURL(dataset, series,
599                                item);
600                    }
601                    XYItemEntity entity = new XYItemEntity(shape, dataset,
602                            series, item, tip, url);
603                    entities.add(entity);
604                }
605            }
606
607        }
608    }
609
610    /**
611     * Calculates the stacked value of the all series up to, but not including
612     * <code>series</code> for the specified item. It returns 0.0 if
613     * <code>series</code> is the first series, i.e. 0.
614     *
615     * @param dataset  the dataset.
616     * @param series  the series.
617     * @param index  the index.
618     *
619     * @return The cumulative value for all series' values up to but excluding
620     *         <code>series</code> for <code>index</code>.
621     */
622    protected double getPreviousHeight(TableXYDataset dataset,
623                                       int series, int index) {
624        double result = 0.0;
625        for (int i = 0; i < series; i++) {
626            double value = dataset.getYValue(i, index);
627            if (!Double.isNaN(value)) {
628                result += value;
629            }
630        }
631        return result;
632    }
633
634    /**
635     * Tests the renderer for equality with an arbitrary object.
636     *
637     * @param obj  the object (<code>null</code> permitted).
638     *
639     * @return A boolean.
640     */
641    public boolean equals(Object obj) {
642        if (obj == this) {
643            return true;
644        }
645        if (!(obj instanceof StackedXYAreaRenderer) || !super.equals(obj)) {
646            return false;
647        }
648        StackedXYAreaRenderer that = (StackedXYAreaRenderer) obj;
649        if (!PaintUtilities.equal(this.shapePaint, that.shapePaint)) {
650            return false;
651        }
652        if (!ObjectUtilities.equal(this.shapeStroke, that.shapeStroke)) {
653            return false;
654        }
655        return true;
656    }
657
658    /**
659     * Returns a clone of the renderer.
660     *
661     * @return A clone.
662     *
663     * @throws CloneNotSupportedException if the renderer cannot be cloned.
664     */
665    public Object clone() throws CloneNotSupportedException {
666        return super.clone();
667    }
668
669    /**
670     * Provides serialization support.
671     *
672     * @param stream  the input stream.
673     *
674     * @throws IOException  if there is an I/O error.
675     * @throws ClassNotFoundException  if there is a classpath problem.
676     */
677    private void readObject(ObjectInputStream stream)
678            throws IOException, ClassNotFoundException {
679        stream.defaultReadObject();
680        this.shapePaint = SerialUtilities.readPaint(stream);
681        this.shapeStroke = SerialUtilities.readStroke(stream);
682    }
683
684    /**
685     * Provides serialization support.
686     *
687     * @param stream  the output stream.
688     *
689     * @throws IOException  if there is an I/O error.
690     */
691    private void writeObject(ObjectOutputStream stream) throws IOException {
692        stream.defaultWriteObject();
693        SerialUtilities.writePaint(this.shapePaint, stream);
694        SerialUtilities.writeStroke(this.shapeStroke, stream);
695    }
696
697}