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
092package org.jfree.chart.renderer.category;
093
094import java.awt.GradientPaint;
095import java.awt.Graphics2D;
096import java.awt.Paint;
097import java.awt.geom.Rectangle2D;
098import java.io.Serializable;
099
100import org.jfree.chart.axis.CategoryAxis;
101import org.jfree.chart.axis.ValueAxis;
102import org.jfree.chart.entity.EntityCollection;
103import org.jfree.chart.event.RendererChangeEvent;
104import org.jfree.chart.labels.CategoryItemLabelGenerator;
105import org.jfree.chart.labels.ItemLabelAnchor;
106import org.jfree.chart.labels.ItemLabelPosition;
107import org.jfree.chart.plot.CategoryPlot;
108import org.jfree.chart.plot.PlotOrientation;
109import org.jfree.data.DataUtilities;
110import org.jfree.data.Range;
111import org.jfree.data.category.CategoryDataset;
112import org.jfree.data.general.DatasetUtilities;
113import org.jfree.ui.GradientPaintTransformer;
114import org.jfree.ui.RectangleEdge;
115import org.jfree.ui.TextAnchor;
116import org.jfree.util.PublicCloneable;
117
118/**
119 * A stacked bar renderer for use with the 
120 * {@link org.jfree.chart.plot.CategoryPlot} class.
121 */
122public 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        notifyListeners(new RendererChangeEvent(this));
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}