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