001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2007, 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     * StackedBarRenderer.java
029     * -----------------------
030     * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
031     *
032     * Original Author:  David Gilbert (for Object Refinery Limited);
033     * Contributor(s):   Richard Atkinson;
034     *                   Thierry Saura;
035     *                   Christian W. Zuckschwerdt;
036     *
037     * $Id: StackedBarRenderer.java,v 1.10.2.9 2007/03/15 16:55:06 mungady Exp $
038     *
039     * Changes
040     * -------
041     * 19-Oct-2001 : Version 1 (DG);
042     * 22-Oct-2001 : Renamed DataSource.java --> Dataset.java etc. (DG);
043     * 23-Oct-2001 : Changed intro and trail gaps on bar plots to use percentage of 
044     *               available space rather than a fixed number of units (DG);
045     * 15-Nov-2001 : Modified to allow for null data values (DG);
046     * 22-Nov-2001 : Modified to allow for negative data values (DG);
047     * 13-Dec-2001 : Added tooltips (DG);
048     * 16-Jan-2002 : Fixed bug for single category datasets (DG);
049     * 15-Feb-2002 : Added isStacked() method (DG);
050     * 14-Mar-2002 : Modified to implement the CategoryItemRenderer interface (DG);
051     * 24-May-2002 : Incorporated tooltips into chart entities (DG);
052     * 11-Jun-2002 : Added check for (permitted) null info object, bug and fix 
053     *               reported by David Basten.  Also updated Javadocs. (DG);
054     * 25-Jun-2002 : Removed redundant import (DG);
055     * 26-Jun-2002 : Small change to entity (DG);
056     * 05-Aug-2002 : Small modification to drawCategoryItem method to support URLs 
057     *               for HTML image maps (RA);
058     * 08-Aug-2002 : Added optional linking lines, contributed by Thierry 
059     *               Saura (DG);
060     * 26-Sep-2002 : Fixed errors reported by Checkstyle (DG);
061     * 24-Oct-2002 : Amendments for changes in CategoryDataset interface and 
062     *               CategoryToolTipGenerator interface (DG);
063     * 05-Nov-2002 : Replaced references to CategoryDataset with TableDataset (DG);
064     * 26-Nov-2002 : Replaced isStacked() method with getRangeType() method (DG);
065     * 17-Jan-2003 : Moved plot classes to a separate package (DG);
066     * 25-Mar-2003 : Implemented Serializable (DG);
067     * 12-May-2003 : Merged horizontal and vertical stacked bar renderers (DG);
068     * 30-Jul-2003 : Modified entity constructor (CZ);
069     * 08-Sep-2003 : Fixed bug 799668 (isBarOutlineDrawn() ignored) (DG);
070     * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
071     * 21-Oct-2003 : Moved bar width into renderer state (DG);
072     * 26-Nov-2003 : Added code to respect maxBarWidth attribute (DG);
073     * 05-Nov-2004 : Changed to a two-pass renderer so that item labels are not 
074     *               overwritten by other bars (DG);
075     * 07-Jan-2005 : Renamed getRangeExtent() --> findRangeBounds() (DG);
076     * 29-Mar-2005 : Modified drawItem() method so that a zero value is handled 
077     *               within the code for positive rather than negative values (DG);
078     * 20-Apr-2005 : Renamed CategoryLabelGenerator 
079     *               --> CategoryItemLabelGenerator (DG);
080     * 17-May-2005 : Added flag to allow rendering values as percentages - inspired
081     *               by patch 1200886 submitted by John Xiao (DG);
082     * 09-Jun-2005 : Added accessor methods for the renderAsPercentages flag,
083     *               provided equals() method, and use addItemEntity from 
084     *               superclass (DG);
085     * 09-Jun-2005 : Added support for GradientPaint - see bug report 1215670 (DG);
086     * 22-Sep-2005 : Renamed getMaxBarWidth() --> getMaximumBarWidth() (DG);
087     * 29-Sep-2005 : Use outline stroke in drawItem method - see bug report 
088     *               1304139 (DG);
089     * ------------- JFREECHART 1.0.x ---------------------------------------------
090     * 11-Oct-2006 : Source reformatting (DG);
091     * 
092     */
093    
094    package org.jfree.chart.renderer.category;
095    
096    import java.awt.GradientPaint;
097    import java.awt.Graphics2D;
098    import java.awt.Paint;
099    import java.awt.geom.Rectangle2D;
100    import java.io.Serializable;
101    
102    import org.jfree.chart.axis.CategoryAxis;
103    import org.jfree.chart.axis.ValueAxis;
104    import org.jfree.chart.entity.EntityCollection;
105    import org.jfree.chart.event.RendererChangeEvent;
106    import org.jfree.chart.labels.CategoryItemLabelGenerator;
107    import org.jfree.chart.labels.ItemLabelAnchor;
108    import org.jfree.chart.labels.ItemLabelPosition;
109    import org.jfree.chart.plot.CategoryPlot;
110    import org.jfree.chart.plot.PlotOrientation;
111    import org.jfree.data.DataUtilities;
112    import org.jfree.data.Range;
113    import org.jfree.data.category.CategoryDataset;
114    import org.jfree.data.general.DatasetUtilities;
115    import org.jfree.ui.GradientPaintTransformer;
116    import org.jfree.ui.RectangleEdge;
117    import org.jfree.ui.TextAnchor;
118    import org.jfree.util.PublicCloneable;
119    
120    /**
121     * A stacked bar renderer for use with the 
122     * {@link org.jfree.chart.plot.CategoryPlot} class.
123     */
124    public class StackedBarRenderer extends BarRenderer 
125                                    implements Cloneable, PublicCloneable, 
126                                               Serializable {
127    
128        /** For serialization. */
129        static final long serialVersionUID = 6402943811500067531L;
130        
131        /** A flag that controls whether the bars display values or percentages. */
132        private boolean renderAsPercentages;
133        
134        /**
135         * Creates a new renderer.  By default, the renderer has no tool tip 
136         * generator and no URL generator.  These defaults have been chosen to 
137         * minimise the processing required to generate a default chart.  If you 
138         * require tool tips or URLs, then you can easily add the required 
139         * generators.
140         */
141        public StackedBarRenderer() {
142            this(false);
143        }
144        
145        /**
146         * Creates a new renderer.
147         * 
148         * @param renderAsPercentages  a flag that controls whether the data values
149         *                             are rendered as percentages.
150         */
151        public StackedBarRenderer(boolean renderAsPercentages) {
152            super();
153            this.renderAsPercentages = renderAsPercentages;
154            
155            // set the default item label positions, which will only be used if 
156            // the user requests visible item labels...
157            ItemLabelPosition p = new ItemLabelPosition(ItemLabelAnchor.CENTER, 
158                    TextAnchor.CENTER);
159            setBasePositiveItemLabelPosition(p);
160            setBaseNegativeItemLabelPosition(p);
161            setPositiveItemLabelPositionFallback(null);
162            setNegativeItemLabelPositionFallback(null);
163        }
164    
165        /**
166         * Returns <code>true</code> if the renderer displays each item value as
167         * a percentage (so that the stacked bars add to 100%), and 
168         * <code>false</code> otherwise.
169         * 
170         * @return A boolean.
171         * 
172         * @see #setRenderAsPercentages(boolean)
173         */
174        public boolean getRenderAsPercentages() {
175            return this.renderAsPercentages;   
176        }
177        
178        /**
179         * Sets the flag that controls whether the renderer displays each item
180         * value as a percentage (so that the stacked bars add to 100%), and sends
181         * a {@link RendererChangeEvent} to all registered listeners.
182         * 
183         * @param asPercentages  the flag.
184         * 
185         * @see #getRenderAsPercentages()
186         */
187        public void setRenderAsPercentages(boolean asPercentages) {
188            this.renderAsPercentages = asPercentages; 
189            notifyListeners(new RendererChangeEvent(this));
190        }
191        
192        /**
193         * Returns the number of passes (<code>2</code>) required by this renderer. 
194         * The first pass is used to draw the bars, the second pass is used to
195         * draw the item labels (if visible).
196         * 
197         * @return The number of passes required by the renderer.
198         */
199        public int getPassCount() {
200            return 2;
201        }
202        
203        /**
204         * Returns the range of values the renderer requires to display all the
205         * items from the specified dataset.
206         * 
207         * @param dataset  the dataset (<code>null</code> permitted).
208         * 
209         * @return The range (or <code>null</code> if the dataset is empty).
210         */
211        public Range findRangeBounds(CategoryDataset dataset) {
212            if (this.renderAsPercentages) {
213                return new Range(0.0, 1.0);   
214            }
215            else {
216                return DatasetUtilities.findStackedRangeBounds(dataset, getBase());
217            }
218        }
219    
220        /**
221         * Calculates the bar width and stores it in the renderer state.
222         * 
223         * @param plot  the plot.
224         * @param dataArea  the data area.
225         * @param rendererIndex  the renderer index.
226         * @param state  the renderer state.
227         */
228        protected void calculateBarWidth(CategoryPlot plot, 
229                                         Rectangle2D dataArea, 
230                                         int rendererIndex,
231                                         CategoryItemRendererState state) {
232    
233            // calculate the bar width
234            CategoryAxis xAxis = plot.getDomainAxisForDataset(rendererIndex);
235            CategoryDataset data = plot.getDataset(rendererIndex);
236            if (data != null) {
237                PlotOrientation orientation = plot.getOrientation();
238                double space = 0.0;
239                if (orientation == PlotOrientation.HORIZONTAL) {
240                    space = dataArea.getHeight();
241                }
242                else if (orientation == PlotOrientation.VERTICAL) {
243                    space = dataArea.getWidth();
244                }
245                double maxWidth = space * getMaximumBarWidth();
246                int columns = data.getColumnCount();
247                double categoryMargin = 0.0;
248                if (columns > 1) {
249                    categoryMargin = xAxis.getCategoryMargin();
250                }
251    
252                double used = space * (1 - xAxis.getLowerMargin() 
253                                         - xAxis.getUpperMargin()
254                                         - categoryMargin);
255                if (columns > 0) {
256                    state.setBarWidth(Math.min(used / columns, maxWidth));
257                }
258                else {
259                    state.setBarWidth(Math.min(used, maxWidth));
260                }
261            }
262    
263        }
264    
265        /**
266         * Draws a stacked bar for a specific item.
267         *
268         * @param g2  the graphics device.
269         * @param state  the renderer state.
270         * @param dataArea  the plot area.
271         * @param plot  the plot.
272         * @param domainAxis  the domain (category) axis.
273         * @param rangeAxis  the range (value) axis.
274         * @param dataset  the data.
275         * @param row  the row index (zero-based).
276         * @param column  the column index (zero-based).
277         * @param pass  the pass index.
278         */
279        public void drawItem(Graphics2D g2,
280                             CategoryItemRendererState state,
281                             Rectangle2D dataArea,
282                             CategoryPlot plot,
283                             CategoryAxis domainAxis,
284                             ValueAxis rangeAxis,
285                             CategoryDataset dataset,
286                             int row,
287                             int column,
288                             int pass) {
289         
290            // nothing is drawn for null values...
291            Number dataValue = dataset.getValue(row, column);
292            if (dataValue == null) {
293                return;
294            }
295            
296            double value = dataValue.doubleValue();
297            double total = 0.0;  // only needed if calculating percentages
298            if (this.renderAsPercentages) {
299                total = DataUtilities.calculateColumnTotal(dataset, column);
300                value = value / total;
301            }
302            
303            PlotOrientation orientation = plot.getOrientation();
304            double barW0 = domainAxis.getCategoryMiddle(column, getColumnCount(), 
305                    dataArea, plot.getDomainAxisEdge()) 
306                    - state.getBarWidth() / 2.0;
307    
308            double positiveBase = getBase();
309            double negativeBase = positiveBase;
310    
311            for (int i = 0; i < row; i++) {
312                Number v = dataset.getValue(i, column);
313                if (v != null) {
314                    double d = v.doubleValue();
315                    if (this.renderAsPercentages) {
316                        d = d / total;
317                    }
318                    if (d > 0) {
319                        positiveBase = positiveBase + d;
320                    }
321                    else {
322                        negativeBase = negativeBase + d;
323                    }
324                }
325            }
326    
327            double translatedBase;
328            double translatedValue;
329            RectangleEdge location = plot.getRangeAxisEdge();
330            if (value >= 0.0) {
331                translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea, 
332                        location);
333                translatedValue = rangeAxis.valueToJava2D(positiveBase + value, 
334                        dataArea, location);
335            }
336            else {
337                translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea, 
338                        location);
339                translatedValue = rangeAxis.valueToJava2D(negativeBase + value, 
340                        dataArea, location);
341            }
342            double barL0 = Math.min(translatedBase, translatedValue);
343            double barLength = Math.max(Math.abs(translatedValue - translatedBase),
344                    getMinimumBarLength());
345    
346            Rectangle2D bar = null;
347            if (orientation == PlotOrientation.HORIZONTAL) {
348                bar = new Rectangle2D.Double(barL0, barW0, barLength, 
349                        state.getBarWidth());
350            }
351            else {
352                bar = new Rectangle2D.Double(barW0, barL0, state.getBarWidth(), 
353                        barLength);
354            }
355            if (pass == 0) {
356                Paint itemPaint = getItemPaint(row, column);
357                GradientPaintTransformer t = getGradientPaintTransformer();
358                if (t != null && itemPaint instanceof GradientPaint) {
359                    itemPaint = t.transform((GradientPaint) itemPaint, bar);
360                }
361                g2.setPaint(itemPaint);
362                g2.fill(bar);
363                if (isDrawBarOutline() 
364                        && state.getBarWidth() > BAR_OUTLINE_WIDTH_THRESHOLD) {
365                    g2.setStroke(getItemOutlineStroke(row, column));
366                    g2.setPaint(getItemOutlinePaint(row, column));
367                    g2.draw(bar);
368                }
369    
370                // add an item entity, if this information is being collected
371                EntityCollection entities = state.getEntityCollection();
372                if (entities != null) {
373                    addItemEntity(entities, dataset, row, column, bar);
374                }
375            }
376            else if (pass == 1) {
377                CategoryItemLabelGenerator generator = getItemLabelGenerator(row, 
378                        column);
379                if (generator != null && isItemLabelVisible(row, column)) {
380                    drawItemLabel(g2, dataset, row, column, plot, generator, bar, 
381                            (value < 0.0));
382                }
383            }        
384        }
385    
386        /**
387         * Tests this renderer for equality with an arbitrary object.
388         * 
389         * @param obj  the object (<code>null</code> permitted).
390         * 
391         * @return A boolean.
392         */
393        public boolean equals(Object obj) {
394            if (obj == this) {
395                return true;   
396            }
397            if (!(obj instanceof StackedBarRenderer)) {
398                return false;   
399            }
400            StackedBarRenderer that = (StackedBarRenderer) obj;
401            if (this.renderAsPercentages != that.renderAsPercentages) {
402                return false;   
403            }
404            return super.equals(obj);
405        }
406    
407    }