001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2014, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * ------------------------
028 * StackedAreaRenderer.java
029 * ------------------------
030 * (C) Copyright 2002-2014, 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    @Override
161    public int getPassCount() {
162        return 2;
163    }
164
165    /**
166     * Returns the range of values the renderer requires to display all the
167     * items from the specified dataset.
168     *
169     * @param dataset  the dataset (<code>null</code> not permitted).
170     *
171     * @return The range (or <code>null</code> if the dataset is empty).
172     */
173    @Override
174    public Range findRangeBounds(CategoryDataset dataset) {
175        if (dataset == null) {
176            return null;
177        }
178        if (this.renderAsPercentages) {
179            return new Range(0.0, 1.0);
180        }
181        else {
182            return DatasetUtilities.findStackedRangeBounds(dataset);
183        }
184    }
185
186    /**
187     * Draw a single data item.
188     *
189     * @param g2  the graphics device.
190     * @param state  the renderer state.
191     * @param dataArea  the data plot area.
192     * @param plot  the plot.
193     * @param domainAxis  the domain axis.
194     * @param rangeAxis  the range axis.
195     * @param dataset  the data.
196     * @param row  the row index (zero-based).
197     * @param column  the column index (zero-based).
198     * @param pass  the pass index.
199     */
200    @Override
201    public void drawItem(Graphics2D g2, CategoryItemRendererState state,
202            Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis,
203            ValueAxis rangeAxis, CategoryDataset dataset, int row, int column,
204            int pass) {
205
206        if (!isSeriesVisible(row)) {
207            return;
208        }
209        
210        // setup for collecting optional entity info...
211        Shape entityArea;
212        EntityCollection entities = state.getEntityCollection();
213
214        double y1 = 0.0;
215        Number n = dataset.getValue(row, column);
216        if (n != null) {
217            y1 = n.doubleValue();
218            if (this.renderAsPercentages) {
219                double total = DataUtilities.calculateColumnTotal(dataset,
220                        column, state.getVisibleSeriesArray());
221                y1 = y1 / total;
222            }
223        }
224        double[] stack1 = getStackValues(dataset, row, column,
225                state.getVisibleSeriesArray());
226
227
228        // leave the y values (y1, y0) untranslated as it is going to be be
229        // stacked up later by previous series values, after this it will be
230        // translated.
231        double xx1 = domainAxis.getCategoryMiddle(column, getColumnCount(),
232                dataArea, plot.getDomainAxisEdge());
233
234
235        // get the previous point and the next point so we can calculate a
236        // "hot spot" for the area (used by the chart entity)...
237        double y0 = 0.0;
238        n = dataset.getValue(row, Math.max(column - 1, 0));
239        if (n != null) {
240            y0 = n.doubleValue();
241            if (this.renderAsPercentages) {
242                double total = DataUtilities.calculateColumnTotal(dataset,
243                        Math.max(column - 1, 0), state.getVisibleSeriesArray());
244                y0 = y0 / total;
245            }
246        }
247        double[] stack0 = getStackValues(dataset, row, Math.max(column - 1, 0),
248                state.getVisibleSeriesArray());
249
250        // FIXME: calculate xx0
251        double xx0 = domainAxis.getCategoryStart(column, getColumnCount(),
252                dataArea, plot.getDomainAxisEdge());
253
254        int itemCount = dataset.getColumnCount();
255        double y2 = 0.0;
256        n = dataset.getValue(row, Math.min(column + 1, itemCount - 1));
257        if (n != null) {
258            y2 = n.doubleValue();
259            if (this.renderAsPercentages) {
260                double total = DataUtilities.calculateColumnTotal(dataset,
261                        Math.min(column + 1, itemCount - 1),
262                        state.getVisibleSeriesArray());
263                y2 = y2 / total;
264            }
265        }
266        double[] stack2 = getStackValues(dataset, row, Math.min(column + 1,
267                itemCount - 1), state.getVisibleSeriesArray());
268
269        double xx2 = domainAxis.getCategoryEnd(column, getColumnCount(),
270                dataArea, plot.getDomainAxisEdge());
271
272        // FIXME: calculate xxLeft and xxRight
273        double xxLeft = xx0;
274        double xxRight = xx2;
275
276        double[] stackLeft = averageStackValues(stack0, stack1);
277        double[] stackRight = averageStackValues(stack1, stack2);
278        double[] adjStackLeft = adjustedStackValues(stack0, stack1);
279        double[] adjStackRight = adjustedStackValues(stack1, stack2);
280
281        float transY1;
282
283        RectangleEdge edge1 = plot.getRangeAxisEdge();
284
285        GeneralPath left = new GeneralPath();
286        GeneralPath right = new GeneralPath();
287        if (y1 >= 0.0) {  // handle positive value
288            transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1], dataArea,
289                    edge1);
290            float transStack1 = (float) rangeAxis.valueToJava2D(stack1[1],
291                    dataArea, edge1);
292            float transStackLeft = (float) rangeAxis.valueToJava2D(
293                    adjStackLeft[1], dataArea, edge1);
294
295            // LEFT POLYGON
296            if (y0 >= 0.0) {
297                double yleft = (y0 + y1) / 2.0 + stackLeft[1];
298                float transYLeft
299                    = (float) rangeAxis.valueToJava2D(yleft, dataArea, edge1);
300                left.moveTo((float) xx1, transY1);
301                left.lineTo((float) xx1, transStack1);
302                left.lineTo((float) xxLeft, transStackLeft);
303                left.lineTo((float) xxLeft, transYLeft);
304                left.closePath();
305            }
306            else {
307                left.moveTo((float) xx1, transStack1);
308                left.lineTo((float) xx1, transY1);
309                left.lineTo((float) xxLeft, transStackLeft);
310                left.closePath();
311            }
312
313            float transStackRight = (float) rangeAxis.valueToJava2D(
314                    adjStackRight[1], dataArea, edge1);
315            // RIGHT POLYGON
316            if (y2 >= 0.0) {
317                double yright = (y1 + y2) / 2.0 + stackRight[1];
318                float transYRight
319                    = (float) rangeAxis.valueToJava2D(yright, dataArea, edge1);
320                right.moveTo((float) xx1, transStack1);
321                right.lineTo((float) xx1, transY1);
322                right.lineTo((float) xxRight, transYRight);
323                right.lineTo((float) xxRight, transStackRight);
324                right.closePath();
325            }
326            else {
327                right.moveTo((float) xx1, transStack1);
328                right.lineTo((float) xx1, transY1);
329                right.lineTo((float) xxRight, transStackRight);
330                right.closePath();
331            }
332        }
333        else {  // handle negative value
334            transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0], dataArea,
335                    edge1);
336            float transStack1 = (float) rangeAxis.valueToJava2D(stack1[0],
337                    dataArea, edge1);
338            float transStackLeft = (float) rangeAxis.valueToJava2D(
339                    adjStackLeft[0], dataArea, edge1);
340
341            // LEFT POLYGON
342            if (y0 >= 0.0) {
343                left.moveTo((float) xx1, transStack1);
344                left.lineTo((float) xx1, transY1);
345                left.lineTo((float) xxLeft, transStackLeft);
346                left.clone();
347            }
348            else {
349                double yleft = (y0 + y1) / 2.0 + stackLeft[0];
350                float transYLeft = (float) rangeAxis.valueToJava2D(yleft,
351                        dataArea, edge1);
352                left.moveTo((float) xx1, transY1);
353                left.lineTo((float) xx1, transStack1);
354                left.lineTo((float) xxLeft, transStackLeft);
355                left.lineTo((float) xxLeft, transYLeft);
356                left.closePath();
357            }
358            float transStackRight = (float) rangeAxis.valueToJava2D(
359                    adjStackRight[0], dataArea, edge1);
360
361            // RIGHT POLYGON
362            if (y2 >= 0.0) {
363                right.moveTo((float) xx1, transStack1);
364                right.lineTo((float) xx1, transY1);
365                right.lineTo((float) xxRight, transStackRight);
366                right.closePath();
367            }
368            else {
369                double yright = (y1 + y2) / 2.0 + stackRight[0];
370                float transYRight = (float) rangeAxis.valueToJava2D(yright,
371                        dataArea, edge1);
372                right.moveTo((float) xx1, transStack1);
373                right.lineTo((float) xx1, transY1);
374                right.lineTo((float) xxRight, transYRight);
375                right.lineTo((float) xxRight, transStackRight);
376                right.closePath();
377            }
378        }
379
380        if (pass == 0) {
381            Paint itemPaint = getItemPaint(row, column);
382            g2.setPaint(itemPaint);
383            g2.fill(left);
384            g2.fill(right);
385
386            // add an entity for the item...
387            if (entities != null) {
388                GeneralPath gp = new GeneralPath(left);
389                gp.append(right, false);
390                entityArea = gp;
391                addItemEntity(entities, dataset, row, column, entityArea);
392            }
393        }
394        else if (pass == 1) {
395            drawItemLabel(g2, plot.getOrientation(), dataset, row, column,
396                    xx1, transY1, y1 < 0.0);
397        }
398
399    }
400
401    /**
402     * Calculates the stacked values (one positive and one negative) of all
403     * series up to, but not including, <code>series</code> for the specified
404     * item. It returns [0.0, 0.0] if <code>series</code> is the first series.
405     *
406     * @param dataset  the dataset (<code>null</code> not permitted).
407     * @param series  the series index.
408     * @param index  the item index.
409     * @param validRows  the valid rows.
410     *
411     * @return An array containing the cumulative negative and positive values
412     *     for all series values up to but excluding <code>series</code>
413     *     for <code>index</code>.
414     */
415    protected double[] getStackValues(CategoryDataset dataset,
416            int series, int index, int[] validRows) {
417        double[] result = new double[2];
418        double total = 0.0;
419        if (this.renderAsPercentages) {
420            total = DataUtilities.calculateColumnTotal(dataset, index, 
421                    validRows);
422        }
423        for (int i = 0; i < series; i++) {
424            if (isSeriesVisible(i)) {
425                double v = 0.0;
426                Number n = dataset.getValue(i, index);
427                if (n != null) {
428                    v = n.doubleValue();
429                    if (this.renderAsPercentages) {
430                        v = v / total;
431                    }
432                }
433                if (!Double.isNaN(v)) {
434                    if (v >= 0.0) {
435                        result[1] += v;
436                    }
437                    else {
438                        result[0] += v;
439                    }
440                }
441            }
442        }
443        return result;
444    }
445
446    /**
447     * Returns a pair of "stack" values calculated as the mean of the two
448     * specified stack value pairs.
449     *
450     * @param stack1  the first stack pair.
451     * @param stack2  the second stack pair.
452     *
453     * @return A pair of average stack values.
454     */
455    private double[] averageStackValues(double[] stack1, double[] stack2) {
456        double[] result = new double[2];
457        result[0] = (stack1[0] + stack2[0]) / 2.0;
458        result[1] = (stack1[1] + stack2[1]) / 2.0;
459        return result;
460    }
461
462    /**
463     * Calculates adjusted stack values from the supplied values.  The value is
464     * the mean of the supplied values, unless either of the supplied values
465     * is zero, in which case the adjusted value is zero also.
466     *
467     * @param stack1  the first stack pair.
468     * @param stack2  the second stack pair.
469     *
470     * @return A pair of average stack values.
471     */
472    private double[] adjustedStackValues(double[] stack1, double[] stack2) {
473        double[] result = new double[2];
474        if (stack1[0] == 0.0 || stack2[0] == 0.0) {
475            result[0] = 0.0;
476        }
477        else {
478            result[0] = (stack1[0] + stack2[0]) / 2.0;
479        }
480        if (stack1[1] == 0.0 || stack2[1] == 0.0) {
481            result[1] = 0.0;
482        }
483        else {
484            result[1] = (stack1[1] + stack2[1]) / 2.0;
485        }
486        return result;
487    }
488
489    /**
490     * Checks this instance for equality with an arbitrary object.
491     *
492     * @param obj  the object (<code>null</code> not permitted).
493     *
494     * @return A boolean.
495     */
496    @Override
497    public boolean equals(Object obj) {
498        if (obj == this) {
499            return true;
500        }
501        if (!(obj instanceof StackedAreaRenderer)) {
502            return false;
503        }
504        StackedAreaRenderer that = (StackedAreaRenderer) obj;
505        if (this.renderAsPercentages != that.renderAsPercentages) {
506            return false;
507        }
508        return super.equals(obj);
509    }
510
511    /**
512     * Calculates the stacked value of the all series up to, but not including
513     * <code>series</code> for the specified category, <code>category</code>.
514     * It returns 0.0 if <code>series</code> is the first series, i.e. 0.
515     *
516     * @param dataset  the dataset (<code>null</code> not permitted).
517     * @param series  the series.
518     * @param category  the category.
519     *
520     * @return double returns a cumulative value for all series' values up to
521     *         but excluding <code>series</code> for Object
522     *         <code>category</code>.
523     *
524     * @deprecated As of 1.0.13, as the method is never used internally.
525     */
526    protected double getPreviousHeight(CategoryDataset dataset,
527            int series, int category) {
528
529        double result = 0.0;
530        Number n;
531        double total = 0.0;
532        if (this.renderAsPercentages) {
533            total = DataUtilities.calculateColumnTotal(dataset, category);
534        }
535        for (int i = 0; i < series; i++) {
536            n = dataset.getValue(i, category);
537            if (n != null) {
538                double v = n.doubleValue();
539                if (this.renderAsPercentages) {
540                    v = v / total;
541                }
542                result += v;
543            }
544        }
545        return result;
546
547    }
548
549}