001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2014, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * --------------------------
028 * StackedXYAreaRenderer.java
029 * --------------------------
030 * (C) Copyright 2003-2014, 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
069package org.jfree.chart.renderer.xy;
070
071import java.awt.Graphics2D;
072import java.awt.Paint;
073import java.awt.Point;
074import java.awt.Polygon;
075import java.awt.Shape;
076import java.awt.Stroke;
077import java.awt.geom.Line2D;
078import java.awt.geom.Rectangle2D;
079import java.io.IOException;
080import java.io.ObjectInputStream;
081import java.io.ObjectOutputStream;
082import java.io.Serializable;
083import java.util.Stack;
084
085import org.jfree.chart.axis.ValueAxis;
086import org.jfree.chart.entity.EntityCollection;
087import org.jfree.chart.entity.XYItemEntity;
088import org.jfree.chart.event.RendererChangeEvent;
089import org.jfree.chart.labels.XYToolTipGenerator;
090import org.jfree.chart.plot.CrosshairState;
091import org.jfree.chart.plot.PlotOrientation;
092import org.jfree.chart.plot.PlotRenderingInfo;
093import org.jfree.chart.plot.XYPlot;
094import org.jfree.chart.urls.XYURLGenerator;
095import org.jfree.data.Range;
096import org.jfree.data.general.DatasetUtilities;
097import org.jfree.data.xy.TableXYDataset;
098import org.jfree.data.xy.XYDataset;
099import org.jfree.io.SerialUtilities;
100import org.jfree.util.ObjectUtilities;
101import org.jfree.util.PaintUtilities;
102import org.jfree.util.PublicCloneable;
103import 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 */
119public 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, XYToolTipGenerator labelGenerator,
259            XYURLGenerator urlGenerator) {
260        super(type, labelGenerator, urlGenerator);
261    }
262
263    /**
264     * Returns the paint used for rendering shapes, or <code>null</code> if
265     * using series paints.
266     *
267     * @return The paint (possibly <code>null</code>).
268     *
269     * @see #setShapePaint(Paint)
270     */
271    public Paint getShapePaint() {
272        return this.shapePaint;
273    }
274
275    /**
276     * Sets the paint for rendering shapes and sends a
277     * {@link RendererChangeEvent} to all registered listeners.
278     *
279     * @param shapePaint  the paint (<code>null</code> permitted).
280     *
281     * @see #getShapePaint()
282     */
283    public void setShapePaint(Paint shapePaint) {
284        this.shapePaint = shapePaint;
285        fireChangeEvent();
286    }
287
288    /**
289     * Returns the stroke used for rendering shapes, or <code>null</code> if
290     * using series strokes.
291     *
292     * @return The stroke (possibly <code>null</code>).
293     *
294     * @see #setShapeStroke(Stroke)
295     */
296    public Stroke getShapeStroke() {
297        return this.shapeStroke;
298    }
299
300    /**
301     * Sets the stroke for rendering shapes and sends a
302     * {@link RendererChangeEvent} to all registered listeners.
303     *
304     * @param shapeStroke  the stroke (<code>null</code> permitted).
305     *
306     * @see #getShapeStroke()
307     */
308    public void setShapeStroke(Stroke shapeStroke) {
309        this.shapeStroke = shapeStroke;
310        fireChangeEvent();
311    }
312
313    /**
314     * Initialises the renderer. This method will be called before the first
315     * item is rendered, giving the renderer an opportunity to initialise any
316     * state information it wants to maintain.
317     *
318     * @param g2  the graphics device.
319     * @param dataArea  the area inside the axes.
320     * @param plot  the plot.
321     * @param data  the data.
322     * @param info  an optional info collection object to return data back to
323     *              the caller.
324     *
325     * @return A state object that should be passed to subsequent calls to the
326     *         drawItem() method.
327     */
328    @Override
329    public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea,
330            XYPlot plot, XYDataset data, PlotRenderingInfo info) {
331
332        XYItemRendererState state = new StackedXYAreaRendererState(info);
333        // in the rendering process, there is special handling for item
334        // zero, so we can't support processing of visible data items only
335        state.setProcessVisibleItemsOnly(false);
336        return state;
337    }
338
339    /**
340     * Returns the number of passes required by the renderer.
341     *
342     * @return 2.
343     */
344    @Override
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    @Override
362    public Range findRangeBounds(XYDataset dataset) {
363        if (dataset != null) {
364            return DatasetUtilities.findStackedRangeBounds(
365                (TableXYDataset) dataset);
366        }
367        else {
368            return null;
369        }
370    }
371
372    /**
373     * Draws the visual representation of a single data item.
374     *
375     * @param g2  the graphics device.
376     * @param state  the renderer state.
377     * @param dataArea  the area within which the data is being drawn.
378     * @param info  collects information about the drawing.
379     * @param plot  the plot (can be used to obtain standard color information
380     *              etc).
381     * @param domainAxis  the domain axis.
382     * @param rangeAxis  the range axis.
383     * @param dataset  the dataset.
384     * @param series  the series index (zero-based).
385     * @param item  the item index (zero-based).
386     * @param crosshairState  information about crosshairs on a plot.
387     * @param pass  the pass index.
388     *
389     * @throws ClassCastException if <code>state</code> is not an instance of
390     *         <code>StackedXYAreaRendererState</code> or <code>dataset</code>
391     *         is not an instance of {@link TableXYDataset}.
392     */
393    @Override
394    public void drawItem(Graphics2D g2, XYItemRendererState state,
395            Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
396            ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset,
397            int series, int item, CrosshairState crosshairState, int pass) {
398
399        PlotOrientation orientation = plot.getOrientation();
400        StackedXYAreaRendererState areaState
401            = (StackedXYAreaRendererState) state;
402        // Get the item count for the series, so that we can know which is the
403        // end of the series.
404        TableXYDataset tdataset = (TableXYDataset) dataset;
405        int itemCount = tdataset.getItemCount();
406
407        // get the data point...
408        double x1 = dataset.getXValue(series, item);
409        double y1 = dataset.getYValue(series, item);
410        boolean nullPoint = false;
411        if (Double.isNaN(y1)) {
412            y1 = 0.0;
413            nullPoint = true;
414        }
415
416        //  Get height adjustment based on stack and translate to Java2D values
417        double ph1 = getPreviousHeight(tdataset, series, item);
418        double transX1 = domainAxis.valueToJava2D(x1, dataArea,
419                plot.getDomainAxisEdge());
420        double transY1 = rangeAxis.valueToJava2D(y1 + ph1, dataArea,
421                plot.getRangeAxisEdge());
422
423        //  Get series Paint and Stroke
424        Paint seriesPaint = getItemPaint(series, item);
425        Paint seriesFillPaint = seriesPaint;
426        if (getUseFillPaint()) {
427            seriesFillPaint = getItemFillPaint(series, item);
428        }
429        Stroke seriesStroke = getItemStroke(series, item);
430
431        if (pass == 0) {
432            //  On first pass render the areas, line and outlines
433
434            if (item == 0) {
435                // Create a new Area for the series
436                areaState.setSeriesArea(new Polygon());
437                areaState.setLastSeriesPoints(
438                        areaState.getCurrentSeriesPoints());
439                areaState.setCurrentSeriesPoints(new Stack());
440
441                // start from previous height (ph1)
442                double transY2 = rangeAxis.valueToJava2D(ph1, dataArea,
443                        plot.getRangeAxisEdge());
444
445                // The first point is (x, 0)
446                if (orientation == PlotOrientation.VERTICAL) {
447                    areaState.getSeriesArea().addPoint((int) transX1,
448                            (int) transY2);
449                }
450                else if (orientation == PlotOrientation.HORIZONTAL) {
451                    areaState.getSeriesArea().addPoint((int) transY2,
452                            (int) transX1);
453                }
454            }
455
456            // Add each point to Area (x, y)
457            if (orientation == PlotOrientation.VERTICAL) {
458                Point point = new Point((int) transX1, (int) transY1);
459                areaState.getSeriesArea().addPoint((int) point.getX(),
460                        (int) point.getY());
461                areaState.getCurrentSeriesPoints().push(point);
462            }
463            else if (orientation == PlotOrientation.HORIZONTAL) {
464                areaState.getSeriesArea().addPoint((int) transY1,
465                        (int) transX1);
466            }
467
468            if (getPlotLines()) {
469                if (item > 0) {
470                    // get the previous data point...
471                    double x0 = dataset.getXValue(series, item - 1);
472                    double y0 = dataset.getYValue(series, item - 1);
473                    double ph0 = getPreviousHeight(tdataset, series, item - 1);
474                    double transX0 = domainAxis.valueToJava2D(x0, dataArea,
475                            plot.getDomainAxisEdge());
476                    double transY0 = rangeAxis.valueToJava2D(y0 + ph0,
477                            dataArea, plot.getRangeAxisEdge());
478
479                    if (orientation == PlotOrientation.VERTICAL) {
480                        areaState.getLine().setLine(transX0, transY0, transX1,
481                                transY1);
482                    }
483                    else if (orientation == PlotOrientation.HORIZONTAL) {
484                        areaState.getLine().setLine(transY0, transX0, transY1,
485                                transX1);
486                    }
487                    g2.setPaint(seriesPaint);
488                    g2.setStroke(seriesStroke);
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(seriesFillPaint);
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    @Override
639    public boolean equals(Object obj) {
640        if (obj == this) {
641            return true;
642        }
643        if (!(obj instanceof StackedXYAreaRenderer) || !super.equals(obj)) {
644            return false;
645        }
646        StackedXYAreaRenderer that = (StackedXYAreaRenderer) obj;
647        if (!PaintUtilities.equal(this.shapePaint, that.shapePaint)) {
648            return false;
649        }
650        if (!ObjectUtilities.equal(this.shapeStroke, that.shapeStroke)) {
651            return false;
652        }
653        return true;
654    }
655
656    /**
657     * Returns a clone of the renderer.
658     *
659     * @return A clone.
660     *
661     * @throws CloneNotSupportedException if the renderer cannot be cloned.
662     */
663    @Override
664    public Object clone() throws CloneNotSupportedException {
665        return super.clone();
666    }
667
668    /**
669     * Provides serialization support.
670     *
671     * @param stream  the input stream.
672     *
673     * @throws IOException  if there is an I/O error.
674     * @throws ClassNotFoundException  if there is a classpath problem.
675     */
676    private void readObject(ObjectInputStream stream)
677            throws IOException, ClassNotFoundException {
678        stream.defaultReadObject();
679        this.shapePaint = SerialUtilities.readPaint(stream);
680        this.shapeStroke = SerialUtilities.readStroke(stream);
681    }
682
683    /**
684     * Provides serialization support.
685     *
686     * @param stream  the output stream.
687     *
688     * @throws IOException  if there is an I/O error.
689     */
690    private void writeObject(ObjectOutputStream stream) throws IOException {
691        stream.defaultWriteObject();
692        SerialUtilities.writePaint(this.shapePaint, stream);
693        SerialUtilities.writeStroke(this.shapeStroke, stream);
694    }
695
696}