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