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 * StackedAreaRenderer.java
029 * ------------------------
030 * (C) Copyright 2002-2007, by Dan Rivett (d.rivett@ukonline.co.uk) and 
031 *                          Contributors.
032 *
033 * Original Author:  Dan Rivett (adapted from AreaCategoryItemRenderer);
034 * Contributor(s):   Jon Iles;
035 *                   David Gilbert (for Object Refinery Limited);
036 *                   Christian W. Zuckschwerdt;
037 *
038 * Changes:
039 * --------
040 * 20-Sep-2002 : Version 1, contributed by Dan Rivett;
041 * 24-Oct-2002 : Amendments for changes in CategoryDataset interface and 
042 *               CategoryToolTipGenerator interface (DG);
043 * 01-Nov-2002 : Added tooltips (DG);
044 * 06-Nov-2002 : Renamed drawCategoryItem() --> drawItem() and now using axis 
045 *               for category spacing. Renamed StackedAreaCategoryItemRenderer 
046 *               --> StackedAreaRenderer (DG);
047 * 26-Nov-2002 : Switched CategoryDataset --> TableDataset (DG);
048 * 26-Nov-2002 : Replaced isStacked() method with getRangeType() method (DG);
049 * 17-Jan-2003 : Moved plot classes to a separate package (DG);
050 * 25-Mar-2003 : Implemented Serializable (DG);
051 * 13-May-2003 : Modified to take into account the plot orientation (DG);
052 * 30-Jul-2003 : Modified entity constructor (CZ);
053 * 07-Oct-2003 : Added renderer state (DG);
054 * 29-Apr-2004 : Added getRangeExtent() override (DG);
055 * 05-Nov-2004 : Modified drawItem() signature (DG);
056 * 07-Jan-2005 : Renamed getRangeExtent() --> findRangeBounds() (DG);
057 * ------------- JFREECHART 1.0.x ---------------------------------------------
058 * 11-Oct-2006 : Added support for rendering data values as percentages,
059 *               and added a second pass for drawing item labels (DG);
060 * 
061 */
062
063package org.jfree.chart.renderer.category;
064
065import java.awt.Graphics2D;
066import java.awt.Paint;
067import java.awt.Shape;
068import java.awt.geom.GeneralPath;
069import java.awt.geom.Rectangle2D;
070import java.io.Serializable;
071
072import org.jfree.chart.axis.CategoryAxis;
073import org.jfree.chart.axis.ValueAxis;
074import org.jfree.chart.entity.EntityCollection;
075import org.jfree.chart.event.RendererChangeEvent;
076import org.jfree.chart.plot.CategoryPlot;
077import org.jfree.data.DataUtilities;
078import org.jfree.data.Range;
079import org.jfree.data.category.CategoryDataset;
080import org.jfree.data.general.DatasetUtilities;
081import org.jfree.ui.RectangleEdge;
082import org.jfree.util.PublicCloneable;
083
084/**
085 * A renderer that draws stacked area charts for a 
086 * {@link org.jfree.chart.plot.CategoryPlot}.
087 */
088public class StackedAreaRenderer extends AreaRenderer 
089                                 implements Cloneable, PublicCloneable, 
090                                            Serializable {
091
092    /** For serialization. */
093    private static final long serialVersionUID = -3595635038460823663L;
094     
095    /** A flag that controls whether the areas display values or percentages. */
096    private boolean renderAsPercentages;
097    
098    /**
099     * Creates a new renderer.
100     */
101    public StackedAreaRenderer() {
102        this(false);
103    }
104    
105    /**
106     * Creates a new renderer.
107     * 
108     * @param renderAsPercentages  a flag that controls whether the data values
109     *                             are rendered as percentages.
110     */
111    public StackedAreaRenderer(boolean renderAsPercentages) {
112        super();
113        this.renderAsPercentages = renderAsPercentages;
114    }
115
116    /**
117     * Returns <code>true</code> if the renderer displays each item value as
118     * a percentage (so that the stacked areas add to 100%), and 
119     * <code>false</code> otherwise.
120     * 
121     * @return A boolean.
122     *
123     * @since 1.0.3
124     */
125    public boolean getRenderAsPercentages() {
126        return this.renderAsPercentages;   
127    }
128    
129    /**
130     * Sets the flag that controls whether the renderer displays each item
131     * value as a percentage (so that the stacked areas add to 100%), and sends
132     * a {@link RendererChangeEvent} to all registered listeners.
133     * 
134     * @param asPercentages  the flag.
135     *
136     * @since 1.0.3
137     */
138    public void setRenderAsPercentages(boolean asPercentages) {
139        this.renderAsPercentages = asPercentages; 
140        notifyListeners(new RendererChangeEvent(this));
141    }
142    
143    /**
144     * Returns the number of passes (<code>2</code>) required by this renderer. 
145     * The first pass is used to draw the bars, the second pass is used to
146     * draw the item labels (if visible).
147     * 
148     * @return The number of passes required by the renderer.
149     */
150    public int getPassCount() {
151        return 2;
152    }
153
154    /**
155     * Returns the range of values the renderer requires to display all the 
156     * items from the specified dataset.
157     * 
158     * @param dataset  the dataset (<code>null</code> not permitted).
159     * 
160     * @return The range (or <code>null</code> if the dataset is empty).
161     */
162    public Range findRangeBounds(CategoryDataset dataset) {
163        if (this.renderAsPercentages) {
164            return new Range(0.0, 1.0);   
165        }
166        else {
167            return DatasetUtilities.findStackedRangeBounds(dataset);
168        }
169    }
170
171    /**
172     * Draw a single data item.
173     *
174     * @param g2  the graphics device.
175     * @param state  the renderer state.
176     * @param dataArea  the data plot area.
177     * @param plot  the plot.
178     * @param domainAxis  the domain axis.
179     * @param rangeAxis  the range axis.
180     * @param dataset  the data.
181     * @param row  the row index (zero-based).
182     * @param column  the column index (zero-based).
183     * @param pass  the pass index.
184     */
185    public void drawItem(Graphics2D g2,
186                         CategoryItemRendererState state,
187                         Rectangle2D dataArea,
188                         CategoryPlot plot,
189                         CategoryAxis domainAxis,
190                         ValueAxis rangeAxis,
191                         CategoryDataset dataset,
192                         int row,
193                         int column,
194                         int pass) {
195
196        // setup for collecting optional entity info...
197        Shape entityArea = null;
198        EntityCollection entities = state.getEntityCollection();
199        
200        double y1 = 0.0;
201        Number n = dataset.getValue(row, column);
202        if (n != null) {
203            y1 = n.doubleValue();
204        }        
205        double[] stack1 = getStackValues(dataset, row, column);
206
207
208        // leave the y values (y1, y0) untranslated as it is going to be be 
209        // stacked up later by previous series values, after this it will be 
210        // translated.
211        double xx1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 
212                dataArea, plot.getDomainAxisEdge());
213        
214        
215        // get the previous point and the next point so we can calculate a 
216        // "hot spot" for the area (used by the chart entity)...
217        double y0 = 0.0;
218        n = dataset.getValue(row, Math.max(column - 1, 0));
219        if (n != null) {
220            y0 = n.doubleValue();
221        }
222        double[] stack0 = getStackValues(dataset, row, Math.max(column - 1, 0));
223
224        // FIXME: calculate xx0
225        double xx0 = domainAxis.getCategoryStart(column, getColumnCount(), 
226                dataArea, plot.getDomainAxisEdge());
227        
228        int itemCount = dataset.getColumnCount();
229        double y2 = 0.0;
230        n = dataset.getValue(row, Math.min(column + 1, itemCount - 1));
231        if (n != null) {
232            y2 = n.doubleValue();
233        }
234        double[] stack2 = getStackValues(dataset, row, Math.min(column + 1, 
235                itemCount - 1));
236
237        double xx2 = domainAxis.getCategoryEnd(column, getColumnCount(), 
238                dataArea, plot.getDomainAxisEdge());
239        
240        // FIXME: calculate xxLeft and xxRight
241        double xxLeft = xx0;
242        double xxRight = xx2;
243        
244        double[] stackLeft = averageStackValues(stack0, stack1);
245        double[] stackRight = averageStackValues(stack1, stack2);
246        double[] adjStackLeft = adjustedStackValues(stack0, stack1);
247        double[] adjStackRight = adjustedStackValues(stack1, stack2);
248
249        float transY1;
250        
251        RectangleEdge edge1 = plot.getRangeAxisEdge();
252        
253        GeneralPath left = new GeneralPath();
254        GeneralPath right = new GeneralPath();
255        if (y1 >= 0.0) {  // handle positive value
256            transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1], dataArea, 
257                    edge1);
258            float transStack1 = (float) rangeAxis.valueToJava2D(stack1[1], 
259                    dataArea, edge1);
260            float transStackLeft = (float) rangeAxis.valueToJava2D(
261                    adjStackLeft[1], dataArea, edge1);
262            
263            // LEFT POLYGON
264            if (y0 >= 0.0) {
265                double yleft = (y0 + y1) / 2.0 + stackLeft[1];
266                float transYLeft 
267                    = (float) rangeAxis.valueToJava2D(yleft, dataArea, edge1);
268                left.moveTo((float) xx1, transY1);
269                left.lineTo((float) xx1, transStack1);
270                left.lineTo((float) xxLeft, transStackLeft);
271                left.lineTo((float) xxLeft, transYLeft);
272                left.closePath();
273            }
274            else {
275                left.moveTo((float) xx1, transStack1);
276                left.lineTo((float) xx1, transY1);
277                left.lineTo((float) xxLeft, transStackLeft);
278                left.closePath();
279            }
280
281            float transStackRight = (float) rangeAxis.valueToJava2D(
282                    adjStackRight[1], dataArea, edge1);
283            // RIGHT POLYGON
284            if (y2 >= 0.0) {
285                double yright = (y1 + y2) / 2.0 + stackRight[1];
286                float transYRight 
287                    = (float) rangeAxis.valueToJava2D(yright, dataArea, edge1);
288                right.moveTo((float) xx1, transStack1);
289                right.lineTo((float) xx1, transY1);
290                right.lineTo((float) xxRight, transYRight);
291                right.lineTo((float) xxRight, transStackRight);
292                right.closePath();
293            }
294            else {
295                right.moveTo((float) xx1, transStack1);
296                right.lineTo((float) xx1, transY1);
297                right.lineTo((float) xxRight, transStackRight);
298                right.closePath();
299            }
300        }
301        else {  // handle negative value 
302            transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0], dataArea,
303                    edge1);
304            float transStack1 = (float) rangeAxis.valueToJava2D(stack1[0], 
305                    dataArea, edge1);
306            float transStackLeft = (float) rangeAxis.valueToJava2D(
307                    adjStackLeft[0], dataArea, edge1);
308
309            // LEFT POLYGON
310            if (y0 >= 0.0) {
311                left.moveTo((float) xx1, transStack1);
312                left.lineTo((float) xx1, transY1);
313                left.lineTo((float) xxLeft, transStackLeft);
314                left.clone();
315            }
316            else {
317                double yleft = (y0 + y1) / 2.0 + stackLeft[0];
318                float transYLeft = (float) rangeAxis.valueToJava2D(yleft, 
319                        dataArea, edge1);
320                left.moveTo((float) xx1, transY1);
321                left.lineTo((float) xx1, transStack1);
322                left.lineTo((float) xxLeft, transStackLeft);
323                left.lineTo((float) xxLeft, transYLeft);
324                left.closePath();
325            }
326            float transStackRight = (float) rangeAxis.valueToJava2D(
327                    adjStackRight[0], dataArea, edge1);
328            
329            // RIGHT POLYGON
330            if (y2 >= 0.0) {
331                right.moveTo((float) xx1, transStack1);
332                right.lineTo((float) xx1, transY1);
333                right.lineTo((float) xxRight, transStackRight);
334                right.closePath();
335            }
336            else {
337                double yright = (y1 + y2) / 2.0 + stackRight[0];
338                float transYRight = (float) rangeAxis.valueToJava2D(yright, 
339                        dataArea, edge1);
340                right.moveTo((float) xx1, transStack1);
341                right.lineTo((float) xx1, transY1);
342                right.lineTo((float) xxRight, transYRight);
343                right.lineTo((float) xxRight, transStackRight);
344                right.closePath();
345            }
346        }
347
348        g2.setPaint(getItemPaint(row, column));
349        g2.setStroke(getItemStroke(row, column));
350
351        //  Get series Paint and Stroke
352        Paint itemPaint = getItemPaint(row, column);
353        if (pass == 0) {
354            g2.setPaint(itemPaint);
355            g2.fill(left);
356            g2.fill(right);
357        } 
358        
359        // add an entity for the item...
360        if (entities != null) {
361            GeneralPath gp = new GeneralPath(left);
362            gp.append(right, false);
363            entityArea = gp;
364            addItemEntity(entities, dataset, row, column, entityArea);
365        }
366        
367    }
368
369    /**
370     * Calculates the stacked value of the all series up to, but not including 
371     * <code>series</code> for the specified category, <code>category</code>.  
372     * It returns 0.0 if <code>series</code> is the first series, i.e. 0.
373     *
374     * @param dataset  the dataset (<code>null</code> not permitted).
375     * @param series  the series.
376     * @param category  the category.
377     *
378     * @return double returns a cumulative value for all series' values up to 
379     *         but excluding <code>series</code> for Object 
380     *         <code>category</code>.
381     */
382    protected double getPreviousHeight(CategoryDataset dataset, 
383                                       int series, int category) {
384
385        double result = 0.0;
386        Number n;
387        double total = 0.0;
388        if (this.renderAsPercentages) {
389            total = DataUtilities.calculateColumnTotal(dataset, category);
390        }
391        for (int i = 0; i < series; i++) {
392            n = dataset.getValue(i, category);
393            if (n != null) {
394                double v = n.doubleValue();
395                if (this.renderAsPercentages) {
396                    v = v / total;
397                }
398                result += v;
399            }
400        }
401        return result;
402
403    }
404
405    /**
406     * Calculates the stacked values (one positive and one negative) of all 
407     * series up to, but not including, <code>series</code> for the specified 
408     * item. It returns [0.0, 0.0] if <code>series</code> is the first series.
409     *
410     * @param dataset  the dataset (<code>null</code> not permitted).
411     * @param series  the series index.
412     * @param index  the item index.
413     *
414     * @return An array containing the cumulative negative and positive values
415     *     for all series values up to but excluding <code>series</code> 
416     *     for <code>index</code>.
417     */
418    protected double[] getStackValues(CategoryDataset dataset, 
419            int series, int index) {
420        double[] result = new double[2];
421        for (int i = 0; i < series; i++) {
422            if (isSeriesVisible(i)) {
423                double v = 0.0;
424                Number n = dataset.getValue(i, index);
425                if (n != null) {
426                    v = n.doubleValue();
427                }
428                if (!Double.isNaN(v)) {
429                    if (v >= 0.0) {
430                        result[1] += v;   
431                    }
432                    else {
433                        result[0] += v;   
434                    }
435                }
436            }
437        }
438        return result;
439    }
440
441    /**
442     * Returns a pair of "stack" values calculated as the mean of the two 
443     * specified stack value pairs.
444     * 
445     * @param stack1  the first stack pair.
446     * @param stack2  the second stack pair.
447     * 
448     * @return A pair of average stack values.
449     */
450    private double[] averageStackValues(double[] stack1, double[] stack2) {
451        double[] result = new double[2];
452        result[0] = (stack1[0] + stack2[0]) / 2.0;
453        result[1] = (stack1[1] + stack2[1]) / 2.0;
454        return result;
455    }
456
457    /**
458     * Calculates adjusted stack values from the supplied values.  The value is
459     * the mean of the supplied values, unless either of the supplied values
460     * is zero, in which case the adjusted value is zero also.
461     * 
462     * @param stack1  the first stack pair.
463     * @param stack2  the second stack pair.
464     * 
465     * @return A pair of average stack values.
466     */
467    private double[] adjustedStackValues(double[] stack1, double[] stack2) {
468        double[] result = new double[2];
469        if (stack1[0] == 0.0 || stack2[0] == 0.0) {
470            result[0] = 0.0;   
471        }
472        else {
473            result[0] = (stack1[0] + stack2[0]) / 2.0;
474        }
475        if (stack1[1] == 0.0 || stack2[1] == 0.0) {
476            result[1] = 0.0;   
477        }
478        else {
479            result[1] = (stack1[1] + stack2[1]) / 2.0;
480        }
481        return result;
482    }
483
484    /**
485     * Checks this instance for equality with an arbitrary object.
486     *
487     * @param obj  the object (<code>null</code> not permitted).
488     *
489     * @return A boolean.
490     */
491    public boolean equals(Object obj) {
492        if (obj == this) {
493            return true;
494        }
495        if (!(obj instanceof StackedAreaRenderer)) {
496            return false;
497        }
498        StackedAreaRenderer that = (StackedAreaRenderer) obj;
499        if (this.renderAsPercentages != that.renderAsPercentages) {
500            return false;
501        }
502        return super.equals(obj);
503    }
504}