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