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     * StackedBarRenderer.java
029     * -----------------------
030     * (C) Copyright 2000-2006, 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.8 2006/10/11 09:41:49 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        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        public void setRenderAsPercentages(boolean asPercentages) {
184            this.renderAsPercentages = asPercentages; 
185            notifyListeners(new RendererChangeEvent(this));
186        }
187        
188        /**
189         * Returns the number of passes (<code>2</code>) required by this renderer. 
190         * The first pass is used to draw the bars, the second pass is used to
191         * draw the item labels (if visible).
192         * 
193         * @return The number of passes required by the renderer.
194         */
195        public int getPassCount() {
196            return 2;
197        }
198        
199        /**
200         * Returns the range of values the renderer requires to display all the
201         * items from the specified dataset.
202         * 
203         * @param dataset  the dataset (<code>null</code> permitted).
204         * 
205         * @return The range (or <code>null</code> if the dataset is empty).
206         */
207        public Range findRangeBounds(CategoryDataset dataset) {
208            if (this.renderAsPercentages) {
209                return new Range(0.0, 1.0);   
210            }
211            else {
212                return DatasetUtilities.findStackedRangeBounds(dataset, getBase());
213            }
214        }
215    
216        /**
217         * Calculates the bar width and stores it in the renderer state.
218         * 
219         * @param plot  the plot.
220         * @param dataArea  the data area.
221         * @param rendererIndex  the renderer index.
222         * @param state  the renderer state.
223         */
224        protected void calculateBarWidth(CategoryPlot plot, 
225                                         Rectangle2D dataArea, 
226                                         int rendererIndex,
227                                         CategoryItemRendererState state) {
228    
229            // calculate the bar width
230            CategoryAxis xAxis = plot.getDomainAxisForDataset(rendererIndex);
231            CategoryDataset data = plot.getDataset(rendererIndex);
232            if (data != null) {
233                PlotOrientation orientation = plot.getOrientation();
234                double space = 0.0;
235                if (orientation == PlotOrientation.HORIZONTAL) {
236                    space = dataArea.getHeight();
237                }
238                else if (orientation == PlotOrientation.VERTICAL) {
239                    space = dataArea.getWidth();
240                }
241                double maxWidth = space * getMaximumBarWidth();
242                int columns = data.getColumnCount();
243                double categoryMargin = 0.0;
244                if (columns > 1) {
245                    categoryMargin = xAxis.getCategoryMargin();
246                }
247    
248                double used = space * (1 - xAxis.getLowerMargin() 
249                                         - xAxis.getUpperMargin()
250                                         - categoryMargin);
251                if (columns > 0) {
252                    state.setBarWidth(Math.min(used / columns, maxWidth));
253                }
254                else {
255                    state.setBarWidth(Math.min(used, maxWidth));
256                }
257            }
258    
259        }
260    
261        /**
262         * Draws a stacked bar for a specific item.
263         *
264         * @param g2  the graphics device.
265         * @param state  the renderer state.
266         * @param dataArea  the plot area.
267         * @param plot  the plot.
268         * @param domainAxis  the domain (category) axis.
269         * @param rangeAxis  the range (value) axis.
270         * @param dataset  the data.
271         * @param row  the row index (zero-based).
272         * @param column  the column index (zero-based).
273         * @param pass  the pass index.
274         */
275        public void drawItem(Graphics2D g2,
276                             CategoryItemRendererState state,
277                             Rectangle2D dataArea,
278                             CategoryPlot plot,
279                             CategoryAxis domainAxis,
280                             ValueAxis rangeAxis,
281                             CategoryDataset dataset,
282                             int row,
283                             int column,
284                             int pass) {
285         
286            // nothing is drawn for null values...
287            Number dataValue = dataset.getValue(row, column);
288            if (dataValue == null) {
289                return;
290            }
291            
292            double value = dataValue.doubleValue();
293            double total = 0.0;  // only needed if calculating percentages
294            if (this.renderAsPercentages) {
295                total = DataUtilities.calculateColumnTotal(dataset, column);
296                value = value / total;
297            }
298            
299            PlotOrientation orientation = plot.getOrientation();
300            double barW0 = domainAxis.getCategoryMiddle(column, getColumnCount(), 
301                    dataArea, plot.getDomainAxisEdge()) 
302                    - state.getBarWidth() / 2.0;
303    
304            double positiveBase = getBase();
305            double negativeBase = positiveBase;
306    
307            for (int i = 0; i < row; i++) {
308                Number v = dataset.getValue(i, column);
309                if (v != null) {
310                    double d = v.doubleValue();
311                    if (this.renderAsPercentages) {
312                        d = d / total;
313                    }
314                    if (d > 0) {
315                        positiveBase = positiveBase + d;
316                    }
317                    else {
318                        negativeBase = negativeBase + d;
319                    }
320                }
321            }
322    
323            double translatedBase;
324            double translatedValue;
325            RectangleEdge location = plot.getRangeAxisEdge();
326            if (value >= 0.0) {
327                translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea, 
328                        location);
329                translatedValue = rangeAxis.valueToJava2D(positiveBase + value, 
330                        dataArea, location);
331            }
332            else {
333                translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea, 
334                        location);
335                translatedValue = rangeAxis.valueToJava2D(negativeBase + value, 
336                        dataArea, location);
337            }
338            double barL0 = Math.min(translatedBase, translatedValue);
339            double barLength = Math.max(Math.abs(translatedValue - translatedBase),
340                    getMinimumBarLength());
341    
342            Rectangle2D bar = null;
343            if (orientation == PlotOrientation.HORIZONTAL) {
344                bar = new Rectangle2D.Double(barL0, barW0, barLength, 
345                        state.getBarWidth());
346            }
347            else {
348                bar = new Rectangle2D.Double(barW0, barL0, state.getBarWidth(), 
349                        barLength);
350            }
351            if (pass == 0) {
352                Paint itemPaint = getItemPaint(row, column);
353                GradientPaintTransformer t = getGradientPaintTransformer();
354                if (t != null && itemPaint instanceof GradientPaint) {
355                    itemPaint = t.transform((GradientPaint) itemPaint, bar);
356                }
357                g2.setPaint(itemPaint);
358                g2.fill(bar);
359                if (isDrawBarOutline() 
360                        && state.getBarWidth() > BAR_OUTLINE_WIDTH_THRESHOLD) {
361                    g2.setStroke(getItemOutlineStroke(row, column));
362                    g2.setPaint(getItemOutlinePaint(row, column));
363                    g2.draw(bar);
364                }
365    
366                // add an item entity, if this information is being collected
367                EntityCollection entities = state.getEntityCollection();
368                if (entities != null) {
369                    addItemEntity(entities, dataset, row, column, bar);
370                }
371            }
372            else if (pass == 1) {
373                CategoryItemLabelGenerator generator 
374                    = getItemLabelGenerator(row, column);
375                if (generator != null && isItemLabelVisible(row, column)) {
376                    drawItemLabel(g2, dataset, row, column, plot, generator, bar, 
377                            (value < 0.0));
378                }
379            }        
380        }
381    
382        /**
383         * Tests this renderer for equality with an arbitrary object.
384         * 
385         * @param obj  the object (<code>null</code> permitted).
386         * 
387         * @return A boolean.
388         */
389        public boolean equals(Object obj) {
390            if (obj == this) {
391                return true;   
392            }
393            if (!(obj instanceof StackedBarRenderer)) {
394                return false;   
395            }
396            if (!super.equals(obj)) {
397                return false;   
398            }
399            StackedBarRenderer that = (StackedBarRenderer) obj;
400            if (this.renderAsPercentages != that.renderAsPercentages) {
401                return false;   
402            }
403            return true;
404        }
405    
406    }