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