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 }