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