001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-present, by David Gilbert 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-present, 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;
036 *                   Christian W. Zuckschwerdt;
037 *                   Peter Kolb (patch 2511330);
038 */
039
040package org.jfree.chart.renderer.category;
041
042import java.awt.Graphics2D;
043import java.awt.Paint;
044import java.awt.Shape;
045import java.awt.geom.GeneralPath;
046import java.awt.geom.Rectangle2D;
047import java.io.Serializable;
048
049import org.jfree.chart.axis.CategoryAxis;
050import org.jfree.chart.axis.ValueAxis;
051import org.jfree.chart.entity.EntityCollection;
052import org.jfree.chart.event.RendererChangeEvent;
053import org.jfree.chart.plot.CategoryPlot;
054import org.jfree.chart.ui.RectangleEdge;
055import org.jfree.chart.util.PublicCloneable;
056import org.jfree.data.DataUtils;
057import org.jfree.data.Range;
058import org.jfree.data.category.CategoryDataset;
059import org.jfree.data.general.DatasetUtils;
060
061/**
062 * A renderer that draws stacked area charts for a {@link CategoryPlot}.
063 * The example shown here is generated by the
064 * {@code StackedAreaChartDemo1.java} program included in the
065 * JFreeChart Demo Collection:
066 * <br><br>
067 * <img src="doc-files/StackedAreaRendererSample.png"
068 * alt="StackedAreaRendererSample.png">
069 */
070public class StackedAreaRenderer extends AreaRenderer
071        implements Cloneable, PublicCloneable, Serializable {
072
073    /** For serialization. */
074    private static final long serialVersionUID = -3595635038460823663L;
075
076    /** A flag that controls whether the areas display values or percentages. */
077    private boolean renderAsPercentages;
078
079    /**
080     * Creates a new renderer.
081     */
082    public StackedAreaRenderer() {
083        this(false);
084    }
085
086    /**
087     * Creates a new renderer.
088     *
089     * @param renderAsPercentages  a flag that controls whether the data values
090     *                             are rendered as percentages.
091     */
092    public StackedAreaRenderer(boolean renderAsPercentages) {
093        super();
094        this.renderAsPercentages = renderAsPercentages;
095    }
096
097    /**
098     * Returns {@code true} if the renderer displays each item value as
099     * a percentage (so that the stacked areas add to 100%), and
100     * {@code false} otherwise.
101     *
102     * @return A boolean.
103     */
104    public boolean getRenderAsPercentages() {
105        return this.renderAsPercentages;
106    }
107
108    /**
109     * Sets the flag that controls whether the renderer displays each item
110     * value as a percentage (so that the stacked areas add to 100%), and sends
111     * a {@link RendererChangeEvent} to all registered listeners.
112     *
113     * @param asPercentages  the flag.
114     */
115    public void setRenderAsPercentages(boolean asPercentages) {
116        this.renderAsPercentages = asPercentages;
117        fireChangeEvent();
118    }
119
120    /**
121     * Returns the number of passes ({@code 2}) required by this renderer.
122     * The first pass is used to draw the areas, the second pass is used to
123     * draw the item labels (if visible).
124     *
125     * @return The number of passes required by the renderer.
126     */
127    @Override
128    public int getPassCount() {
129        return 2;
130    }
131
132    /**
133     * Returns the range of values the renderer requires to display all the
134     * items from the specified dataset.
135     *
136     * @param dataset  the dataset ({@code null} not permitted).
137     *
138     * @return The range (or {@code null} if the dataset is empty).
139     */
140    @Override
141    public Range findRangeBounds(CategoryDataset dataset) {
142        if (dataset == null) {
143            return null;
144        }
145        if (this.renderAsPercentages) {
146            return new Range(0.0, 1.0);
147        }
148        else {
149            return DatasetUtils.findStackedRangeBounds(dataset);
150        }
151    }
152
153    /**
154     * Draw a single data item.
155     *
156     * @param g2  the graphics device.
157     * @param state  the renderer state.
158     * @param dataArea  the data plot area.
159     * @param plot  the plot.
160     * @param domainAxis  the domain axis.
161     * @param rangeAxis  the range axis.
162     * @param dataset  the data.
163     * @param row  the row index (zero-based).
164     * @param column  the column index (zero-based).
165     * @param pass  the pass index.
166     */
167    @Override
168    public void drawItem(Graphics2D g2, CategoryItemRendererState state,
169            Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis,
170            ValueAxis rangeAxis, CategoryDataset dataset, int row, int column,
171            int pass) {
172
173        if (!isSeriesVisible(row)) {
174            return;
175        }
176        
177        // setup for collecting optional entity info...
178        Shape entityArea;
179        EntityCollection entities = state.getEntityCollection();
180
181        double y1 = 0.0;
182        Number n = dataset.getValue(row, column);
183        if (n != null) {
184            y1 = n.doubleValue();
185            if (this.renderAsPercentages) {
186                double total = DataUtils.calculateColumnTotal(dataset,
187                        column, state.getVisibleSeriesArray());
188                y1 = y1 / total;
189            }
190        }
191        double[] stack1 = getStackValues(dataset, row, column,
192                state.getVisibleSeriesArray());
193
194
195        // leave the y values (y1, y0) untranslated as it is going to be be
196        // stacked up later by previous series values, after this it will be
197        // translated.
198        double xx1 = domainAxis.getCategoryMiddle(column, getColumnCount(),
199                dataArea, plot.getDomainAxisEdge());
200
201
202        // get the previous point and the next point so we can calculate a
203        // "hot spot" for the area (used by the chart entity)...
204        double y0 = 0.0;
205        n = dataset.getValue(row, Math.max(column - 1, 0));
206        if (n != null) {
207            y0 = n.doubleValue();
208            if (this.renderAsPercentages) {
209                double total = DataUtils.calculateColumnTotal(dataset,
210                        Math.max(column - 1, 0), state.getVisibleSeriesArray());
211                y0 = y0 / total;
212            }
213        }
214        double[] stack0 = getStackValues(dataset, row, Math.max(column - 1, 0),
215                state.getVisibleSeriesArray());
216
217        // FIXME: calculate xx0
218        double xx0 = domainAxis.getCategoryStart(column, getColumnCount(),
219                dataArea, plot.getDomainAxisEdge());
220
221        int itemCount = dataset.getColumnCount();
222        double y2 = 0.0;
223        n = dataset.getValue(row, Math.min(column + 1, itemCount - 1));
224        if (n != null) {
225            y2 = n.doubleValue();
226            if (this.renderAsPercentages) {
227                double total = DataUtils.calculateColumnTotal(dataset,
228                        Math.min(column + 1, itemCount - 1),
229                        state.getVisibleSeriesArray());
230                y2 = y2 / total;
231            }
232        }
233        double[] stack2 = getStackValues(dataset, row, Math.min(column + 1,
234                itemCount - 1), state.getVisibleSeriesArray());
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        if (pass == 0) {
348            Paint itemPaint = getItemPaint(row, column);
349            g2.setPaint(itemPaint);
350            g2.fill(left);
351            g2.fill(right);
352
353            // add an entity for the item...
354            if (entities != null) {
355                GeneralPath gp = new GeneralPath(left);
356                gp.append(right, false);
357                entityArea = gp;
358                addItemEntity(entities, dataset, row, column, entityArea);
359            }
360        }
361        else if (pass == 1) {
362            drawItemLabel(g2, plot.getOrientation(), dataset, row, column,
363                    xx1, transY1, y1 < 0.0);
364        }
365
366    }
367
368    /**
369     * Calculates the stacked values (one positive and one negative) of all
370     * series up to, but not including, {@code series} for the specified
371     * item. It returns [0.0, 0.0] if {@code series} is the first series.
372     *
373     * @param dataset  the dataset ({@code null} not permitted).
374     * @param series  the series index.
375     * @param index  the item index.
376     * @param validRows  the valid rows.
377     *
378     * @return An array containing the cumulative negative and positive values
379     *     for all series values up to but excluding {@code series}
380     *     for {@code index}.
381     */
382    protected double[] getStackValues(CategoryDataset dataset,
383            int series, int index, int[] validRows) {
384        double[] result = new double[2];
385        double total = 0.0;
386        if (this.renderAsPercentages) {
387            total = DataUtils.calculateColumnTotal(dataset, index, 
388                    validRows);
389        }
390        for (int i = 0; i < series; i++) {
391            if (isSeriesVisible(i)) {
392                double v = 0.0;
393                Number n = dataset.getValue(i, index);
394                if (n != null) {
395                    v = n.doubleValue();
396                    if (this.renderAsPercentages) {
397                        v = v / total;
398                    }
399                }
400                if (!Double.isNaN(v)) {
401                    if (v >= 0.0) {
402                        result[1] += v;
403                    }
404                    else {
405                        result[0] += v;
406                    }
407                }
408            }
409        }
410        return result;
411    }
412
413    /**
414     * Returns a pair of "stack" values calculated as the mean of the two
415     * specified stack value pairs.
416     *
417     * @param stack1  the first stack pair.
418     * @param stack2  the second stack pair.
419     *
420     * @return A pair of average stack values.
421     */
422    private double[] averageStackValues(double[] stack1, double[] stack2) {
423        double[] result = new double[2];
424        result[0] = (stack1[0] + stack2[0]) / 2.0;
425        result[1] = (stack1[1] + stack2[1]) / 2.0;
426        return result;
427    }
428
429    /**
430     * Calculates adjusted stack values from the supplied values.  The value is
431     * the mean of the supplied values, unless either of the supplied values
432     * is zero, in which case the adjusted value is zero also.
433     *
434     * @param stack1  the first stack pair.
435     * @param stack2  the second stack pair.
436     *
437     * @return A pair of average stack values.
438     */
439    private double[] adjustedStackValues(double[] stack1, double[] stack2) {
440        double[] result = new double[2];
441        if (stack1[0] == 0.0 || stack2[0] == 0.0) {
442            result[0] = 0.0;
443        }
444        else {
445            result[0] = (stack1[0] + stack2[0]) / 2.0;
446        }
447        if (stack1[1] == 0.0 || stack2[1] == 0.0) {
448            result[1] = 0.0;
449        }
450        else {
451            result[1] = (stack1[1] + stack2[1]) / 2.0;
452        }
453        return result;
454    }
455
456    /**
457     * Checks this instance for equality with an arbitrary object.
458     *
459     * @param obj  the object ({@code null} not permitted).
460     *
461     * @return A boolean.
462     */
463    @Override
464    public boolean equals(Object obj) {
465        if (obj == this) {
466            return true;
467        }
468        if (!(obj instanceof StackedAreaRenderer)) {
469            return false;
470        }
471        StackedAreaRenderer that = (StackedAreaRenderer) obj;
472        if (this.renderAsPercentages != that.renderAsPercentages) {
473            return false;
474        }
475        return super.equals(obj);
476    }
477
478}