001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2014, 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 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * ----------------------- 028 * StackedBarRenderer.java 029 * ----------------------- 030 * (C) Copyright 2000-2014, 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 095package org.jfree.chart.renderer.category; 096 097import java.awt.Graphics2D; 098import java.awt.geom.Rectangle2D; 099import java.io.Serializable; 100 101import org.jfree.chart.axis.CategoryAxis; 102import org.jfree.chart.axis.ValueAxis; 103import org.jfree.chart.entity.EntityCollection; 104import org.jfree.chart.event.RendererChangeEvent; 105import org.jfree.chart.labels.CategoryItemLabelGenerator; 106import org.jfree.chart.labels.ItemLabelAnchor; 107import org.jfree.chart.labels.ItemLabelPosition; 108import org.jfree.chart.plot.CategoryPlot; 109import org.jfree.chart.plot.PlotOrientation; 110import org.jfree.data.DataUtilities; 111import org.jfree.data.Range; 112import org.jfree.data.category.CategoryDataset; 113import org.jfree.data.general.DatasetUtilities; 114import org.jfree.ui.RectangleEdge; 115import org.jfree.ui.TextAnchor; 116import 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 */ 127public 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 @Override 203 public int getPassCount() { 204 return 3; 205 } 206 207 /** 208 * Returns the range of values the renderer requires to display all the 209 * items from the specified dataset. 210 * 211 * @param dataset the dataset (<code>null</code> permitted). 212 * 213 * @return The range (or <code>null</code> if the dataset is empty). 214 */ 215 @Override 216 public Range findRangeBounds(CategoryDataset dataset) { 217 if (dataset == null) { 218 return null; 219 } 220 if (this.renderAsPercentages) { 221 return new Range(0.0, 1.0); 222 } 223 else { 224 return DatasetUtilities.findStackedRangeBounds(dataset, getBase()); 225 } 226 } 227 228 /** 229 * Calculates the bar width and stores it in the renderer state. 230 * 231 * @param plot the plot. 232 * @param dataArea the data area. 233 * @param rendererIndex the renderer index. 234 * @param state the renderer state. 235 */ 236 @Override 237 protected void calculateBarWidth(CategoryPlot plot, Rectangle2D dataArea, 238 int rendererIndex, CategoryItemRendererState state) { 239 240 // calculate the bar width 241 CategoryAxis xAxis = plot.getDomainAxisForDataset(rendererIndex); 242 CategoryDataset data = plot.getDataset(rendererIndex); 243 if (data != null) { 244 PlotOrientation orientation = plot.getOrientation(); 245 double space = 0.0; 246 if (orientation == PlotOrientation.HORIZONTAL) { 247 space = dataArea.getHeight(); 248 } 249 else if (orientation == PlotOrientation.VERTICAL) { 250 space = dataArea.getWidth(); 251 } 252 double maxWidth = space * getMaximumBarWidth(); 253 int columns = data.getColumnCount(); 254 double categoryMargin = 0.0; 255 if (columns > 1) { 256 categoryMargin = xAxis.getCategoryMargin(); 257 } 258 259 double used = space * (1 - xAxis.getLowerMargin() 260 - xAxis.getUpperMargin() 261 - categoryMargin); 262 if (columns > 0) { 263 state.setBarWidth(Math.min(used / columns, maxWidth)); 264 } 265 else { 266 state.setBarWidth(Math.min(used, maxWidth)); 267 } 268 } 269 270 } 271 272 /** 273 * Draws a stacked bar for a specific item. 274 * 275 * @param g2 the graphics device. 276 * @param state the renderer state. 277 * @param dataArea the plot area. 278 * @param plot the plot. 279 * @param domainAxis the domain (category) axis. 280 * @param rangeAxis the range (value) axis. 281 * @param dataset the data. 282 * @param row the row index (zero-based). 283 * @param column the column index (zero-based). 284 * @param pass the pass index. 285 */ 286 @Override 287 public void drawItem(Graphics2D g2, CategoryItemRendererState state, 288 Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 289 ValueAxis rangeAxis, CategoryDataset dataset, int row, 290 int column, int pass) { 291 292 if (!isSeriesVisible(row)) { 293 return; 294 } 295 296 // nothing is drawn for null values... 297 Number dataValue = dataset.getValue(row, column); 298 if (dataValue == null) { 299 return; 300 } 301 302 double value = dataValue.doubleValue(); 303 double total = 0.0; // only needed if calculating percentages 304 if (this.renderAsPercentages) { 305 total = DataUtilities.calculateColumnTotal(dataset, column, 306 state.getVisibleSeriesArray()); 307 value = value / total; 308 } 309 310 PlotOrientation orientation = plot.getOrientation(); 311 double barW0 = domainAxis.getCategoryMiddle(column, getColumnCount(), 312 dataArea, plot.getDomainAxisEdge()) 313 - state.getBarWidth() / 2.0; 314 315 double positiveBase = getBase(); 316 double negativeBase = positiveBase; 317 318 for (int i = 0; i < row; i++) { 319 Number v = dataset.getValue(i, column); 320 if (v != null && isSeriesVisible(i)) { 321 double d = v.doubleValue(); 322 if (this.renderAsPercentages) { 323 d = d / total; 324 } 325 if (d > 0) { 326 positiveBase = positiveBase + d; 327 } 328 else { 329 negativeBase = negativeBase + d; 330 } 331 } 332 } 333 334 double translatedBase; 335 double translatedValue; 336 boolean positive = (value > 0.0); 337 boolean inverted = rangeAxis.isInverted(); 338 RectangleEdge barBase; 339 if (orientation == PlotOrientation.HORIZONTAL) { 340 if (positive && inverted || !positive && !inverted) { 341 barBase = RectangleEdge.RIGHT; 342 } 343 else { 344 barBase = RectangleEdge.LEFT; 345 } 346 } 347 else { 348 if (positive && !inverted || !positive && inverted) { 349 barBase = RectangleEdge.BOTTOM; 350 } 351 else { 352 barBase = RectangleEdge.TOP; 353 } 354 } 355 356 RectangleEdge location = plot.getRangeAxisEdge(); 357 if (positive) { 358 translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea, 359 location); 360 translatedValue = rangeAxis.valueToJava2D(positiveBase + value, 361 dataArea, location); 362 } 363 else { 364 translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea, 365 location); 366 translatedValue = rangeAxis.valueToJava2D(negativeBase + value, 367 dataArea, location); 368 } 369 double barL0 = Math.min(translatedBase, translatedValue); 370 double barLength = Math.max(Math.abs(translatedValue - translatedBase), 371 getMinimumBarLength()); 372 373 Rectangle2D bar; 374 if (orientation == PlotOrientation.HORIZONTAL) { 375 bar = new Rectangle2D.Double(barL0, barW0, barLength, 376 state.getBarWidth()); 377 } 378 else { 379 bar = new Rectangle2D.Double(barW0, barL0, state.getBarWidth(), 380 barLength); 381 } 382 if (pass == 0) { 383 if (getShadowsVisible()) { 384 boolean pegToBase = (positive && (positiveBase == getBase())) 385 || (!positive && (negativeBase == getBase())); 386 getBarPainter().paintBarShadow(g2, this, row, column, bar, 387 barBase, pegToBase); 388 } 389 } 390 else if (pass == 1) { 391 getBarPainter().paintBar(g2, this, row, column, bar, barBase); 392 393 // add an item entity, if this information is being collected 394 EntityCollection entities = state.getEntityCollection(); 395 if (entities != null) { 396 addItemEntity(entities, dataset, row, column, bar); 397 } 398 } 399 else if (pass == 2) { 400 CategoryItemLabelGenerator generator = getItemLabelGenerator(row, 401 column); 402 if (generator != null && isItemLabelVisible(row, column)) { 403 drawItemLabel(g2, dataset, row, column, plot, generator, bar, 404 (value < 0.0)); 405 } 406 } 407 } 408 409 /** 410 * Tests this renderer for equality with an arbitrary object. 411 * 412 * @param obj the object (<code>null</code> permitted). 413 * 414 * @return A boolean. 415 */ 416 @Override 417 public boolean equals(Object obj) { 418 if (obj == this) { 419 return true; 420 } 421 if (!(obj instanceof StackedBarRenderer)) { 422 return false; 423 } 424 StackedBarRenderer that = (StackedBarRenderer) obj; 425 if (this.renderAsPercentages != that.renderAsPercentages) { 426 return false; 427 } 428 return super.equals(obj); 429 } 430 431}