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