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