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 }