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    
063    package org.jfree.chart.renderer.category;
064    
065    import java.awt.Graphics2D;
066    import java.awt.Paint;
067    import java.awt.Shape;
068    import java.awt.geom.GeneralPath;
069    import java.awt.geom.Rectangle2D;
070    import java.io.Serializable;
071    
072    import org.jfree.chart.axis.CategoryAxis;
073    import org.jfree.chart.axis.ValueAxis;
074    import org.jfree.chart.entity.EntityCollection;
075    import org.jfree.chart.event.RendererChangeEvent;
076    import org.jfree.chart.plot.CategoryPlot;
077    import org.jfree.data.DataUtilities;
078    import org.jfree.data.Range;
079    import org.jfree.data.category.CategoryDataset;
080    import org.jfree.data.general.DatasetUtilities;
081    import org.jfree.ui.RectangleEdge;
082    import org.jfree.util.PublicCloneable;
083    
084    /**
085     * A renderer that draws stacked area charts for a 
086     * {@link org.jfree.chart.plot.CategoryPlot}.
087     */
088    public 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            fireChangeEvent();
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    }