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