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