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