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 }