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