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