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