001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2005, 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-2006, 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     * $Id: StackedAreaRenderer.java,v 1.6.2.3 2006/10/11 16:25:50 mungady Exp $
039     *
040     * Changes:
041     * --------
042     * 20-Sep-2002 : Version 1, contributed by Dan Rivett;
043     * 24-Oct-2002 : Amendments for changes in CategoryDataset interface and 
044     *               CategoryToolTipGenerator interface (DG);
045     * 01-Nov-2002 : Added tooltips (DG);
046     * 06-Nov-2002 : Renamed drawCategoryItem() --> drawItem() and now using axis 
047     *               for category spacing. Renamed StackedAreaCategoryItemRenderer 
048     *               --> StackedAreaRenderer (DG);
049     * 26-Nov-2002 : Switched CategoryDataset --> TableDataset (DG);
050     * 26-Nov-2002 : Replaced isStacked() method with getRangeType() method (DG);
051     * 17-Jan-2003 : Moved plot classes to a separate package (DG);
052     * 25-Mar-2003 : Implemented Serializable (DG);
053     * 13-May-2003 : Modified to take into account the plot orientation (DG);
054     * 30-Jul-2003 : Modified entity constructor (CZ);
055     * 07-Oct-2003 : Added renderer state (DG);
056     * 29-Apr-2004 : Added getRangeExtent() override (DG);
057     * 05-Nov-2004 : Modified drawItem() signature (DG);
058     * 07-Jan-2005 : Renamed getRangeExtent() --> findRangeBounds() (DG);
059     * ------------- JFREECHART 1.0.x ---------------------------------------------
060     * 11-Oct-2006 : Added support for rendering data values as percentages,
061     *               and added a second pass for drawing item labels (DG);
062     * 
063     */
064    
065    package org.jfree.chart.renderer.category;
066    
067    import java.awt.Graphics2D;
068    import java.awt.Polygon;
069    import java.awt.Shape;
070    import java.awt.geom.Rectangle2D;
071    import java.io.Serializable;
072    
073    import org.jfree.chart.axis.CategoryAxis;
074    import org.jfree.chart.axis.ValueAxis;
075    import org.jfree.chart.entity.EntityCollection;
076    import org.jfree.chart.event.RendererChangeEvent;
077    import org.jfree.chart.plot.CategoryPlot;
078    import org.jfree.chart.plot.PlotOrientation;
079    import org.jfree.data.DataUtilities;
080    import org.jfree.data.Range;
081    import org.jfree.data.category.CategoryDataset;
082    import org.jfree.data.general.DatasetUtilities;
083    import org.jfree.ui.RectangleEdge;
084    import org.jfree.util.PublicCloneable;
085    
086    /**
087     * A renderer that draws stacked area charts for a 
088     * {@link org.jfree.chart.plot.CategoryPlot}.
089     */
090    public class StackedAreaRenderer extends AreaRenderer 
091                                     implements Cloneable, PublicCloneable, 
092                                                Serializable {
093    
094        /** For serialization. */
095        private static final long serialVersionUID = -3595635038460823663L;
096         
097        /** A flag that controls whether the areas display values or percentages. */
098        private boolean renderAsPercentages;
099        
100        /**
101         * Creates a new renderer.
102         */
103        public StackedAreaRenderer() {
104            this(false);
105        }
106        
107        /**
108         * Creates a new renderer.
109         * 
110         * @param renderAsPercentages  a flag that controls whether the data values
111         *                             are rendered as percentages.
112         */
113        public StackedAreaRenderer(boolean renderAsPercentages) {
114            super();
115            this.renderAsPercentages = renderAsPercentages;
116        }
117    
118        /**
119         * Returns <code>true</code> if the renderer displays each item value as
120         * a percentage (so that the stacked areas add to 100%), and 
121         * <code>false</code> otherwise.
122         * 
123         * @return A boolean.
124         *
125         * @since 1.0.3
126         */
127        public boolean getRenderAsPercentages() {
128            return this.renderAsPercentages;   
129        }
130        
131        /**
132         * Sets the flag that controls whether the renderer displays each item
133         * value as a percentage (so that the stacked areas add to 100%), and sends
134         * a {@link RendererChangeEvent} to all registered listeners.
135         * 
136         * @param asPercentages  the flag.
137         *
138         * @since 1.0.3
139         */
140        public void setRenderAsPercentages(boolean asPercentages) {
141            this.renderAsPercentages = asPercentages; 
142            notifyListeners(new RendererChangeEvent(this));
143        }
144        
145        /**
146         * Returns the number of passes (<code>2</code>) required by this renderer. 
147         * The first pass is used to draw the bars, the second pass is used to
148         * draw the item labels (if visible).
149         * 
150         * @return The number of passes required by the renderer.
151         */
152        public int getPassCount() {
153            return 2;
154        }
155    
156        /**
157         * Returns the range of values the renderer requires to display all the 
158         * items from the specified dataset.
159         * 
160         * @param dataset  the dataset (<code>null</code> not permitted).
161         * 
162         * @return The range (or <code>null</code> if the dataset is empty).
163         */
164        public Range findRangeBounds(CategoryDataset dataset) {
165            if (this.renderAsPercentages) {
166                return new Range(0.0, 1.0);   
167            }
168            else {
169                return DatasetUtilities.findStackedRangeBounds(dataset);
170            }
171        }
172    
173        /**
174         * Draw a single data item.
175         *
176         * @param g2  the graphics device.
177         * @param state  the renderer state.
178         * @param dataArea  the data plot area.
179         * @param plot  the plot.
180         * @param domainAxis  the domain axis.
181         * @param rangeAxis  the range axis.
182         * @param dataset  the data.
183         * @param row  the row index (zero-based).
184         * @param column  the column index (zero-based).
185         * @param pass  the pass index.
186         */
187        public void drawItem(Graphics2D g2,
188                             CategoryItemRendererState state,
189                             Rectangle2D dataArea,
190                             CategoryPlot plot,
191                             CategoryAxis domainAxis,
192                             ValueAxis rangeAxis,
193                             CategoryDataset dataset,
194                             int row,
195                             int column,
196                             int pass) {
197    
198            // plot non-null values...
199            Number dataValue = dataset.getValue(row, column);
200            if (dataValue == null) {
201                return;
202            }
203    
204            double value = dataValue.doubleValue();
205            double total = 0.0;  // only needed if calculating percentages
206            if (this.renderAsPercentages) {
207                total = DataUtilities.calculateColumnTotal(dataset, column);
208                value = value / total;
209            }
210    
211            // leave the y values (y1, y0) untranslated as it is going to be be 
212            // stacked up later by previous series values, after this it will be 
213            // translated.
214            double xx1 = domainAxis.getCategoryMiddle(column, getColumnCount(), 
215                    dataArea, plot.getDomainAxisEdge());
216            
217            double previousHeightx1 = getPreviousHeight(dataset, row, column);
218            double y1 = value + previousHeightx1;
219            RectangleEdge location = plot.getRangeAxisEdge();
220            double yy1 = rangeAxis.valueToJava2D(y1, dataArea, location);
221    
222            g2.setPaint(getItemPaint(row, column));
223            g2.setStroke(getItemStroke(row, column));
224    
225            // in column zero, the only job to do is draw any visible item labels
226            // and this is done in the second pass...
227            if (column == 0) {
228                if (pass == 1) {
229                    // draw item labels, if visible
230                    if (isItemLabelVisible(row, column)) {
231                        drawItemLabel(g2, plot.getOrientation(), dataset, row, column, 
232                                xx1, yy1, (y1 < 0.0));
233                    }    
234                }
235            }
236            else {
237                Number previousValue = dataset.getValue(row, column - 1);
238                if (previousValue != null) {
239    
240                    double xx0 = domainAxis.getCategoryMiddle(column - 1, 
241                            getColumnCount(), dataArea, plot.getDomainAxisEdge());
242                    double y0 = previousValue.doubleValue();
243                    if (this.renderAsPercentages) {
244                        total = DataUtilities.calculateColumnTotal(dataset, 
245                                column - 1);
246                        y0 = y0 / total;
247                    }
248                   
249    
250                    // Get the previous height, but this will be different for both
251                    // y0 and y1 as the previous series values could differ.
252                    double previousHeightx0 = getPreviousHeight(dataset, row, 
253                            column - 1);
254    
255                    // Now stack the current y values on top of the previous values.
256                    y0 += previousHeightx0;
257    
258                    // Now translate the previous heights
259                    double previousHeightxx0 = rangeAxis.valueToJava2D(
260                            previousHeightx0, dataArea, location);
261                    double previousHeightxx1 = rangeAxis.valueToJava2D(
262                            previousHeightx1, dataArea, location);
263    
264                    // Now translate the current y values.
265                    double yy0 = rangeAxis.valueToJava2D(y0, dataArea, location);
266    
267                    if (pass == 0) {
268                        Polygon p = null;
269                        PlotOrientation orientation = plot.getOrientation();
270                        if (orientation == PlotOrientation.HORIZONTAL) {
271                            p = new Polygon();
272                            p.addPoint((int) yy0, (int) xx0);
273                            p.addPoint((int) yy1, (int) xx1);
274                            p.addPoint((int) previousHeightxx1, (int) xx1);
275                            p.addPoint((int) previousHeightxx0, (int) xx0);
276                        }
277                        else if (orientation == PlotOrientation.VERTICAL) {
278                            p = new Polygon();
279                            p.addPoint((int) xx0, (int) yy0);
280                            p.addPoint((int) xx1, (int) yy1);
281                            p.addPoint((int) xx1, (int) previousHeightxx1);
282                            p.addPoint((int) xx0, (int) previousHeightxx0);
283                        }
284                        g2.setPaint(getItemPaint(row, column));
285                        g2.setStroke(getItemStroke(row, column));
286                        g2.fill(p);
287                    }
288                    else {
289                        if (isItemLabelVisible(row, column)) {
290                            drawItemLabel(g2, plot.getOrientation(), dataset, row, 
291                                    column, xx1, yy1, (y1 < 0.0));
292                        }  
293                    }
294                }
295                
296    
297            }
298            
299    
300            // add an item entity, if this information is being collected
301            EntityCollection entities = state.getEntityCollection();
302            if (entities != null) {
303                Shape shape = new Rectangle2D.Double(xx1 - 3.0, yy1 - 3.0, 6.0, 6.0);
304                addItemEntity(entities, dataset, row, column, shape);
305            }
306    
307        }
308    
309        /**
310         * Calculates the stacked value of the all series up to, but not including 
311         * <code>series</code> for the specified category, <code>category</code>.  
312         * It returns 0.0 if <code>series</code> is the first series, i.e. 0.
313         *
314         * @param dataset  the dataset (<code>null</code> not permitted).
315         * @param series  the series.
316         * @param category  the category.
317         *
318         * @return double returns a cumulative value for all series' values up to 
319         *         but excluding <code>series</code> for Object 
320         *         <code>category</code>.
321         */
322        protected double getPreviousHeight(CategoryDataset dataset, 
323                                           int series, int category) {
324    
325            double result = 0.0;
326            Number n;
327            double total = 0.0;
328            if (this.renderAsPercentages) {
329                total = DataUtilities.calculateColumnTotal(dataset, category);
330            }
331            for (int i = 0; i < series; i++) {
332                n = dataset.getValue(i, category);
333                if (n != null) {
334                    double v = n.doubleValue();
335                    if (this.renderAsPercentages) {
336                        v = v / total;
337                    }
338                    result += v;
339                }
340            }
341            return result;
342    
343        }
344    
345        /**
346         * Checks this instance for equality with an arbitrary object.
347         *
348         * @param obj  the object (<code>null</code> not permitted).
349         *
350         * @return A boolean.
351         */
352        public boolean equals(Object obj) {
353            if (obj == this) {
354                return true;
355            }
356            if (! (obj instanceof StackedAreaRenderer)) {
357                return false;
358            }
359            StackedAreaRenderer that = (StackedAreaRenderer) obj;
360            if (this.renderAsPercentages != that.renderAsPercentages) {
361                return false;
362            }
363            return super.equals(obj);
364        }
365    }