001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2008, by Object Refinery Limited and Contributors.
006     *
007     * Project Info:  http://www.jfree.org/jfreechart/index.html
008     *
009     * This library is free software; you can redistribute it and/or modify it
010     * under the terms of the GNU Lesser General Public License as published by
011     * the Free Software Foundation; either version 2.1 of the License, or
012     * (at your option) any later version.
013     *
014     * This library is distributed in the hope that it will be useful, but
015     * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016     * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017     * License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this library; if not, write to the Free Software
021     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022     * USA.
023     *
024     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025     * in the United States and other countries.]
026     *
027     * --------------------------
028     * StackedXYAreaRenderer.java
029     * --------------------------
030     * (C) Copyright 2003-2008, by Richard Atkinson and Contributors.
031     *
032     * Original Author:  Richard Atkinson;
033     * Contributor(s):   Christian W. Zuckschwerdt;
034     *                   David Gilbert (for Object Refinery Limited);
035     *
036     * Changes:
037     * --------
038     * 27-Jul-2003 : Initial version (RA);
039     * 30-Jul-2003 : Modified entity constructor (CZ);
040     * 18-Aug-2003 : Now handles null values (RA);
041     * 20-Aug-2003 : Implemented Cloneable, PublicCloneable and Serializable (DG);
042     * 22-Sep-2003 : Changed to be a two pass renderer with optional shape Paint
043     *               and Stroke (RA);
044     * 07-Oct-2003 : Added renderer state (DG);
045     * 10-Feb-2004 : Updated state object and changed drawItem() method to make
046     *               overriding easier (DG);
047     * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState.  Renamed
048     *               XYToolTipGenerator --> XYItemLabelGenerator (DG);
049     * 15-Jul-2004 : Switched getX() with getXValue() and getY() with
050     *               getYValue() (DG);
051     * 10-Sep-2004 : Removed getRangeType() method (DG);
052     * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG);
053     * 06-Jan-2005 : Override equals() (DG);
054     * 07-Jan-2005 : Update for method name changes in DatasetUtilities (DG);
055     * 28-Mar-2005 : Use getXValue() and getYValue() from dataset (DG);
056     * 06-Jun-2005 : Fixed null pointer exception, plus problems with equals() and
057     *               serialization (DG);
058     * ------------- JFREECHART 1.0.x ---------------------------------------------
059     * 10-Nov-2006 : Fixed bug 1593156, NullPointerException with line
060     *               plotting (DG);
061     * 02-Feb-2007 : Fixed bug 1649686, crosshairs don't stack y-values (DG);
062     * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG);
063     * 22-Mar-2007 : Fire change events in setShapePaint() and setShapeStroke()
064     *               methods (DG);
065     * 20-Apr-2007 : Updated getLegendItem() for renderer change (DG);
066     *
067     */
068    
069    package org.jfree.chart.renderer.xy;
070    
071    import java.awt.Graphics2D;
072    import java.awt.Paint;
073    import java.awt.Point;
074    import java.awt.Polygon;
075    import java.awt.Shape;
076    import java.awt.Stroke;
077    import java.awt.geom.Line2D;
078    import java.awt.geom.Rectangle2D;
079    import java.io.IOException;
080    import java.io.ObjectInputStream;
081    import java.io.ObjectOutputStream;
082    import java.io.Serializable;
083    import java.util.Stack;
084    
085    import org.jfree.chart.axis.ValueAxis;
086    import org.jfree.chart.entity.EntityCollection;
087    import org.jfree.chart.entity.XYItemEntity;
088    import org.jfree.chart.event.RendererChangeEvent;
089    import org.jfree.chart.labels.XYToolTipGenerator;
090    import org.jfree.chart.plot.CrosshairState;
091    import org.jfree.chart.plot.PlotOrientation;
092    import org.jfree.chart.plot.PlotRenderingInfo;
093    import org.jfree.chart.plot.XYPlot;
094    import org.jfree.chart.urls.XYURLGenerator;
095    import org.jfree.data.Range;
096    import org.jfree.data.general.DatasetUtilities;
097    import org.jfree.data.xy.TableXYDataset;
098    import org.jfree.data.xy.XYDataset;
099    import org.jfree.io.SerialUtilities;
100    import org.jfree.util.ObjectUtilities;
101    import org.jfree.util.PaintUtilities;
102    import org.jfree.util.PublicCloneable;
103    import 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     */
112    public class StackedXYAreaRenderer extends XYAreaRenderer
113            implements Cloneable, PublicCloneable, 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        /**
232         * Constructs a new renderer.
233         *
234         * @param type  the type of the renderer.
235         */
236        public StackedXYAreaRenderer(int type) {
237            this(type, null, null);
238        }
239    
240        /**
241         * Constructs a new renderer.  To specify the type of renderer, use one of
242         * the constants: <code>SHAPES</code>, <code>LINES</code>,
243         * <code>SHAPES_AND_LINES</code>, <code>AREA</code> or
244         * <code>AREA_AND_SHAPES</code>.
245         *
246         * @param type  the type of renderer.
247         * @param labelGenerator  the tool tip generator to use (<code>null</code>
248         *                        is none).
249         * @param urlGenerator  the URL generator (<code>null</code> permitted).
250         */
251        public StackedXYAreaRenderer(int type,
252                                     XYToolTipGenerator labelGenerator,
253                                     XYURLGenerator urlGenerator) {
254    
255            super(type, labelGenerator, urlGenerator);
256        }
257    
258        /**
259         * Returns the paint used for rendering shapes, or <code>null</code> if
260         * using series paints.
261         *
262         * @return The paint (possibly <code>null</code>).
263         *
264         * @see #setShapePaint(Paint)
265         */
266        public Paint getShapePaint() {
267            return this.shapePaint;
268        }
269    
270        /**
271         * Sets the paint for rendering shapes and sends a
272         * {@link RendererChangeEvent} to all registered listeners.
273         *
274         * @param shapePaint  the paint (<code>null</code> permitted).
275         *
276         * @see #getShapePaint()
277         */
278        public void setShapePaint(Paint shapePaint) {
279            this.shapePaint = shapePaint;
280            fireChangeEvent();
281        }
282    
283        /**
284         * Returns the stroke used for rendering shapes, or <code>null</code> if
285         * using series strokes.
286         *
287         * @return The stroke (possibly <code>null</code>).
288         *
289         * @see #setShapeStroke(Stroke)
290         */
291        public Stroke getShapeStroke() {
292            return this.shapeStroke;
293        }
294    
295        /**
296         * Sets the stroke for rendering shapes and sends a
297         * {@link RendererChangeEvent} to all registered listeners.
298         *
299         * @param shapeStroke  the stroke (<code>null</code> permitted).
300         *
301         * @see #getShapeStroke()
302         */
303        public void setShapeStroke(Stroke shapeStroke) {
304            this.shapeStroke = shapeStroke;
305            fireChangeEvent();
306        }
307    
308        /**
309         * Initialises the renderer. This method will be called before the first
310         * item is rendered, giving the renderer an opportunity to initialise any
311         * state information it wants to maintain.
312         *
313         * @param g2  the graphics device.
314         * @param dataArea  the area inside the axes.
315         * @param plot  the plot.
316         * @param data  the data.
317         * @param info  an optional info collection object to return data back to
318         *              the caller.
319         *
320         * @return A state object that should be passed to subsequent calls to the
321         *         drawItem() method.
322         */
323        public XYItemRendererState initialise(Graphics2D g2,
324                                              Rectangle2D dataArea,
325                                              XYPlot plot,
326                                              XYDataset data,
327                                              PlotRenderingInfo info) {
328    
329            XYItemRendererState state = new StackedXYAreaRendererState(info);
330            // in the rendering process, there is special handling for item
331            // zero, so we can't support processing of visible data items only
332            state.setProcessVisibleItemsOnly(false);
333            return state;
334        }
335    
336        /**
337         * Returns the number of passes required by the renderer.
338         *
339         * @return 2.
340         */
341        public int getPassCount() {
342            return 2;
343        }
344    
345        /**
346         * Returns the range of values the renderer requires to display all the
347         * items from the specified dataset.
348         *
349         * @param dataset  the dataset (<code>null</code> permitted).
350         *
351         * @return The range ([0.0, 0.0] if the dataset contains no values, and
352         *         <code>null</code> if the dataset is <code>null</code>).
353         *
354         * @throws ClassCastException if <code>dataset</code> is not an instance
355         *         of {@link TableXYDataset}.
356         */
357        public Range findRangeBounds(XYDataset dataset) {
358            if (dataset != null) {
359                return DatasetUtilities.findStackedRangeBounds(
360                    (TableXYDataset) dataset);
361            }
362            else {
363                return null;
364            }
365        }
366    
367        /**
368         * Draws the visual representation of a single data item.
369         *
370         * @param g2  the graphics device.
371         * @param state  the renderer state.
372         * @param dataArea  the area within which the data is being drawn.
373         * @param info  collects information about the drawing.
374         * @param plot  the plot (can be used to obtain standard color information
375         *              etc).
376         * @param domainAxis  the domain axis.
377         * @param rangeAxis  the range axis.
378         * @param dataset  the dataset.
379         * @param series  the series index (zero-based).
380         * @param item  the item index (zero-based).
381         * @param crosshairState  information about crosshairs on a plot.
382         * @param pass  the pass index.
383         *
384         * @throws ClassCastException if <code>state</code> is not an instance of
385         *         <code>StackedXYAreaRendererState</code> or <code>dataset</code>
386         *         is not an instance of {@link TableXYDataset}.
387         */
388        public void drawItem(Graphics2D g2,
389                             XYItemRendererState state,
390                             Rectangle2D dataArea,
391                             PlotRenderingInfo info,
392                             XYPlot plot,
393                             ValueAxis domainAxis,
394                             ValueAxis rangeAxis,
395                             XYDataset dataset,
396                             int series,
397                             int item,
398                             CrosshairState crosshairState,
399                             int pass) {
400    
401            PlotOrientation orientation = plot.getOrientation();
402            StackedXYAreaRendererState areaState
403                = (StackedXYAreaRendererState) state;
404            // Get the item count for the series, so that we can know which is the
405            // end of the series.
406            TableXYDataset tdataset = (TableXYDataset) dataset;
407            int itemCount = tdataset.getItemCount();
408    
409            // get the data point...
410            double x1 = dataset.getXValue(series, item);
411            double y1 = dataset.getYValue(series, item);
412            boolean nullPoint = false;
413            if (Double.isNaN(y1)) {
414                y1 = 0.0;
415                nullPoint = true;
416            }
417    
418            //  Get height adjustment based on stack and translate to Java2D values
419            double ph1 = getPreviousHeight(tdataset, series, item);
420            double transX1 = domainAxis.valueToJava2D(x1, dataArea,
421                    plot.getDomainAxisEdge());
422            double transY1 = rangeAxis.valueToJava2D(y1 + ph1, dataArea,
423                    plot.getRangeAxisEdge());
424    
425            //  Get series Paint and Stroke
426            Paint seriesPaint = getItemPaint(series, item);
427            Stroke seriesStroke = getItemStroke(series, item);
428    
429            if (pass == 0) {
430                //  On first pass render the areas, line and outlines
431    
432                if (item == 0) {
433                    // Create a new Area for the series
434                    areaState.setSeriesArea(new Polygon());
435                    areaState.setLastSeriesPoints(
436                            areaState.getCurrentSeriesPoints());
437                    areaState.setCurrentSeriesPoints(new Stack());
438    
439                    // start from previous height (ph1)
440                    double transY2 = rangeAxis.valueToJava2D(ph1, dataArea,
441                            plot.getRangeAxisEdge());
442    
443                    // The first point is (x, 0)
444                    if (orientation == PlotOrientation.VERTICAL) {
445                        areaState.getSeriesArea().addPoint((int) transX1,
446                                (int) transY2);
447                    }
448                    else if (orientation == PlotOrientation.HORIZONTAL) {
449                        areaState.getSeriesArea().addPoint((int) transY2,
450                                (int) transX1);
451                    }
452                }
453    
454                // Add each point to Area (x, y)
455                if (orientation == PlotOrientation.VERTICAL) {
456                    Point point = new Point((int) transX1, (int) transY1);
457                    areaState.getSeriesArea().addPoint((int) point.getX(),
458                            (int) point.getY());
459                    areaState.getCurrentSeriesPoints().push(point);
460                }
461                else if (orientation == PlotOrientation.HORIZONTAL) {
462                    areaState.getSeriesArea().addPoint((int) transY1,
463                            (int) transX1);
464                }
465    
466                if (getPlotLines()) {
467                    if (item > 0) {
468                        // get the previous data point...
469                        double x0 = dataset.getXValue(series, item - 1);
470                        double y0 = dataset.getYValue(series, item - 1);
471                        double ph0 = getPreviousHeight(tdataset, series, item - 1);
472                        double transX0 = domainAxis.valueToJava2D(x0, dataArea,
473                                plot.getDomainAxisEdge());
474                        double transY0 = rangeAxis.valueToJava2D(y0 + ph0,
475                                dataArea, plot.getRangeAxisEdge());
476    
477                        if (orientation == PlotOrientation.VERTICAL) {
478                            areaState.getLine().setLine(transX0, transY0, transX1,
479                                    transY1);
480                        }
481                        else if (orientation == PlotOrientation.HORIZONTAL) {
482                            areaState.getLine().setLine(transY0, transX0, transY1,
483                                    transX1);
484                        }
485                        g2.draw(areaState.getLine());
486                    }
487                }
488    
489                // Check if the item is the last item for the series and number of
490                // items > 0.  We can't draw an area for a single point.
491                if (getPlotArea() && item > 0 && item == (itemCount - 1)) {
492    
493                    double transY2 = rangeAxis.valueToJava2D(ph1, dataArea,
494                            plot.getRangeAxisEdge());
495    
496                    if (orientation == PlotOrientation.VERTICAL) {
497                        // Add the last point (x,0)
498                        areaState.getSeriesArea().addPoint((int) transX1,
499                                (int) transY2);
500                    }
501                    else if (orientation == PlotOrientation.HORIZONTAL) {
502                        // Add the last point (x,0)
503                        areaState.getSeriesArea().addPoint((int) transY2,
504                                (int) transX1);
505                    }
506    
507                    // Add points from last series to complete the base of the
508                    // polygon
509                    if (series != 0) {
510                        Stack points = areaState.getLastSeriesPoints();
511                        while (!points.empty()) {
512                            Point point = (Point) points.pop();
513                            areaState.getSeriesArea().addPoint((int) point.getX(),
514                                    (int) point.getY());
515                        }
516                    }
517    
518                    //  Fill the polygon
519                    g2.setPaint(seriesPaint);
520                    g2.setStroke(seriesStroke);
521                    g2.fill(areaState.getSeriesArea());
522    
523                    //  Draw an outline around the Area.
524                    if (isOutline()) {
525                        g2.setStroke(lookupSeriesOutlineStroke(series));
526                        g2.setPaint(lookupSeriesOutlinePaint(series));
527                        g2.draw(areaState.getSeriesArea());
528                    }
529                }
530    
531                int domainAxisIndex = plot.getDomainAxisIndex(domainAxis);
532                int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis);
533                updateCrosshairValues(crosshairState, x1, ph1 + y1, domainAxisIndex,
534                        rangeAxisIndex, transX1, transY1, orientation);
535    
536            }
537            else if (pass == 1) {
538                // On second pass render shapes and collect entity and tooltip
539                // information
540    
541                Shape shape = null;
542                if (getPlotShapes()) {
543                    shape = getItemShape(series, item);
544                    if (plot.getOrientation() == PlotOrientation.VERTICAL) {
545                        shape = ShapeUtilities.createTranslatedShape(shape,
546                                transX1, transY1);
547                    }
548                    else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
549                        shape = ShapeUtilities.createTranslatedShape(shape,
550                                transY1, transX1);
551                    }
552                    if (!nullPoint) {
553                        if (getShapePaint() != null) {
554                            g2.setPaint(getShapePaint());
555                        }
556                        else {
557                            g2.setPaint(seriesPaint);
558                        }
559                        if (getShapeStroke() != null) {
560                            g2.setStroke(getShapeStroke());
561                        }
562                        else {
563                            g2.setStroke(seriesStroke);
564                        }
565                        g2.draw(shape);
566                    }
567                }
568                else {
569                    if (plot.getOrientation() == PlotOrientation.VERTICAL) {
570                        shape = new Rectangle2D.Double(transX1 - 3, transY1 - 3,
571                                6.0, 6.0);
572                    }
573                    else if (plot.getOrientation() == PlotOrientation.HORIZONTAL) {
574                        shape = new Rectangle2D.Double(transY1 - 3, transX1 - 3,
575                                6.0, 6.0);
576                    }
577                }
578    
579                // collect entity and tool tip information...
580                if (state.getInfo() != null) {
581                    EntityCollection entities = state.getEntityCollection();
582                    if (entities != null && shape != null && !nullPoint) {
583                        String tip = null;
584                        XYToolTipGenerator generator
585                            = getToolTipGenerator(series, item);
586                        if (generator != null) {
587                            tip = generator.generateToolTip(dataset, series, item);
588                        }
589                        String url = null;
590                        if (getURLGenerator() != null) {
591                            url = getURLGenerator().generateURL(dataset, series,
592                                    item);
593                        }
594                        XYItemEntity entity = new XYItemEntity(shape, dataset,
595                                series, item, tip, url);
596                        entities.add(entity);
597                    }
598                }
599    
600            }
601        }
602    
603        /**
604         * Calculates the stacked value of the all series up to, but not including
605         * <code>series</code> for the specified item. It returns 0.0 if
606         * <code>series</code> is the first series, i.e. 0.
607         *
608         * @param dataset  the dataset.
609         * @param series  the series.
610         * @param index  the index.
611         *
612         * @return The cumulative value for all series' values up to but excluding
613         *         <code>series</code> for <code>index</code>.
614         */
615        protected double getPreviousHeight(TableXYDataset dataset,
616                                           int series, int index) {
617            double result = 0.0;
618            for (int i = 0; i < series; i++) {
619                double value = dataset.getYValue(i, index);
620                if (!Double.isNaN(value)) {
621                    result += value;
622                }
623            }
624            return result;
625        }
626    
627        /**
628         * Tests the renderer for equality with an arbitrary object.
629         *
630         * @param obj  the object (<code>null</code> permitted).
631         *
632         * @return A boolean.
633         */
634        public boolean equals(Object obj) {
635            if (obj == this) {
636                return true;
637            }
638            if (!(obj instanceof StackedXYAreaRenderer) || !super.equals(obj)) {
639                return false;
640            }
641            StackedXYAreaRenderer that = (StackedXYAreaRenderer) obj;
642            if (!PaintUtilities.equal(this.shapePaint, that.shapePaint)) {
643                return false;
644            }
645            if (!ObjectUtilities.equal(this.shapeStroke, that.shapeStroke)) {
646                return false;
647            }
648            return true;
649        }
650    
651        /**
652         * Returns a clone of the renderer.
653         *
654         * @return A clone.
655         *
656         * @throws CloneNotSupportedException if the renderer cannot be cloned.
657         */
658        public Object clone() throws CloneNotSupportedException {
659            return super.clone();
660        }
661    
662        /**
663         * Provides serialization support.
664         *
665         * @param stream  the input stream.
666         *
667         * @throws IOException  if there is an I/O error.
668         * @throws ClassNotFoundException  if there is a classpath problem.
669         */
670        private void readObject(ObjectInputStream stream)
671                throws IOException, ClassNotFoundException {
672            stream.defaultReadObject();
673            this.shapePaint = SerialUtilities.readPaint(stream);
674            this.shapeStroke = SerialUtilities.readStroke(stream);
675        }
676    
677        /**
678         * Provides serialization support.
679         *
680         * @param stream  the output stream.
681         *
682         * @throws IOException  if there is an I/O error.
683         */
684        private void writeObject(ObjectOutputStream stream) throws IOException {
685            stream.defaultWriteObject();
686            SerialUtilities.writePaint(this.shapePaint, stream);
687            SerialUtilities.writeStroke(this.shapeStroke, stream);
688        }
689    
690    }