001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2009, 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-2009, 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     *                   Peter Kolb (patch 2511330);
037     *
038     * Changes
039     * -------
040     * 19-Oct-2001 : Version 1 (DG);
041     * 22-Oct-2001 : Renamed DataSource.java --> Dataset.java etc. (DG);
042     * 23-Oct-2001 : Changed intro and trail gaps on bar plots to use percentage of
043     *               available space rather than a fixed number of units (DG);
044     * 15-Nov-2001 : Modified to allow for null data values (DG);
045     * 22-Nov-2001 : Modified to allow for negative data values (DG);
046     * 13-Dec-2001 : Added tooltips (DG);
047     * 16-Jan-2002 : Fixed bug for single category datasets (DG);
048     * 15-Feb-2002 : Added isStacked() method (DG);
049     * 14-Mar-2002 : Modified to implement the CategoryItemRenderer interface (DG);
050     * 24-May-2002 : Incorporated tooltips into chart entities (DG);
051     * 11-Jun-2002 : Added check for (permitted) null info object, bug and fix
052     *               reported by David Basten.  Also updated Javadocs. (DG);
053     * 25-Jun-2002 : Removed redundant import (DG);
054     * 26-Jun-2002 : Small change to entity (DG);
055     * 05-Aug-2002 : Small modification to drawCategoryItem method to support URLs
056     *               for HTML image maps (RA);
057     * 08-Aug-2002 : Added optional linking lines, contributed by Thierry
058     *               Saura (DG);
059     * 26-Sep-2002 : Fixed errors reported by Checkstyle (DG);
060     * 24-Oct-2002 : Amendments for changes in CategoryDataset interface and
061     *               CategoryToolTipGenerator interface (DG);
062     * 05-Nov-2002 : Replaced references to CategoryDataset with TableDataset (DG);
063     * 26-Nov-2002 : Replaced isStacked() method with getRangeType() method (DG);
064     * 17-Jan-2003 : Moved plot classes to a separate package (DG);
065     * 25-Mar-2003 : Implemented Serializable (DG);
066     * 12-May-2003 : Merged horizontal and vertical stacked bar renderers (DG);
067     * 30-Jul-2003 : Modified entity constructor (CZ);
068     * 08-Sep-2003 : Fixed bug 799668 (isBarOutlineDrawn() ignored) (DG);
069     * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG);
070     * 21-Oct-2003 : Moved bar width into renderer state (DG);
071     * 26-Nov-2003 : Added code to respect maxBarWidth attribute (DG);
072     * 05-Nov-2004 : Changed to a two-pass renderer so that item labels are not
073     *               overwritten by other bars (DG);
074     * 07-Jan-2005 : Renamed getRangeExtent() --> findRangeBounds() (DG);
075     * 29-Mar-2005 : Modified drawItem() method so that a zero value is handled
076     *               within the code for positive rather than negative values (DG);
077     * 20-Apr-2005 : Renamed CategoryLabelGenerator
078     *               --> CategoryItemLabelGenerator (DG);
079     * 17-May-2005 : Added flag to allow rendering values as percentages - inspired
080     *               by patch 1200886 submitted by John Xiao (DG);
081     * 09-Jun-2005 : Added accessor methods for the renderAsPercentages flag,
082     *               provided equals() method, and use addItemEntity from
083     *               superclass (DG);
084     * 09-Jun-2005 : Added support for GradientPaint - see bug report 1215670 (DG);
085     * 22-Sep-2005 : Renamed getMaxBarWidth() --> getMaximumBarWidth() (DG);
086     * 29-Sep-2005 : Use outline stroke in drawItem method - see bug report
087     *               1304139 (DG);
088     * ------------- JFREECHART 1.0.x ---------------------------------------------
089     * 11-Oct-2006 : Source reformatting (DG);
090     * 24-Jun-2008 : Added new barPainter mechanism (DG);
091     * 04-Feb-2009 : Added support for hidden series (PK);
092     *
093     */
094    
095    package org.jfree.chart.renderer.category;
096    
097    import java.awt.Graphics2D;
098    import java.awt.geom.Rectangle2D;
099    import java.io.Serializable;
100    
101    import org.jfree.chart.axis.CategoryAxis;
102    import org.jfree.chart.axis.ValueAxis;
103    import org.jfree.chart.entity.EntityCollection;
104    import org.jfree.chart.event.RendererChangeEvent;
105    import org.jfree.chart.labels.CategoryItemLabelGenerator;
106    import org.jfree.chart.labels.ItemLabelAnchor;
107    import org.jfree.chart.labels.ItemLabelPosition;
108    import org.jfree.chart.plot.CategoryPlot;
109    import org.jfree.chart.plot.PlotOrientation;
110    import org.jfree.data.DataUtilities;
111    import org.jfree.data.Range;
112    import org.jfree.data.category.CategoryDataset;
113    import org.jfree.data.general.DatasetUtilities;
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 {@link CategoryPlot} class.
120     * The example shown here is generated by the
121     * <code>StackedBarChartDemo1.java</code> program included in the
122     * JFreeChart Demo Collection:
123     * <br><br>
124     * <img src="../../../../../images/StackedBarRendererSample.png"
125     * alt="StackedBarRendererSample.png" />
126     */
127    public class StackedBarRenderer extends BarRenderer
128            implements Cloneable, PublicCloneable, Serializable {
129    
130        /** For serialization. */
131        static final long serialVersionUID = 6402943811500067531L;
132    
133        /** A flag that controls whether the bars display values or percentages. */
134        private boolean renderAsPercentages;
135    
136        /**
137         * Creates a new renderer.  By default, the renderer has no tool tip
138         * generator and no URL generator.  These defaults have been chosen to
139         * minimise the processing required to generate a default chart.  If you
140         * require tool tips or URLs, then you can easily add the required
141         * generators.
142         */
143        public StackedBarRenderer() {
144            this(false);
145        }
146    
147        /**
148         * Creates a new renderer.
149         *
150         * @param renderAsPercentages  a flag that controls whether the data values
151         *                             are rendered as percentages.
152         */
153        public StackedBarRenderer(boolean renderAsPercentages) {
154            super();
155            this.renderAsPercentages = renderAsPercentages;
156    
157            // set the default item label positions, which will only be used if
158            // the user requests visible item labels...
159            ItemLabelPosition p = new ItemLabelPosition(ItemLabelAnchor.CENTER,
160                    TextAnchor.CENTER);
161            setBasePositiveItemLabelPosition(p);
162            setBaseNegativeItemLabelPosition(p);
163            setPositiveItemLabelPositionFallback(null);
164            setNegativeItemLabelPositionFallback(null);
165        }
166    
167        /**
168         * Returns <code>true</code> if the renderer displays each item value as
169         * a percentage (so that the stacked bars add to 100%), and
170         * <code>false</code> otherwise.
171         *
172         * @return A boolean.
173         *
174         * @see #setRenderAsPercentages(boolean)
175         */
176        public boolean getRenderAsPercentages() {
177            return this.renderAsPercentages;
178        }
179    
180        /**
181         * Sets the flag that controls whether the renderer displays each item
182         * value as a percentage (so that the stacked bars add to 100%), and sends
183         * a {@link RendererChangeEvent} to all registered listeners.
184         *
185         * @param asPercentages  the flag.
186         *
187         * @see #getRenderAsPercentages()
188         */
189        public void setRenderAsPercentages(boolean asPercentages) {
190            this.renderAsPercentages = asPercentages;
191            fireChangeEvent();
192        }
193    
194        /**
195         * Returns the number of passes (<code>3</code>) required by this renderer.
196         * The first pass is used to draw the bar shadows, the second pass is used
197         * to draw the bars, and the third pass is used to draw the item labels
198         * (if visible).
199         *
200         * @return The number of passes required by the renderer.
201         */
202        public int getPassCount() {
203            return 3;
204        }
205    
206        /**
207         * Returns the range of values the renderer requires to display all the
208         * items from the specified dataset.
209         *
210         * @param dataset  the dataset (<code>null</code> permitted).
211         *
212         * @return The range (or <code>null</code> if the dataset is empty).
213         */
214        public Range findRangeBounds(CategoryDataset dataset) {
215            if (dataset == null) {
216                return null;
217            }
218            if (this.renderAsPercentages) {
219                return new Range(0.0, 1.0);
220            }
221            else {
222                return DatasetUtilities.findStackedRangeBounds(dataset, getBase());
223            }
224        }
225    
226        /**
227         * Calculates the bar width and stores it in the renderer state.
228         *
229         * @param plot  the plot.
230         * @param dataArea  the data area.
231         * @param rendererIndex  the renderer index.
232         * @param state  the renderer state.
233         */
234        protected void calculateBarWidth(CategoryPlot plot,
235                                         Rectangle2D dataArea,
236                                         int rendererIndex,
237                                         CategoryItemRendererState state) {
238    
239            // calculate the bar width
240            CategoryAxis xAxis = plot.getDomainAxisForDataset(rendererIndex);
241            CategoryDataset data = plot.getDataset(rendererIndex);
242            if (data != null) {
243                PlotOrientation orientation = plot.getOrientation();
244                double space = 0.0;
245                if (orientation == PlotOrientation.HORIZONTAL) {
246                    space = dataArea.getHeight();
247                }
248                else if (orientation == PlotOrientation.VERTICAL) {
249                    space = dataArea.getWidth();
250                }
251                double maxWidth = space * getMaximumBarWidth();
252                int columns = data.getColumnCount();
253                double categoryMargin = 0.0;
254                if (columns > 1) {
255                    categoryMargin = xAxis.getCategoryMargin();
256                }
257    
258                double used = space * (1 - xAxis.getLowerMargin()
259                                         - xAxis.getUpperMargin()
260                                         - categoryMargin);
261                if (columns > 0) {
262                    state.setBarWidth(Math.min(used / columns, maxWidth));
263                }
264                else {
265                    state.setBarWidth(Math.min(used, maxWidth));
266                }
267            }
268    
269        }
270    
271        /**
272         * Draws a stacked bar for a specific item.
273         *
274         * @param g2  the graphics device.
275         * @param state  the renderer state.
276         * @param dataArea  the plot area.
277         * @param plot  the plot.
278         * @param domainAxis  the domain (category) axis.
279         * @param rangeAxis  the range (value) axis.
280         * @param dataset  the data.
281         * @param row  the row index (zero-based).
282         * @param column  the column index (zero-based).
283         * @param pass  the pass index.
284         */
285        public void drawItem(Graphics2D g2,
286                             CategoryItemRendererState state,
287                             Rectangle2D dataArea,
288                             CategoryPlot plot,
289                             CategoryAxis domainAxis,
290                             ValueAxis rangeAxis,
291                             CategoryDataset dataset,
292                             int row,
293                             int column,
294                             int pass) {
295    
296            if (!isSeriesVisible(row)) {
297                return;
298            }
299    
300            // nothing is drawn for null values...
301            Number dataValue = dataset.getValue(row, column);
302            if (dataValue == null) {
303                return;
304            }
305    
306            double value = dataValue.doubleValue();
307            double total = 0.0;  // only needed if calculating percentages
308            if (this.renderAsPercentages) {
309                total = DataUtilities.calculateColumnTotal(dataset, column,
310                        state.getVisibleSeriesArray());
311                value = value / total;
312            }
313    
314            PlotOrientation orientation = plot.getOrientation();
315            double barW0 = domainAxis.getCategoryMiddle(column, getColumnCount(),
316                    dataArea, plot.getDomainAxisEdge())
317                    - state.getBarWidth() / 2.0;
318    
319            double positiveBase = getBase();
320            double negativeBase = positiveBase;
321    
322            for (int i = 0; i < row; i++) {
323                Number v = dataset.getValue(i, column);
324                if (v != null && isSeriesVisible(i)) {
325                    double d = v.doubleValue();
326                    if (this.renderAsPercentages) {
327                        d = d / total;
328                    }
329                    if (d > 0) {
330                        positiveBase = positiveBase + d;
331                    }
332                    else {
333                        negativeBase = negativeBase + d;
334                    }
335                }
336            }
337    
338            double translatedBase;
339            double translatedValue;
340            boolean positive = (value > 0.0);
341            boolean inverted = rangeAxis.isInverted();
342            RectangleEdge barBase;
343            if (orientation == PlotOrientation.HORIZONTAL) {
344                if (positive && inverted || !positive && !inverted) {
345                    barBase = RectangleEdge.RIGHT;
346                }
347                else {
348                    barBase = RectangleEdge.LEFT;
349                }
350            }
351            else {
352                if (positive && !inverted || !positive && inverted) {
353                    barBase = RectangleEdge.BOTTOM;
354                }
355                else {
356                    barBase = RectangleEdge.TOP;
357                }
358            }
359    
360            RectangleEdge location = plot.getRangeAxisEdge();
361            if (positive) {
362                translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea,
363                        location);
364                translatedValue = rangeAxis.valueToJava2D(positiveBase + value,
365                        dataArea, location);
366            }
367            else {
368                translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea,
369                        location);
370                translatedValue = rangeAxis.valueToJava2D(negativeBase + value,
371                        dataArea, location);
372            }
373            double barL0 = Math.min(translatedBase, translatedValue);
374            double barLength = Math.max(Math.abs(translatedValue - translatedBase),
375                    getMinimumBarLength());
376    
377            Rectangle2D bar = null;
378            if (orientation == PlotOrientation.HORIZONTAL) {
379                bar = new Rectangle2D.Double(barL0, barW0, barLength,
380                        state.getBarWidth());
381            }
382            else {
383                bar = new Rectangle2D.Double(barW0, barL0, state.getBarWidth(),
384                        barLength);
385            }
386            if (pass == 0) {
387                if (getShadowsVisible()) {
388                    boolean pegToBase = (positive && (positiveBase == getBase()))
389                            || (!positive && (negativeBase == getBase()));
390                    getBarPainter().paintBarShadow(g2, this, row, column, bar,
391                            barBase, pegToBase);
392                }
393            }
394            else if (pass == 1) {
395                getBarPainter().paintBar(g2, this, row, column, bar, barBase);
396    
397                // add an item entity, if this information is being collected
398                EntityCollection entities = state.getEntityCollection();
399                if (entities != null) {
400                    addItemEntity(entities, dataset, row, column, bar);
401                }
402            }
403            else if (pass == 2) {
404                CategoryItemLabelGenerator generator = getItemLabelGenerator(row,
405                        column);
406                if (generator != null && isItemLabelVisible(row, column)) {
407                    drawItemLabel(g2, dataset, row, column, plot, generator, bar,
408                            (value < 0.0));
409                }
410            }
411        }
412    
413        /**
414         * Tests this renderer for equality with an arbitrary object.
415         *
416         * @param obj  the object (<code>null</code> permitted).
417         *
418         * @return A boolean.
419         */
420        public boolean equals(Object obj) {
421            if (obj == this) {
422                return true;
423            }
424            if (!(obj instanceof StackedBarRenderer)) {
425                return false;
426            }
427            StackedBarRenderer that = (StackedBarRenderer) obj;
428            if (this.renderAsPercentages != that.renderAsPercentages) {
429                return false;
430            }
431            return super.equals(obj);
432        }
433    
434    }