001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2008, 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-2008, 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 * 24-Jun-2008 : Added new barPainter mechanism (DG); 090 * 091 */ 092 093 package org.jfree.chart.renderer.category; 094 095 import java.awt.Graphics2D; 096 import java.awt.geom.Rectangle2D; 097 import java.io.Serializable; 098 099 import org.jfree.chart.axis.CategoryAxis; 100 import org.jfree.chart.axis.ValueAxis; 101 import org.jfree.chart.entity.EntityCollection; 102 import org.jfree.chart.event.RendererChangeEvent; 103 import org.jfree.chart.labels.CategoryItemLabelGenerator; 104 import org.jfree.chart.labels.ItemLabelAnchor; 105 import org.jfree.chart.labels.ItemLabelPosition; 106 import org.jfree.chart.plot.CategoryPlot; 107 import org.jfree.chart.plot.PlotOrientation; 108 import org.jfree.data.DataUtilities; 109 import org.jfree.data.Range; 110 import org.jfree.data.category.CategoryDataset; 111 import org.jfree.data.general.DatasetUtilities; 112 import org.jfree.ui.RectangleEdge; 113 import org.jfree.ui.TextAnchor; 114 import org.jfree.util.PublicCloneable; 115 116 /** 117 * A stacked bar renderer for use with the 118 * {@link org.jfree.chart.plot.CategoryPlot} class. 119 */ 120 public class StackedBarRenderer extends BarRenderer 121 implements Cloneable, PublicCloneable, Serializable { 122 123 /** For serialization. */ 124 static final long serialVersionUID = 6402943811500067531L; 125 126 /** A flag that controls whether the bars display values or percentages. */ 127 private boolean renderAsPercentages; 128 129 /** 130 * Creates a new renderer. By default, the renderer has no tool tip 131 * generator and no URL generator. These defaults have been chosen to 132 * minimise the processing required to generate a default chart. If you 133 * require tool tips or URLs, then you can easily add the required 134 * generators. 135 */ 136 public StackedBarRenderer() { 137 this(false); 138 } 139 140 /** 141 * Creates a new renderer. 142 * 143 * @param renderAsPercentages a flag that controls whether the data values 144 * are rendered as percentages. 145 */ 146 public StackedBarRenderer(boolean renderAsPercentages) { 147 super(); 148 this.renderAsPercentages = renderAsPercentages; 149 150 // set the default item label positions, which will only be used if 151 // the user requests visible item labels... 152 ItemLabelPosition p = new ItemLabelPosition(ItemLabelAnchor.CENTER, 153 TextAnchor.CENTER); 154 setBasePositiveItemLabelPosition(p); 155 setBaseNegativeItemLabelPosition(p); 156 setPositiveItemLabelPositionFallback(null); 157 setNegativeItemLabelPositionFallback(null); 158 } 159 160 /** 161 * Returns <code>true</code> if the renderer displays each item value as 162 * a percentage (so that the stacked bars add to 100%), and 163 * <code>false</code> otherwise. 164 * 165 * @return A boolean. 166 * 167 * @see #setRenderAsPercentages(boolean) 168 */ 169 public boolean getRenderAsPercentages() { 170 return this.renderAsPercentages; 171 } 172 173 /** 174 * Sets the flag that controls whether the renderer displays each item 175 * value as a percentage (so that the stacked bars add to 100%), and sends 176 * a {@link RendererChangeEvent} to all registered listeners. 177 * 178 * @param asPercentages the flag. 179 * 180 * @see #getRenderAsPercentages() 181 */ 182 public void setRenderAsPercentages(boolean asPercentages) { 183 this.renderAsPercentages = asPercentages; 184 fireChangeEvent(); 185 } 186 187 /** 188 * Returns the number of passes (<code>3</code>) required by this renderer. 189 * The first pass is used to draw the bar shadows, the second pass is used 190 * to draw the bars, and the third pass is used to draw the item labels 191 * (if visible). 192 * 193 * @return The number of passes required by the renderer. 194 */ 195 public int getPassCount() { 196 return 3; 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 boolean positive = (value > 0.0); 326 boolean inverted = rangeAxis.isInverted(); 327 RectangleEdge barBase; 328 if (orientation == PlotOrientation.HORIZONTAL) { 329 if (positive && inverted || !positive && !inverted) { 330 barBase = RectangleEdge.RIGHT; 331 } 332 else { 333 barBase = RectangleEdge.LEFT; 334 } 335 } 336 else { 337 if (positive && !inverted || !positive && inverted) { 338 barBase = RectangleEdge.BOTTOM; 339 } 340 else { 341 barBase = RectangleEdge.TOP; 342 } 343 } 344 345 RectangleEdge location = plot.getRangeAxisEdge(); 346 if (positive) { 347 translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea, 348 location); 349 translatedValue = rangeAxis.valueToJava2D(positiveBase + value, 350 dataArea, location); 351 } 352 else { 353 translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea, 354 location); 355 translatedValue = rangeAxis.valueToJava2D(negativeBase + value, 356 dataArea, location); 357 } 358 double barL0 = Math.min(translatedBase, translatedValue); 359 double barLength = Math.max(Math.abs(translatedValue - translatedBase), 360 getMinimumBarLength()); 361 362 Rectangle2D bar = null; 363 if (orientation == PlotOrientation.HORIZONTAL) { 364 bar = new Rectangle2D.Double(barL0, barW0, barLength, 365 state.getBarWidth()); 366 } 367 else { 368 bar = new Rectangle2D.Double(barW0, barL0, state.getBarWidth(), 369 barLength); 370 } 371 if (pass == 0) { 372 if (getShadowsVisible()) { 373 boolean pegToBase = (positive && (positiveBase == getBase())) 374 || (!positive && (negativeBase == getBase())); 375 getBarPainter().paintBarShadow(g2, this, row, column, bar, 376 barBase, pegToBase); 377 } 378 } 379 else if (pass == 1) { 380 getBarPainter().paintBar(g2, this, row, column, bar, barBase); 381 382 // add an item entity, if this information is being collected 383 EntityCollection entities = state.getEntityCollection(); 384 if (entities != null) { 385 addItemEntity(entities, dataset, row, column, bar); 386 } 387 } 388 else if (pass == 2) { 389 CategoryItemLabelGenerator generator = getItemLabelGenerator(row, 390 column); 391 if (generator != null && isItemLabelVisible(row, column)) { 392 drawItemLabel(g2, dataset, row, column, plot, generator, bar, 393 (value < 0.0)); 394 } 395 } 396 } 397 398 /** 399 * Tests this renderer for equality with an arbitrary object. 400 * 401 * @param obj the object (<code>null</code> permitted). 402 * 403 * @return A boolean. 404 */ 405 public boolean equals(Object obj) { 406 if (obj == this) { 407 return true; 408 } 409 if (!(obj instanceof StackedBarRenderer)) { 410 return false; 411 } 412 StackedBarRenderer that = (StackedBarRenderer) obj; 413 if (this.renderAsPercentages != that.renderAsPercentages) { 414 return false; 415 } 416 return super.equals(obj); 417 } 418 419 }