001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2020, 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 * StackedXYAreaRenderer2.java
029 * ---------------------------
030 * (C) Copyright 2004-2020, by Object Refinery Limited and Contributors.
031 *
032 * Original Author:  David Gilbert (for Object Refinery Limited), based on
033 *                   the StackedXYAreaRenderer class by Richard Atkinson;
034 * Contributor(s):   Ulrich Voigt (patch #312);
035 *
036 */
037
038package org.jfree.chart.renderer.xy;
039
040import java.awt.Graphics2D;
041import java.awt.Paint;
042import java.awt.geom.Area;
043import java.awt.geom.GeneralPath;
044import java.awt.geom.Rectangle2D;
045import java.io.Serializable;
046
047import org.jfree.chart.axis.ValueAxis;
048import org.jfree.chart.entity.EntityCollection;
049import org.jfree.chart.event.RendererChangeEvent;
050import org.jfree.chart.labels.XYToolTipGenerator;
051import org.jfree.chart.plot.CrosshairState;
052import org.jfree.chart.plot.PlotOrientation;
053import org.jfree.chart.plot.PlotRenderingInfo;
054import org.jfree.chart.plot.XYPlot;
055import org.jfree.chart.ui.RectangleEdge;
056import org.jfree.chart.urls.XYURLGenerator;
057import org.jfree.chart.util.PublicCloneable;
058import org.jfree.data.Range;
059import org.jfree.data.xy.TableXYDataset;
060import org.jfree.data.xy.XYDataset;
061
062/**
063 * A stacked area renderer for the {@link XYPlot} class.
064 * The example shown here is generated by the
065 * {@code StackedXYAreaChartDemo2.java} program included in the
066 * JFreeChart demo collection:
067 * <br><br>
068 * <img src="doc-files/StackedXYAreaRenderer2Sample.png"
069 * alt="StackedXYAreaRenderer2Sample.png">
070 */
071public class StackedXYAreaRenderer2 extends XYAreaRenderer2
072        implements Cloneable, PublicCloneable, Serializable {
073
074    /** For serialization. */
075    private static final long serialVersionUID = 7752676509764539182L;
076
077    /**
078     * This flag controls whether or not the x-coordinates (in Java2D space)
079     * are rounded to integers.  When set to true, this can avoid the vertical
080     * striping that anti-aliasing can generate.  However, the rounding may not
081     * be appropriate for output in high resolution formats (for example,
082     * vector graphics formats such as SVG and PDF).
083     *
084     * @since 1.0.3
085     */
086    private boolean roundXCoordinates;
087
088    /**
089     * Creates a new renderer.
090     */
091    public StackedXYAreaRenderer2() {
092        this(null, null);
093    }
094
095    /**
096     * Constructs a new renderer.
097     *
098     * @param labelGenerator  the tool tip generator to use ({@code null} 
099     *     permitted).
100     * @param urlGenerator  the URL generator ({@code null} permitted).
101     */
102    public StackedXYAreaRenderer2(XYToolTipGenerator labelGenerator,
103                                  XYURLGenerator urlGenerator) {
104        super(labelGenerator, urlGenerator);
105        this.roundXCoordinates = true;
106    }
107
108    /**
109     * Returns the flag that controls whether or not the x-coordinates (in
110     * Java2D space) are rounded to integer values.
111     *
112     * @return The flag.
113     *
114     * @since 1.0.4
115     *
116     * @see #setRoundXCoordinates(boolean)
117     */
118    public boolean getRoundXCoordinates() {
119        return this.roundXCoordinates;
120    }
121
122    /**
123     * Sets the flag that controls whether or not the x-coordinates (in
124     * Java2D space) are rounded to integer values, and sends a
125     * {@link RendererChangeEvent} to all registered listeners.
126     *
127     * @param round  the new flag value.
128     *
129     * @since 1.0.4
130     *
131     * @see #getRoundXCoordinates()
132     */
133    public void setRoundXCoordinates(boolean round) {
134        this.roundXCoordinates = round;
135        fireChangeEvent();
136    }
137
138    /**
139     * Returns the range of values the renderer requires to display all the
140     * items from the specified dataset.
141     *
142     * @param dataset  the dataset ({@code null} permitted).
143     *
144     * @return The range (or {@code null} if the dataset is {@code null} or 
145     *     empty).
146     */
147    @Override
148    public Range findRangeBounds(XYDataset dataset) {
149        if (dataset == null) {
150            return null;
151        }
152        double min = Double.POSITIVE_INFINITY;
153        double max = Double.NEGATIVE_INFINITY;
154        TableXYDataset d = (TableXYDataset) dataset;
155        int itemCount = d.getItemCount();
156        for (int i = 0; i < itemCount; i++) {
157            double[] stackValues = getStackValues((TableXYDataset) dataset,
158                    d.getSeriesCount(), i);
159            min = Math.min(min, stackValues[0]);
160            max = Math.max(max, stackValues[1]);
161        }
162        if (min == Double.POSITIVE_INFINITY) {
163            return null;
164        }
165        return new Range(min, max);
166    }
167
168    /**
169     * Returns the number of passes required by the renderer.
170     *
171     * @return 1.
172     */
173    @Override
174    public int getPassCount() {
175        return 1;
176    }
177
178    /**
179     * Draws the visual representation of a single data item.
180     *
181     * @param g2  the graphics device.
182     * @param state  the renderer state.
183     * @param dataArea  the area within which the data is being drawn.
184     * @param info  collects information about the drawing.
185     * @param plot  the plot (can be used to obtain standard color information
186     *              etc).
187     * @param domainAxis  the domain axis.
188     * @param rangeAxis  the range axis.
189     * @param dataset  the dataset.
190     * @param series  the series index (zero-based).
191     * @param item  the item index (zero-based).
192     * @param crosshairState  information about crosshairs on a plot.
193     * @param pass  the pass index.
194     */
195    @Override
196    public void drawItem(Graphics2D g2, XYItemRendererState state,
197            Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot,
198            ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset,
199            int series, int item, CrosshairState crosshairState, int pass) {
200
201        // setup for collecting optional entity info...
202        EntityCollection entities = null;
203        if (info != null) {
204            entities = info.getOwner().getEntityCollection();
205        }
206
207        TableXYDataset tdataset = (TableXYDataset) dataset;
208        PlotOrientation orientation = plot.getOrientation();
209
210        // get the data point...
211        double x1 = dataset.getXValue(series, item);
212        double y1 = dataset.getYValue(series, item);
213        if (Double.isNaN(y1)) {
214            y1 = 0.0;
215        }
216        double[] stack1 = getStackValues(tdataset, series, item);
217
218        // get the previous point and the next point so we can calculate a
219        // "hot spot" for the area (used by the chart entity)...
220        double x0 = dataset.getXValue(series, Math.max(item - 1, 0));
221        double y0 = dataset.getYValue(series, Math.max(item - 1, 0));
222        if (Double.isNaN(y0)) {
223            y0 = 0.0;
224        }
225        double[] stack0 = getStackValues(tdataset, series, Math.max(item - 1,
226                0));
227
228        int itemCount = dataset.getItemCount(series);
229        double x2 = dataset.getXValue(series, Math.min(item + 1,
230                itemCount - 1));
231        double y2 = dataset.getYValue(series, Math.min(item + 1,
232                itemCount - 1));
233        if (Double.isNaN(y2)) {
234            y2 = 0.0;
235        }
236        double[] stack2 = getStackValues(tdataset, series, Math.min(item + 1,
237                itemCount - 1));
238
239        double xleft = (x0 + x1) / 2.0;
240        double xright = (x1 + x2) / 2.0;
241        double[] stackLeft = averageStackValues(stack0, stack1);
242        double[] stackRight = averageStackValues(stack1, stack2);
243        double[] adjStackLeft = adjustedStackValues(stack0, stack1);
244        double[] adjStackRight = adjustedStackValues(stack1, stack2);
245
246        RectangleEdge edge0 = plot.getDomainAxisEdge();
247
248        float transX1 = (float) domainAxis.valueToJava2D(x1, dataArea, edge0);
249        float transXLeft = (float) domainAxis.valueToJava2D(xleft, dataArea,
250                edge0);
251        float transXRight = (float) domainAxis.valueToJava2D(xright, dataArea,
252                edge0);
253
254        if (this.roundXCoordinates) {
255            transX1 = Math.round(transX1);
256            transXLeft = Math.round(transXLeft);
257            transXRight = Math.round(transXRight);
258        }
259        float transY1;
260
261        RectangleEdge edge1 = plot.getRangeAxisEdge();
262
263        GeneralPath left = new GeneralPath();
264        GeneralPath right = new GeneralPath();
265        if (y1 >= 0.0) {  // handle positive value
266            transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1], dataArea,
267                    edge1);
268            float transStack1 = (float) rangeAxis.valueToJava2D(stack1[1],
269                    dataArea, edge1);
270            float transStackLeft = (float) rangeAxis.valueToJava2D(
271                    adjStackLeft[1], dataArea, edge1);
272
273            // LEFT POLYGON
274            if (y0 >= 0.0) {
275                double yleft = (y0 + y1) / 2.0 + stackLeft[1];
276                float transYLeft
277                    = (float) rangeAxis.valueToJava2D(yleft, dataArea, edge1);
278                if (orientation == PlotOrientation.VERTICAL) {
279                    left.moveTo(transX1, transY1);
280                    left.lineTo(transX1, transStack1);
281                    left.lineTo(transXLeft, transStackLeft);
282                    left.lineTo(transXLeft, transYLeft);
283                } else {
284                    left.moveTo(transY1, transX1);
285                    left.lineTo(transStack1, transX1);
286                    left.lineTo(transStackLeft, transXLeft);
287                    left.lineTo(transYLeft, transXLeft);
288                }
289                left.closePath();
290            } else {
291                if (orientation == PlotOrientation.VERTICAL) {
292                    left.moveTo(transX1, transStack1);
293                    left.lineTo(transX1, transY1);
294                    left.lineTo(transXLeft, transStackLeft);
295                } else {
296                    left.moveTo(transStack1, transX1);
297                    left.lineTo(transY1, transX1);
298                    left.lineTo(transStackLeft, transXLeft);
299                }
300                left.closePath();
301            }
302
303            float transStackRight = (float) rangeAxis.valueToJava2D(
304                    adjStackRight[1], dataArea, edge1);
305            // RIGHT POLYGON
306            if (y2 >= 0.0) {
307                double yright = (y1 + y2) / 2.0 + stackRight[1];
308                float transYRight
309                    = (float) rangeAxis.valueToJava2D(yright, dataArea, edge1);
310                if (orientation == PlotOrientation.VERTICAL) {
311                    right.moveTo(transX1, transStack1);
312                    right.lineTo(transX1, transY1);
313                    right.lineTo(transXRight, transYRight);
314                    right.lineTo(transXRight, transStackRight);
315                } else {
316                    right.moveTo(transStack1, transX1);
317                    right.lineTo(transY1, transX1);
318                    right.lineTo(transYRight, transXRight);
319                    right.lineTo(transStackRight, transXRight);
320                }
321                right.closePath();
322            }
323            else {
324                if (orientation == PlotOrientation.VERTICAL) {
325                    right.moveTo(transX1, transStack1);
326                    right.lineTo(transX1, transY1);
327                    right.lineTo(transXRight, transStackRight);
328                } else {
329                    right.moveTo(transStack1, transX1);
330                    right.lineTo(transY1, transX1);
331                    right.lineTo(transStackRight, transXRight);
332                }
333                right.closePath();
334            }
335        }
336        else {  // handle negative value
337            transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0], dataArea,
338                    edge1);
339            float transStack1 = (float) rangeAxis.valueToJava2D(stack1[0],
340                    dataArea, edge1);
341            float transStackLeft = (float) rangeAxis.valueToJava2D(
342                    adjStackLeft[0], dataArea, edge1);
343
344            // LEFT POLYGON
345            if (y0 >= 0.0) {
346                if (orientation == PlotOrientation.VERTICAL) {
347                    left.moveTo(transX1, transStack1);
348                    left.lineTo(transX1, transY1);
349                    left.lineTo(transXLeft, transStackLeft);
350                } else {
351                    left.moveTo(transStack1, transX1);
352                    left.lineTo(transY1, transX1);
353                    left.lineTo(transStackLeft, transXLeft);
354                }
355                left.clone();
356            } else {
357                double yleft = (y0 + y1) / 2.0 + stackLeft[0];
358                float transYLeft = (float) rangeAxis.valueToJava2D(yleft,
359                        dataArea, edge1);
360                if (orientation == PlotOrientation.VERTICAL) {
361                    left.moveTo(transX1, transY1);
362                    left.lineTo(transX1, transStack1);
363                    left.lineTo(transXLeft, transStackLeft);
364                    left.lineTo(transXLeft, transYLeft);
365                } else {
366                    left.moveTo(transY1, transX1);
367                    left.lineTo(transStack1, transX1);
368                    left.lineTo(transStackLeft, transXLeft);
369                    left.lineTo(transYLeft, transXLeft);
370                }
371                left.closePath();
372            }
373            float transStackRight = (float) rangeAxis.valueToJava2D(
374                    adjStackRight[0], dataArea, edge1);
375
376            // RIGHT POLYGON
377            if (y2 >= 0.0) {
378                if (orientation == PlotOrientation.VERTICAL) {
379                    right.moveTo(transX1, transStack1);
380                    right.lineTo(transX1, transY1);
381                    right.lineTo(transXRight, transStackRight);
382                } else {
383                    right.moveTo(transStack1, transX1);
384                    right.lineTo(transY1, transX1);
385                    right.lineTo(transStackRight, transXRight);
386                }
387                right.closePath();
388            } else {
389                double yright = (y1 + y2) / 2.0 + stackRight[0];
390                float transYRight = (float) rangeAxis.valueToJava2D(yright,
391                        dataArea, edge1);
392                if (orientation == PlotOrientation.VERTICAL) {
393                    right.moveTo(transX1, transStack1);
394                    right.lineTo(transX1, transY1);
395                    right.lineTo(transXRight, transYRight);
396                    right.lineTo(transXRight, transStackRight);
397                } else {
398                    right.moveTo(transStack1, transX1);
399                    right.lineTo(transY1, transX1);
400                    right.lineTo(transYRight, transXRight);
401                    right.lineTo(transStackRight, transXRight);
402                }
403                right.closePath();
404            }
405        }
406
407        //  Get series Paint and Stroke
408        Paint itemPaint = getItemPaint(series, item);
409        if (pass == 0) {
410            g2.setPaint(itemPaint);
411            g2.fill(left);
412            g2.fill(right);
413        }
414
415        // add an entity for the item...
416        if (entities != null) {
417            // Create the entity area and limit it to the data area
418            Area dataAreaHotspot = new Area(left);
419            dataAreaHotspot.add(new Area(right));
420            dataAreaHotspot.intersect(new Area(dataArea));
421
422            if (!dataAreaHotspot.isEmpty()) {
423                addEntity(entities, dataAreaHotspot, dataset, series, item,
424                     0.0, 0.0);
425            }
426        }
427    }
428
429    /**
430     * Calculates the stacked values (one positive and one negative) of all
431     * series up to, but not including, {@code series} for the specified
432     * item. It returns [0.0, 0.0] if {@code series} is the first series.
433     *
434     * @param dataset  the dataset ({@code null} not permitted).
435     * @param series  the series index.
436     * @param index  the item index.
437     *
438     * @return An array containing the cumulative negative and positive values
439     *     for all series values up to but excluding {@code series}
440     *     for {@code index}.
441     */
442    private double[] getStackValues(TableXYDataset dataset,
443                                    int series, int index) {
444        double[] result = new double[2];
445        for (int i = 0; i < series; i++) {
446            double v = dataset.getYValue(i, index);
447            if (!Double.isNaN(v)) {
448                if (v >= 0.0) {
449                    result[1] += v;
450                }
451                else {
452                    result[0] += v;
453                }
454            }
455        }
456        return result;
457    }
458
459    /**
460     * Returns a pair of "stack" values calculated as the mean of the two
461     * specified stack value pairs.
462     *
463     * @param stack1  the first stack pair.
464     * @param stack2  the second stack pair.
465     *
466     * @return A pair of average stack values.
467     */
468    private double[] averageStackValues(double[] stack1, double[] stack2) {
469        double[] result = new double[2];
470        result[0] = (stack1[0] + stack2[0]) / 2.0;
471        result[1] = (stack1[1] + stack2[1]) / 2.0;
472        return result;
473    }
474
475    /**
476     * Calculates adjusted stack values from the supplied values.  The value is
477     * the mean of the supplied values, unless either of the supplied values
478     * is zero, in which case the adjusted value is zero also.
479     *
480     * @param stack1  the first stack pair.
481     * @param stack2  the second stack pair.
482     *
483     * @return A pair of average stack values.
484     */
485    private double[] adjustedStackValues(double[] stack1, double[] stack2) {
486        double[] result = new double[2];
487        if (stack1[0] == 0.0 || stack2[0] == 0.0) {
488            result[0] = 0.0;
489        }
490        else {
491            result[0] = (stack1[0] + stack2[0]) / 2.0;
492        }
493        if (stack1[1] == 0.0 || stack2[1] == 0.0) {
494            result[1] = 0.0;
495        }
496        else {
497            result[1] = (stack1[1] + stack2[1]) / 2.0;
498        }
499        return result;
500    }
501
502    /**
503     * Tests this renderer for equality with an arbitrary object.
504     *
505     * @param obj  the object ({@code null} permitted).
506     *
507     * @return A boolean.
508     */
509    @Override
510    public boolean equals(Object obj) {
511        if (obj == this) {
512            return true;
513        }
514        if (!(obj instanceof StackedXYAreaRenderer2)) {
515            return false;
516        }
517        StackedXYAreaRenderer2 that = (StackedXYAreaRenderer2) obj;
518        if (this.roundXCoordinates != that.roundXCoordinates) {
519            return false;
520        }
521        return super.equals(obj);
522    }
523
524    /**
525     * Returns a clone of the renderer.
526     *
527     * @return A clone.
528     *
529     * @throws CloneNotSupportedException  if the renderer cannot be cloned.
530     */
531    @Override
532    public Object clone() throws CloneNotSupportedException {
533        return super.clone();
534    }
535
536}