001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2008, 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 * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025 * in the United States and other countries.]
026 *
027 * ---------------------------
028 * StackedXYAreaRenderer2.java
029 * ---------------------------
030 * (C) Copyright 2004-2008, 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    public Range findRangeBounds(XYDataset dataset) {
167        if (dataset == null) {
168            return null;
169        }
170        double min = Double.POSITIVE_INFINITY;
171        double max = Double.NEGATIVE_INFINITY;
172        TableXYDataset d = (TableXYDataset) dataset;
173        int itemCount = d.getItemCount();
174        for (int i = 0; i < itemCount; i++) {
175            double[] stackValues = getStackValues((TableXYDataset) dataset,
176                    d.getSeriesCount(), i);
177            min = Math.min(min, stackValues[0]);
178            max = Math.max(max, stackValues[1]);
179        }
180        if (min == Double.POSITIVE_INFINITY) {
181            return null;
182        }
183        return new Range(min, max);
184    }
185
186    /**
187     * Returns the number of passes required by the renderer.
188     *
189     * @return 1.
190     */
191    public int getPassCount() {
192        return 1;
193    }
194
195    /**
196     * Draws the visual representation of a single data item.
197     *
198     * @param g2  the graphics device.
199     * @param state  the renderer state.
200     * @param dataArea  the area within which the data is being drawn.
201     * @param info  collects information about the drawing.
202     * @param plot  the plot (can be used to obtain standard color information
203     *              etc).
204     * @param domainAxis  the domain axis.
205     * @param rangeAxis  the range axis.
206     * @param dataset  the dataset.
207     * @param series  the series index (zero-based).
208     * @param item  the item index (zero-based).
209     * @param crosshairState  information about crosshairs on a plot.
210     * @param pass  the pass index.
211     */
212    public void drawItem(Graphics2D g2,
213                         XYItemRendererState state,
214                         Rectangle2D dataArea,
215                         PlotRenderingInfo info,
216                         XYPlot plot,
217                         ValueAxis domainAxis,
218                         ValueAxis rangeAxis,
219                         XYDataset dataset,
220                         int series,
221                         int item,
222                         CrosshairState crosshairState,
223                         int pass) {
224
225        // setup for collecting optional entity info...
226        Shape entityArea = null;
227        EntityCollection entities = null;
228        if (info != null) {
229            entities = info.getOwner().getEntityCollection();
230        }
231
232        TableXYDataset tdataset = (TableXYDataset) dataset;
233        PlotOrientation orientation = plot.getOrientation();
234
235        // get the data point...
236        double x1 = dataset.getXValue(series, item);
237        double y1 = dataset.getYValue(series, item);
238        if (Double.isNaN(y1)) {
239            y1 = 0.0;
240        }
241        double[] stack1 = getStackValues(tdataset, series, item);
242
243        // get the previous point and the next point so we can calculate a
244        // "hot spot" for the area (used by the chart entity)...
245        double x0 = dataset.getXValue(series, Math.max(item - 1, 0));
246        double y0 = dataset.getYValue(series, Math.max(item - 1, 0));
247        if (Double.isNaN(y0)) {
248            y0 = 0.0;
249        }
250        double[] stack0 = getStackValues(tdataset, series, Math.max(item - 1,
251                0));
252
253        int itemCount = dataset.getItemCount(series);
254        double x2 = dataset.getXValue(series, Math.min(item + 1,
255                itemCount - 1));
256        double y2 = dataset.getYValue(series, Math.min(item + 1,
257                itemCount - 1));
258        if (Double.isNaN(y2)) {
259            y2 = 0.0;
260        }
261        double[] stack2 = getStackValues(tdataset, series, Math.min(item + 1,
262                itemCount - 1));
263
264        double xleft = (x0 + x1) / 2.0;
265        double xright = (x1 + x2) / 2.0;
266        double[] stackLeft = averageStackValues(stack0, stack1);
267        double[] stackRight = averageStackValues(stack1, stack2);
268        double[] adjStackLeft = adjustedStackValues(stack0, stack1);
269        double[] adjStackRight = adjustedStackValues(stack1, stack2);
270
271        RectangleEdge edge0 = plot.getDomainAxisEdge();
272
273        float transX1 = (float) domainAxis.valueToJava2D(x1, dataArea, edge0);
274        float transXLeft = (float) domainAxis.valueToJava2D(xleft, dataArea,
275                edge0);
276        float transXRight = (float) domainAxis.valueToJava2D(xright, dataArea,
277                edge0);
278
279        if (this.roundXCoordinates) {
280            transX1 = Math.round(transX1);
281            transXLeft = Math.round(transXLeft);
282            transXRight = Math.round(transXRight);
283        }
284        float transY1;
285
286        RectangleEdge edge1 = plot.getRangeAxisEdge();
287
288        GeneralPath left = new GeneralPath();
289        GeneralPath right = new GeneralPath();
290        if (y1 >= 0.0) {  // handle positive value
291            transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1], dataArea,
292                    edge1);
293            float transStack1 = (float) rangeAxis.valueToJava2D(stack1[1],
294                    dataArea, edge1);
295            float transStackLeft = (float) rangeAxis.valueToJava2D(
296                    adjStackLeft[1], dataArea, edge1);
297
298            // LEFT POLYGON
299            if (y0 >= 0.0) {
300                double yleft = (y0 + y1) / 2.0 + stackLeft[1];
301                float transYLeft
302                    = (float) rangeAxis.valueToJava2D(yleft, dataArea, edge1);
303                if (orientation == PlotOrientation.VERTICAL) {
304                    left.moveTo(transX1, transY1);
305                    left.lineTo(transX1, transStack1);
306                    left.lineTo(transXLeft, transStackLeft);
307                    left.lineTo(transXLeft, transYLeft);
308                }
309                else {
310                    left.moveTo(transY1, transX1);
311                    left.lineTo(transStack1, transX1);
312                    left.lineTo(transStackLeft, transXLeft);
313                    left.lineTo(transYLeft, transXLeft);
314                }
315                left.closePath();
316            }
317            else {
318                if (orientation == PlotOrientation.VERTICAL) {
319                    left.moveTo(transX1, transStack1);
320                    left.lineTo(transX1, transY1);
321                    left.lineTo(transXLeft, transStackLeft);
322                }
323                else {
324                    left.moveTo(transStack1, transX1);
325                    left.lineTo(transY1, transX1);
326                    left.lineTo(transStackLeft, transXLeft);
327                }
328                left.closePath();
329            }
330
331            float transStackRight = (float) rangeAxis.valueToJava2D(
332                    adjStackRight[1], dataArea, edge1);
333            // RIGHT POLYGON
334            if (y2 >= 0.0) {
335                double yright = (y1 + y2) / 2.0 + stackRight[1];
336                float transYRight
337                    = (float) rangeAxis.valueToJava2D(yright, dataArea, edge1);
338                if (orientation == PlotOrientation.VERTICAL) {
339                    right.moveTo(transX1, transStack1);
340                    right.lineTo(transX1, transY1);
341                    right.lineTo(transXRight, transYRight);
342                    right.lineTo(transXRight, transStackRight);
343                }
344                else {
345                    right.moveTo(transStack1, transX1);
346                    right.lineTo(transY1, transX1);
347                    right.lineTo(transYRight, transXRight);
348                    right.lineTo(transStackRight, transXRight);
349                }
350                right.closePath();
351            }
352            else {
353                if (orientation == PlotOrientation.VERTICAL) {
354                    right.moveTo(transX1, transStack1);
355                    right.lineTo(transX1, transY1);
356                    right.lineTo(transXRight, transStackRight);
357                }
358                else {
359                    right.moveTo(transStack1, transX1);
360                    right.lineTo(transY1, transX1);
361                    right.lineTo(transStackRight, transXRight);
362                }
363                right.closePath();
364            }
365        }
366        else {  // handle negative value
367            transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0], dataArea,
368                    edge1);
369            float transStack1 = (float) rangeAxis.valueToJava2D(stack1[0],
370                    dataArea, edge1);
371            float transStackLeft = (float) rangeAxis.valueToJava2D(
372                    adjStackLeft[0], dataArea, edge1);
373
374            // LEFT POLYGON
375            if (y0 >= 0.0) {
376                if (orientation == PlotOrientation.VERTICAL) {
377                    left.moveTo(transX1, transStack1);
378                    left.lineTo(transX1, transY1);
379                    left.lineTo(transXLeft, transStackLeft);
380                }
381                else {
382                    left.moveTo(transStack1, transX1);
383                    left.lineTo(transY1, transX1);
384                    left.lineTo(transStackLeft, transXLeft);
385                }
386                left.clone();
387            }
388            else {
389                double yleft = (y0 + y1) / 2.0 + stackLeft[0];
390                float transYLeft = (float) rangeAxis.valueToJava2D(yleft,
391                        dataArea, edge1);
392                if (orientation == PlotOrientation.VERTICAL) {
393                    left.moveTo(transX1, transY1);
394                    left.lineTo(transX1, transStack1);
395                    left.lineTo(transXLeft, transStackLeft);
396                    left.lineTo(transXLeft, transYLeft);
397                }
398                else {
399                    left.moveTo(transY1, transX1);
400                    left.lineTo(transStack1, transX1);
401                    left.lineTo(transStackLeft, transXLeft);
402                    left.lineTo(transYLeft, transXLeft);
403                }
404                left.closePath();
405            }
406            float transStackRight = (float) rangeAxis.valueToJava2D(
407                    adjStackRight[0], dataArea, edge1);
408
409            // RIGHT POLYGON
410            if (y2 >= 0.0) {
411                if (orientation == PlotOrientation.VERTICAL) {
412                    right.moveTo(transX1, transStack1);
413                    right.lineTo(transX1, transY1);
414                    right.lineTo(transXRight, transStackRight);
415                }
416                else {
417                    right.moveTo(transStack1, transX1);
418                    right.lineTo(transY1, transX1);
419                    right.lineTo(transStackRight, transXRight);
420                }
421                right.closePath();
422            }
423            else {
424                double yright = (y1 + y2) / 2.0 + stackRight[0];
425                float transYRight = (float) rangeAxis.valueToJava2D(yright,
426                        dataArea, edge1);
427                if (orientation == PlotOrientation.VERTICAL) {
428                    right.moveTo(transX1, transStack1);
429                    right.lineTo(transX1, transY1);
430                    right.lineTo(transXRight, transYRight);
431                    right.lineTo(transXRight, transStackRight);
432                }
433                else {
434                    right.moveTo(transStack1, transX1);
435                    right.lineTo(transY1, transX1);
436                    right.lineTo(transYRight, transXRight);
437                    right.lineTo(transStackRight, transXRight);
438                }
439                right.closePath();
440            }
441        }
442
443        //  Get series Paint and Stroke
444        Paint itemPaint = getItemPaint(series, item);
445        if (pass == 0) {
446            g2.setPaint(itemPaint);
447            g2.fill(left);
448            g2.fill(right);
449        }
450
451        // add an entity for the item...
452        if (entities != null) {
453            GeneralPath gp = new GeneralPath(left);
454            gp.append(right, false);
455            entityArea = gp;
456            addEntity(entities, entityArea, dataset, series, item,
457                    transX1, transY1);
458        }
459
460    }
461
462    /**
463     * Calculates the stacked values (one positive and one negative) of all
464     * series up to, but not including, <code>series</code> for the specified
465     * item. It returns [0.0, 0.0] if <code>series</code> is the first series.
466     *
467     * @param dataset  the dataset (<code>null</code> not permitted).
468     * @param series  the series index.
469     * @param index  the item index.
470     *
471     * @return An array containing the cumulative negative and positive values
472     *     for all series values up to but excluding <code>series</code>
473     *     for <code>index</code>.
474     */
475    private double[] getStackValues(TableXYDataset dataset,
476                                    int series, int index) {
477        double[] result = new double[2];
478        for (int i = 0; i < series; i++) {
479            double v = dataset.getYValue(i, index);
480            if (!Double.isNaN(v)) {
481                if (v >= 0.0) {
482                    result[1] += v;
483                }
484                else {
485                    result[0] += v;
486                }
487            }
488        }
489        return result;
490    }
491
492    /**
493     * Returns a pair of "stack" values calculated as the mean of the two
494     * specified stack value pairs.
495     *
496     * @param stack1  the first stack pair.
497     * @param stack2  the second stack pair.
498     *
499     * @return A pair of average stack values.
500     */
501    private double[] averageStackValues(double[] stack1, double[] stack2) {
502        double[] result = new double[2];
503        result[0] = (stack1[0] + stack2[0]) / 2.0;
504        result[1] = (stack1[1] + stack2[1]) / 2.0;
505        return result;
506    }
507
508    /**
509     * Calculates adjusted stack values from the supplied values.  The value is
510     * the mean of the supplied values, unless either of the supplied values
511     * is zero, in which case the adjusted value is zero also.
512     *
513     * @param stack1  the first stack pair.
514     * @param stack2  the second stack pair.
515     *
516     * @return A pair of average stack values.
517     */
518    private double[] adjustedStackValues(double[] stack1, double[] stack2) {
519        double[] result = new double[2];
520        if (stack1[0] == 0.0 || stack2[0] == 0.0) {
521            result[0] = 0.0;
522        }
523        else {
524            result[0] = (stack1[0] + stack2[0]) / 2.0;
525        }
526        if (stack1[1] == 0.0 || stack2[1] == 0.0) {
527            result[1] = 0.0;
528        }
529        else {
530            result[1] = (stack1[1] + stack2[1]) / 2.0;
531        }
532        return result;
533    }
534
535    /**
536     * Tests this renderer for equality with an arbitrary object.
537     *
538     * @param obj  the object (<code>null</code> permitted).
539     *
540     * @return A boolean.
541     */
542    public boolean equals(Object obj) {
543        if (obj == this) {
544            return true;
545        }
546        if (!(obj instanceof StackedXYAreaRenderer2)) {
547            return false;
548        }
549        StackedXYAreaRenderer2 that = (StackedXYAreaRenderer2) obj;
550        if (this.roundXCoordinates != that.roundXCoordinates) {
551            return false;
552        }
553        return super.equals(obj);
554    }
555
556    /**
557     * Returns a clone of the renderer.
558     *
559     * @return A clone.
560     *
561     * @throws CloneNotSupportedException  if the renderer cannot be cloned.
562     */
563    public Object clone() throws CloneNotSupportedException {
564        return super.clone();
565    }
566
567}