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