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