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