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