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     * 24-Jun-2008 : Added new barPainter mechanism (DG);
090     *
091     */
092    
093    package org.jfree.chart.renderer.category;
094    
095    import java.awt.Graphics2D;
096    import java.awt.geom.Rectangle2D;
097    import java.io.Serializable;
098    
099    import org.jfree.chart.axis.CategoryAxis;
100    import org.jfree.chart.axis.ValueAxis;
101    import org.jfree.chart.entity.EntityCollection;
102    import org.jfree.chart.event.RendererChangeEvent;
103    import org.jfree.chart.labels.CategoryItemLabelGenerator;
104    import org.jfree.chart.labels.ItemLabelAnchor;
105    import org.jfree.chart.labels.ItemLabelPosition;
106    import org.jfree.chart.plot.CategoryPlot;
107    import org.jfree.chart.plot.PlotOrientation;
108    import org.jfree.data.DataUtilities;
109    import org.jfree.data.Range;
110    import org.jfree.data.category.CategoryDataset;
111    import org.jfree.data.general.DatasetUtilities;
112    import org.jfree.ui.RectangleEdge;
113    import org.jfree.ui.TextAnchor;
114    import org.jfree.util.PublicCloneable;
115    
116    /**
117     * A stacked bar renderer for use with the
118     * {@link org.jfree.chart.plot.CategoryPlot} class.
119     */
120    public class StackedBarRenderer extends BarRenderer
121            implements Cloneable, PublicCloneable, Serializable {
122    
123        /** For serialization. */
124        static final long serialVersionUID = 6402943811500067531L;
125    
126        /** A flag that controls whether the bars display values or percentages. */
127        private boolean renderAsPercentages;
128    
129        /**
130         * Creates a new renderer.  By default, the renderer has no tool tip
131         * generator and no URL generator.  These defaults have been chosen to
132         * minimise the processing required to generate a default chart.  If you
133         * require tool tips or URLs, then you can easily add the required
134         * generators.
135         */
136        public StackedBarRenderer() {
137            this(false);
138        }
139    
140        /**
141         * Creates a new renderer.
142         *
143         * @param renderAsPercentages  a flag that controls whether the data values
144         *                             are rendered as percentages.
145         */
146        public StackedBarRenderer(boolean renderAsPercentages) {
147            super();
148            this.renderAsPercentages = renderAsPercentages;
149    
150            // set the default item label positions, which will only be used if
151            // the user requests visible item labels...
152            ItemLabelPosition p = new ItemLabelPosition(ItemLabelAnchor.CENTER,
153                    TextAnchor.CENTER);
154            setBasePositiveItemLabelPosition(p);
155            setBaseNegativeItemLabelPosition(p);
156            setPositiveItemLabelPositionFallback(null);
157            setNegativeItemLabelPositionFallback(null);
158        }
159    
160        /**
161         * Returns <code>true</code> if the renderer displays each item value as
162         * a percentage (so that the stacked bars add to 100%), and
163         * <code>false</code> otherwise.
164         *
165         * @return A boolean.
166         *
167         * @see #setRenderAsPercentages(boolean)
168         */
169        public boolean getRenderAsPercentages() {
170            return this.renderAsPercentages;
171        }
172    
173        /**
174         * Sets the flag that controls whether the renderer displays each item
175         * value as a percentage (so that the stacked bars add to 100%), and sends
176         * a {@link RendererChangeEvent} to all registered listeners.
177         *
178         * @param asPercentages  the flag.
179         *
180         * @see #getRenderAsPercentages()
181         */
182        public void setRenderAsPercentages(boolean asPercentages) {
183            this.renderAsPercentages = asPercentages;
184            fireChangeEvent();
185        }
186    
187        /**
188         * Returns the number of passes (<code>3</code>) required by this renderer.
189         * The first pass is used to draw the bar shadows, the second pass is used
190         * to draw the bars, and the third pass is used to draw the item labels
191         * (if visible).
192         *
193         * @return The number of passes required by the renderer.
194         */
195        public int getPassCount() {
196            return 3;
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            boolean positive = (value > 0.0);
326            boolean inverted = rangeAxis.isInverted();
327            RectangleEdge barBase;
328            if (orientation == PlotOrientation.HORIZONTAL) {
329                if (positive && inverted || !positive && !inverted) {
330                    barBase = RectangleEdge.RIGHT;
331                }
332                else {
333                    barBase = RectangleEdge.LEFT;
334                }
335            }
336            else {
337                if (positive && !inverted || !positive && inverted) {
338                    barBase = RectangleEdge.BOTTOM;
339                }
340                else {
341                    barBase = RectangleEdge.TOP;
342                }
343            }
344    
345            RectangleEdge location = plot.getRangeAxisEdge();
346            if (positive) {
347                translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea,
348                        location);
349                translatedValue = rangeAxis.valueToJava2D(positiveBase + value,
350                        dataArea, location);
351            }
352            else {
353                translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea,
354                        location);
355                translatedValue = rangeAxis.valueToJava2D(negativeBase + value,
356                        dataArea, location);
357            }
358            double barL0 = Math.min(translatedBase, translatedValue);
359            double barLength = Math.max(Math.abs(translatedValue - translatedBase),
360                    getMinimumBarLength());
361    
362            Rectangle2D bar = null;
363            if (orientation == PlotOrientation.HORIZONTAL) {
364                bar = new Rectangle2D.Double(barL0, barW0, barLength,
365                        state.getBarWidth());
366            }
367            else {
368                bar = new Rectangle2D.Double(barW0, barL0, state.getBarWidth(),
369                        barLength);
370            }
371            if (pass == 0) {
372                if (getShadowsVisible()) {
373                    boolean pegToBase = (positive && (positiveBase == getBase()))
374                            || (!positive && (negativeBase == getBase()));
375                    getBarPainter().paintBarShadow(g2, this, row, column, bar,
376                            barBase, pegToBase);
377                }
378            }
379            else if (pass == 1) {
380                getBarPainter().paintBar(g2, this, row, column, bar, barBase);
381    
382                // add an item entity, if this information is being collected
383                EntityCollection entities = state.getEntityCollection();
384                if (entities != null) {
385                    addItemEntity(entities, dataset, row, column, bar);
386                }
387            }
388            else if (pass == 2) {
389                CategoryItemLabelGenerator generator = getItemLabelGenerator(row,
390                        column);
391                if (generator != null && isItemLabelVisible(row, column)) {
392                    drawItemLabel(g2, dataset, row, column, plot, generator, bar,
393                            (value < 0.0));
394                }
395            }
396        }
397    
398        /**
399         * Tests this renderer for equality with an arbitrary object.
400         *
401         * @param obj  the object (<code>null</code> permitted).
402         *
403         * @return A boolean.
404         */
405        public boolean equals(Object obj) {
406            if (obj == this) {
407                return true;
408            }
409            if (!(obj instanceof StackedBarRenderer)) {
410                return false;
411            }
412            StackedBarRenderer that = (StackedBarRenderer) obj;
413            if (this.renderAsPercentages != that.renderAsPercentages) {
414                return false;
415            }
416            return super.equals(obj);
417        }
418    
419    }