001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2009, 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-2009, 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 * Peter Kolb (patch 2511330); 037 * 038 * Changes 039 * ------- 040 * 19-Oct-2001 : Version 1 (DG); 041 * 22-Oct-2001 : Renamed DataSource.java --> Dataset.java etc. (DG); 042 * 23-Oct-2001 : Changed intro and trail gaps on bar plots to use percentage of 043 * available space rather than a fixed number of units (DG); 044 * 15-Nov-2001 : Modified to allow for null data values (DG); 045 * 22-Nov-2001 : Modified to allow for negative data values (DG); 046 * 13-Dec-2001 : Added tooltips (DG); 047 * 16-Jan-2002 : Fixed bug for single category datasets (DG); 048 * 15-Feb-2002 : Added isStacked() method (DG); 049 * 14-Mar-2002 : Modified to implement the CategoryItemRenderer interface (DG); 050 * 24-May-2002 : Incorporated tooltips into chart entities (DG); 051 * 11-Jun-2002 : Added check for (permitted) null info object, bug and fix 052 * reported by David Basten. Also updated Javadocs. (DG); 053 * 25-Jun-2002 : Removed redundant import (DG); 054 * 26-Jun-2002 : Small change to entity (DG); 055 * 05-Aug-2002 : Small modification to drawCategoryItem method to support URLs 056 * for HTML image maps (RA); 057 * 08-Aug-2002 : Added optional linking lines, contributed by Thierry 058 * Saura (DG); 059 * 26-Sep-2002 : Fixed errors reported by Checkstyle (DG); 060 * 24-Oct-2002 : Amendments for changes in CategoryDataset interface and 061 * CategoryToolTipGenerator interface (DG); 062 * 05-Nov-2002 : Replaced references to CategoryDataset with TableDataset (DG); 063 * 26-Nov-2002 : Replaced isStacked() method with getRangeType() method (DG); 064 * 17-Jan-2003 : Moved plot classes to a separate package (DG); 065 * 25-Mar-2003 : Implemented Serializable (DG); 066 * 12-May-2003 : Merged horizontal and vertical stacked bar renderers (DG); 067 * 30-Jul-2003 : Modified entity constructor (CZ); 068 * 08-Sep-2003 : Fixed bug 799668 (isBarOutlineDrawn() ignored) (DG); 069 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG); 070 * 21-Oct-2003 : Moved bar width into renderer state (DG); 071 * 26-Nov-2003 : Added code to respect maxBarWidth attribute (DG); 072 * 05-Nov-2004 : Changed to a two-pass renderer so that item labels are not 073 * overwritten by other bars (DG); 074 * 07-Jan-2005 : Renamed getRangeExtent() --> findRangeBounds() (DG); 075 * 29-Mar-2005 : Modified drawItem() method so that a zero value is handled 076 * within the code for positive rather than negative values (DG); 077 * 20-Apr-2005 : Renamed CategoryLabelGenerator 078 * --> CategoryItemLabelGenerator (DG); 079 * 17-May-2005 : Added flag to allow rendering values as percentages - inspired 080 * by patch 1200886 submitted by John Xiao (DG); 081 * 09-Jun-2005 : Added accessor methods for the renderAsPercentages flag, 082 * provided equals() method, and use addItemEntity from 083 * superclass (DG); 084 * 09-Jun-2005 : Added support for GradientPaint - see bug report 1215670 (DG); 085 * 22-Sep-2005 : Renamed getMaxBarWidth() --> getMaximumBarWidth() (DG); 086 * 29-Sep-2005 : Use outline stroke in drawItem method - see bug report 087 * 1304139 (DG); 088 * ------------- JFREECHART 1.0.x --------------------------------------------- 089 * 11-Oct-2006 : Source reformatting (DG); 090 * 24-Jun-2008 : Added new barPainter mechanism (DG); 091 * 04-Feb-2009 : Added support for hidden series (PK); 092 * 093 */ 094 095 package org.jfree.chart.renderer.category; 096 097 import java.awt.Graphics2D; 098 import java.awt.geom.Rectangle2D; 099 import java.io.Serializable; 100 101 import org.jfree.chart.axis.CategoryAxis; 102 import org.jfree.chart.axis.ValueAxis; 103 import org.jfree.chart.entity.EntityCollection; 104 import org.jfree.chart.event.RendererChangeEvent; 105 import org.jfree.chart.labels.CategoryItemLabelGenerator; 106 import org.jfree.chart.labels.ItemLabelAnchor; 107 import org.jfree.chart.labels.ItemLabelPosition; 108 import org.jfree.chart.plot.CategoryPlot; 109 import org.jfree.chart.plot.PlotOrientation; 110 import org.jfree.data.DataUtilities; 111 import org.jfree.data.Range; 112 import org.jfree.data.category.CategoryDataset; 113 import org.jfree.data.general.DatasetUtilities; 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 {@link CategoryPlot} class. 120 * The example shown here is generated by the 121 * <code>StackedBarChartDemo1.java</code> program included in the 122 * JFreeChart Demo Collection: 123 * <br><br> 124 * <img src="../../../../../images/StackedBarRendererSample.png" 125 * alt="StackedBarRendererSample.png" /> 126 */ 127 public class StackedBarRenderer extends BarRenderer 128 implements Cloneable, PublicCloneable, Serializable { 129 130 /** For serialization. */ 131 static final long serialVersionUID = 6402943811500067531L; 132 133 /** A flag that controls whether the bars display values or percentages. */ 134 private boolean renderAsPercentages; 135 136 /** 137 * Creates a new renderer. By default, the renderer has no tool tip 138 * generator and no URL generator. These defaults have been chosen to 139 * minimise the processing required to generate a default chart. If you 140 * require tool tips or URLs, then you can easily add the required 141 * generators. 142 */ 143 public StackedBarRenderer() { 144 this(false); 145 } 146 147 /** 148 * Creates a new renderer. 149 * 150 * @param renderAsPercentages a flag that controls whether the data values 151 * are rendered as percentages. 152 */ 153 public StackedBarRenderer(boolean renderAsPercentages) { 154 super(); 155 this.renderAsPercentages = renderAsPercentages; 156 157 // set the default item label positions, which will only be used if 158 // the user requests visible item labels... 159 ItemLabelPosition p = new ItemLabelPosition(ItemLabelAnchor.CENTER, 160 TextAnchor.CENTER); 161 setBasePositiveItemLabelPosition(p); 162 setBaseNegativeItemLabelPosition(p); 163 setPositiveItemLabelPositionFallback(null); 164 setNegativeItemLabelPositionFallback(null); 165 } 166 167 /** 168 * Returns <code>true</code> if the renderer displays each item value as 169 * a percentage (so that the stacked bars add to 100%), and 170 * <code>false</code> otherwise. 171 * 172 * @return A boolean. 173 * 174 * @see #setRenderAsPercentages(boolean) 175 */ 176 public boolean getRenderAsPercentages() { 177 return this.renderAsPercentages; 178 } 179 180 /** 181 * Sets the flag that controls whether the renderer displays each item 182 * value as a percentage (so that the stacked bars add to 100%), and sends 183 * a {@link RendererChangeEvent} to all registered listeners. 184 * 185 * @param asPercentages the flag. 186 * 187 * @see #getRenderAsPercentages() 188 */ 189 public void setRenderAsPercentages(boolean asPercentages) { 190 this.renderAsPercentages = asPercentages; 191 fireChangeEvent(); 192 } 193 194 /** 195 * Returns the number of passes (<code>3</code>) required by this renderer. 196 * The first pass is used to draw the bar shadows, the second pass is used 197 * to draw the bars, and the third pass is used to draw the item labels 198 * (if visible). 199 * 200 * @return The number of passes required by the renderer. 201 */ 202 public int getPassCount() { 203 return 3; 204 } 205 206 /** 207 * Returns the range of values the renderer requires to display all the 208 * items from the specified dataset. 209 * 210 * @param dataset the dataset (<code>null</code> permitted). 211 * 212 * @return The range (or <code>null</code> if the dataset is empty). 213 */ 214 public Range findRangeBounds(CategoryDataset dataset) { 215 if (dataset == null) { 216 return null; 217 } 218 if (this.renderAsPercentages) { 219 return new Range(0.0, 1.0); 220 } 221 else { 222 return DatasetUtilities.findStackedRangeBounds(dataset, getBase()); 223 } 224 } 225 226 /** 227 * Calculates the bar width and stores it in the renderer state. 228 * 229 * @param plot the plot. 230 * @param dataArea the data area. 231 * @param rendererIndex the renderer index. 232 * @param state the renderer state. 233 */ 234 protected void calculateBarWidth(CategoryPlot plot, 235 Rectangle2D dataArea, 236 int rendererIndex, 237 CategoryItemRendererState state) { 238 239 // calculate the bar width 240 CategoryAxis xAxis = plot.getDomainAxisForDataset(rendererIndex); 241 CategoryDataset data = plot.getDataset(rendererIndex); 242 if (data != null) { 243 PlotOrientation orientation = plot.getOrientation(); 244 double space = 0.0; 245 if (orientation == PlotOrientation.HORIZONTAL) { 246 space = dataArea.getHeight(); 247 } 248 else if (orientation == PlotOrientation.VERTICAL) { 249 space = dataArea.getWidth(); 250 } 251 double maxWidth = space * getMaximumBarWidth(); 252 int columns = data.getColumnCount(); 253 double categoryMargin = 0.0; 254 if (columns > 1) { 255 categoryMargin = xAxis.getCategoryMargin(); 256 } 257 258 double used = space * (1 - xAxis.getLowerMargin() 259 - xAxis.getUpperMargin() 260 - categoryMargin); 261 if (columns > 0) { 262 state.setBarWidth(Math.min(used / columns, maxWidth)); 263 } 264 else { 265 state.setBarWidth(Math.min(used, maxWidth)); 266 } 267 } 268 269 } 270 271 /** 272 * Draws a stacked bar for a specific item. 273 * 274 * @param g2 the graphics device. 275 * @param state the renderer state. 276 * @param dataArea the plot area. 277 * @param plot the plot. 278 * @param domainAxis the domain (category) axis. 279 * @param rangeAxis the range (value) axis. 280 * @param dataset the data. 281 * @param row the row index (zero-based). 282 * @param column the column index (zero-based). 283 * @param pass the pass index. 284 */ 285 public void drawItem(Graphics2D g2, 286 CategoryItemRendererState state, 287 Rectangle2D dataArea, 288 CategoryPlot plot, 289 CategoryAxis domainAxis, 290 ValueAxis rangeAxis, 291 CategoryDataset dataset, 292 int row, 293 int column, 294 int pass) { 295 296 if (!isSeriesVisible(row)) { 297 return; 298 } 299 300 // nothing is drawn for null values... 301 Number dataValue = dataset.getValue(row, column); 302 if (dataValue == null) { 303 return; 304 } 305 306 double value = dataValue.doubleValue(); 307 double total = 0.0; // only needed if calculating percentages 308 if (this.renderAsPercentages) { 309 total = DataUtilities.calculateColumnTotal(dataset, column, 310 state.getVisibleSeriesArray()); 311 value = value / total; 312 } 313 314 PlotOrientation orientation = plot.getOrientation(); 315 double barW0 = domainAxis.getCategoryMiddle(column, getColumnCount(), 316 dataArea, plot.getDomainAxisEdge()) 317 - state.getBarWidth() / 2.0; 318 319 double positiveBase = getBase(); 320 double negativeBase = positiveBase; 321 322 for (int i = 0; i < row; i++) { 323 Number v = dataset.getValue(i, column); 324 if (v != null && isSeriesVisible(i)) { 325 double d = v.doubleValue(); 326 if (this.renderAsPercentages) { 327 d = d / total; 328 } 329 if (d > 0) { 330 positiveBase = positiveBase + d; 331 } 332 else { 333 negativeBase = negativeBase + d; 334 } 335 } 336 } 337 338 double translatedBase; 339 double translatedValue; 340 boolean positive = (value > 0.0); 341 boolean inverted = rangeAxis.isInverted(); 342 RectangleEdge barBase; 343 if (orientation == PlotOrientation.HORIZONTAL) { 344 if (positive && inverted || !positive && !inverted) { 345 barBase = RectangleEdge.RIGHT; 346 } 347 else { 348 barBase = RectangleEdge.LEFT; 349 } 350 } 351 else { 352 if (positive && !inverted || !positive && inverted) { 353 barBase = RectangleEdge.BOTTOM; 354 } 355 else { 356 barBase = RectangleEdge.TOP; 357 } 358 } 359 360 RectangleEdge location = plot.getRangeAxisEdge(); 361 if (positive) { 362 translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea, 363 location); 364 translatedValue = rangeAxis.valueToJava2D(positiveBase + value, 365 dataArea, location); 366 } 367 else { 368 translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea, 369 location); 370 translatedValue = rangeAxis.valueToJava2D(negativeBase + value, 371 dataArea, location); 372 } 373 double barL0 = Math.min(translatedBase, translatedValue); 374 double barLength = Math.max(Math.abs(translatedValue - translatedBase), 375 getMinimumBarLength()); 376 377 Rectangle2D bar = null; 378 if (orientation == PlotOrientation.HORIZONTAL) { 379 bar = new Rectangle2D.Double(barL0, barW0, barLength, 380 state.getBarWidth()); 381 } 382 else { 383 bar = new Rectangle2D.Double(barW0, barL0, state.getBarWidth(), 384 barLength); 385 } 386 if (pass == 0) { 387 if (getShadowsVisible()) { 388 boolean pegToBase = (positive && (positiveBase == getBase())) 389 || (!positive && (negativeBase == getBase())); 390 getBarPainter().paintBarShadow(g2, this, row, column, bar, 391 barBase, pegToBase); 392 } 393 } 394 else if (pass == 1) { 395 getBarPainter().paintBar(g2, this, row, column, bar, barBase); 396 397 // add an item entity, if this information is being collected 398 EntityCollection entities = state.getEntityCollection(); 399 if (entities != null) { 400 addItemEntity(entities, dataset, row, column, bar); 401 } 402 } 403 else if (pass == 2) { 404 CategoryItemLabelGenerator generator = getItemLabelGenerator(row, 405 column); 406 if (generator != null && isItemLabelVisible(row, column)) { 407 drawItemLabel(g2, dataset, row, column, plot, generator, bar, 408 (value < 0.0)); 409 } 410 } 411 } 412 413 /** 414 * Tests this renderer for equality with an arbitrary object. 415 * 416 * @param obj the object (<code>null</code> permitted). 417 * 418 * @return A boolean. 419 */ 420 public boolean equals(Object obj) { 421 if (obj == this) { 422 return true; 423 } 424 if (!(obj instanceof StackedBarRenderer)) { 425 return false; 426 } 427 StackedBarRenderer that = (StackedBarRenderer) obj; 428 if (this.renderAsPercentages != that.renderAsPercentages) { 429 return false; 430 } 431 return super.equals(obj); 432 } 433 434 }