001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2006, 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-2006, 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.8 2006/10/11 09:41:49 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 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 public void setRenderAsPercentages(boolean asPercentages) { 184 this.renderAsPercentages = asPercentages; 185 notifyListeners(new RendererChangeEvent(this)); 186 } 187 188 /** 189 * Returns the number of passes (<code>2</code>) required by this renderer. 190 * The first pass is used to draw the bars, the second pass is used to 191 * draw the item labels (if visible). 192 * 193 * @return The number of passes required by the renderer. 194 */ 195 public int getPassCount() { 196 return 2; 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 RectangleEdge location = plot.getRangeAxisEdge(); 326 if (value >= 0.0) { 327 translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea, 328 location); 329 translatedValue = rangeAxis.valueToJava2D(positiveBase + value, 330 dataArea, location); 331 } 332 else { 333 translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea, 334 location); 335 translatedValue = rangeAxis.valueToJava2D(negativeBase + value, 336 dataArea, location); 337 } 338 double barL0 = Math.min(translatedBase, translatedValue); 339 double barLength = Math.max(Math.abs(translatedValue - translatedBase), 340 getMinimumBarLength()); 341 342 Rectangle2D bar = null; 343 if (orientation == PlotOrientation.HORIZONTAL) { 344 bar = new Rectangle2D.Double(barL0, barW0, barLength, 345 state.getBarWidth()); 346 } 347 else { 348 bar = new Rectangle2D.Double(barW0, barL0, state.getBarWidth(), 349 barLength); 350 } 351 if (pass == 0) { 352 Paint itemPaint = getItemPaint(row, column); 353 GradientPaintTransformer t = getGradientPaintTransformer(); 354 if (t != null && itemPaint instanceof GradientPaint) { 355 itemPaint = t.transform((GradientPaint) itemPaint, bar); 356 } 357 g2.setPaint(itemPaint); 358 g2.fill(bar); 359 if (isDrawBarOutline() 360 && state.getBarWidth() > BAR_OUTLINE_WIDTH_THRESHOLD) { 361 g2.setStroke(getItemOutlineStroke(row, column)); 362 g2.setPaint(getItemOutlinePaint(row, column)); 363 g2.draw(bar); 364 } 365 366 // add an item entity, if this information is being collected 367 EntityCollection entities = state.getEntityCollection(); 368 if (entities != null) { 369 addItemEntity(entities, dataset, row, column, bar); 370 } 371 } 372 else if (pass == 1) { 373 CategoryItemLabelGenerator generator 374 = getItemLabelGenerator(row, column); 375 if (generator != null && isItemLabelVisible(row, column)) { 376 drawItemLabel(g2, dataset, row, column, plot, generator, bar, 377 (value < 0.0)); 378 } 379 } 380 } 381 382 /** 383 * Tests this renderer for equality with an arbitrary object. 384 * 385 * @param obj the object (<code>null</code> permitted). 386 * 387 * @return A boolean. 388 */ 389 public boolean equals(Object obj) { 390 if (obj == this) { 391 return true; 392 } 393 if (!(obj instanceof StackedBarRenderer)) { 394 return false; 395 } 396 if (!super.equals(obj)) { 397 return false; 398 } 399 StackedBarRenderer that = (StackedBarRenderer) obj; 400 if (this.renderAsPercentages != that.renderAsPercentages) { 401 return false; 402 } 403 return true; 404 } 405 406 }