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