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