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