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
069package org.jfree.chart.renderer.category;
070
071import java.awt.Graphics2D;
072import java.awt.Paint;
073import java.awt.Shape;
074import java.awt.geom.GeneralPath;
075import java.awt.geom.Rectangle2D;
076import java.io.Serializable;
077
078import org.jfree.chart.axis.CategoryAxis;
079import org.jfree.chart.axis.ValueAxis;
080import org.jfree.chart.entity.EntityCollection;
081import org.jfree.chart.event.RendererChangeEvent;
082import org.jfree.chart.plot.CategoryPlot;
083import org.jfree.data.DataUtilities;
084import org.jfree.data.Range;
085import org.jfree.data.category.CategoryDataset;
086import org.jfree.data.general.DatasetUtilities;
087import org.jfree.ui.RectangleEdge;
088import 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 */
099public 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}