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
095package org.jfree.chart.renderer.category;
096
097import java.awt.Graphics2D;
098import java.awt.geom.Rectangle2D;
099import java.io.Serializable;
100
101import org.jfree.chart.axis.CategoryAxis;
102import org.jfree.chart.axis.ValueAxis;
103import org.jfree.chart.entity.EntityCollection;
104import org.jfree.chart.event.RendererChangeEvent;
105import org.jfree.chart.labels.CategoryItemLabelGenerator;
106import org.jfree.chart.labels.ItemLabelAnchor;
107import org.jfree.chart.labels.ItemLabelPosition;
108import org.jfree.chart.plot.CategoryPlot;
109import org.jfree.chart.plot.PlotOrientation;
110import org.jfree.data.DataUtilities;
111import org.jfree.data.Range;
112import org.jfree.data.category.CategoryDataset;
113import org.jfree.data.general.DatasetUtilities;
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 {@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 */
127public 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}