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    
057    package org.jfree.chart.renderer.xy;
058    
059    import java.awt.Graphics2D;
060    import java.awt.Paint;
061    import java.awt.Shape;
062    import java.awt.geom.GeneralPath;
063    import java.awt.geom.Rectangle2D;
064    import java.io.Serializable;
065    
066    import org.jfree.chart.axis.ValueAxis;
067    import org.jfree.chart.entity.EntityCollection;
068    import org.jfree.chart.event.RendererChangeEvent;
069    import org.jfree.chart.labels.XYToolTipGenerator;
070    import org.jfree.chart.plot.CrosshairState;
071    import org.jfree.chart.plot.PlotOrientation;
072    import org.jfree.chart.plot.PlotRenderingInfo;
073    import org.jfree.chart.plot.XYPlot;
074    import org.jfree.chart.urls.XYURLGenerator;
075    import org.jfree.data.Range;
076    import org.jfree.data.xy.TableXYDataset;
077    import org.jfree.data.xy.XYDataset;
078    import org.jfree.ui.RectangleEdge;
079    import 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     */
090    public 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    }