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