001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-present, by David Gilbert 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-present, by David Gilbert and Contributors. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): Richard Atkinson; 034 * Thierry Saura; 035 * Christian W. Zuckschwerdt; 036 * Peter Kolb (patch 2511330); 037 * 038 */ 039 040package org.jfree.chart.renderer.category; 041 042import java.awt.Graphics2D; 043import java.awt.geom.Rectangle2D; 044import java.io.Serializable; 045 046import org.jfree.chart.axis.CategoryAxis; 047import org.jfree.chart.axis.ValueAxis; 048import org.jfree.chart.entity.EntityCollection; 049import org.jfree.chart.event.RendererChangeEvent; 050import org.jfree.chart.labels.CategoryItemLabelGenerator; 051import org.jfree.chart.labels.ItemLabelAnchor; 052import org.jfree.chart.labels.ItemLabelPosition; 053import org.jfree.chart.plot.CategoryPlot; 054import org.jfree.chart.plot.PlotOrientation; 055import org.jfree.chart.ui.RectangleEdge; 056import org.jfree.chart.ui.TextAnchor; 057import org.jfree.chart.util.PublicCloneable; 058import org.jfree.data.DataUtils; 059import org.jfree.data.Range; 060import org.jfree.data.category.CategoryDataset; 061import org.jfree.data.general.DatasetUtils; 062 063/** 064 * A stacked bar renderer for use with the {@link CategoryPlot} class. 065 * The example shown here is generated by the 066 * {@code StackedBarChartDemo1.java} program included in the 067 * JFreeChart Demo Collection: 068 * <br><br> 069 * <img src="doc-files/StackedBarRendererSample.png" 070 * alt="StackedBarRendererSample.png"> 071 */ 072public class StackedBarRenderer extends BarRenderer 073 implements Cloneable, PublicCloneable, Serializable { 074 075 /** For serialization. */ 076 static final long serialVersionUID = 6402943811500067531L; 077 078 /** A flag that controls whether the bars display values or percentages. */ 079 private boolean renderAsPercentages; 080 081 /** 082 * Creates a new renderer. By default, the renderer has no tool tip 083 * generator and no URL generator. These defaults have been chosen to 084 * minimise the processing required to generate a default chart. If you 085 * require tool tips or URLs, then you can easily add the required 086 * generators. 087 */ 088 public StackedBarRenderer() { 089 this(false); 090 } 091 092 /** 093 * Creates a new renderer. 094 * 095 * @param renderAsPercentages a flag that controls whether the data values 096 * are rendered as percentages. 097 */ 098 public StackedBarRenderer(boolean renderAsPercentages) { 099 super(); 100 this.renderAsPercentages = renderAsPercentages; 101 102 // set the default item label positions, which will only be used if 103 // the user requests visible item labels... 104 ItemLabelPosition p = new ItemLabelPosition(ItemLabelAnchor.CENTER, 105 TextAnchor.CENTER); 106 setDefaultPositiveItemLabelPosition(p); 107 setDefaultNegativeItemLabelPosition(p); 108 setPositiveItemLabelPositionFallback(null); 109 setNegativeItemLabelPositionFallback(null); 110 } 111 112 /** 113 * Returns {@code true} if the renderer displays each item value as 114 * a percentage (so that the stacked bars add to 100%), and 115 * {@code false} otherwise. 116 * 117 * @return A boolean. 118 * 119 * @see #setRenderAsPercentages(boolean) 120 */ 121 public boolean getRenderAsPercentages() { 122 return this.renderAsPercentages; 123 } 124 125 /** 126 * Sets the flag that controls whether the renderer displays each item 127 * value as a percentage (so that the stacked bars add to 100%), and sends 128 * a {@link RendererChangeEvent} to all registered listeners. 129 * 130 * @param asPercentages the flag. 131 * 132 * @see #getRenderAsPercentages() 133 */ 134 public void setRenderAsPercentages(boolean asPercentages) { 135 this.renderAsPercentages = asPercentages; 136 fireChangeEvent(); 137 } 138 139 /** 140 * Returns the number of passes ({@code 3}) required by this renderer. 141 * The first pass is used to draw the bar shadows, the second pass is used 142 * to draw the bars, and the third pass is used to draw the item labels 143 * (if visible). 144 * 145 * @return The number of passes required by the renderer. 146 */ 147 @Override 148 public int getPassCount() { 149 return 3; 150 } 151 152 /** 153 * Returns the range of values the renderer requires to display all the 154 * items from the specified dataset. 155 * 156 * @param dataset the dataset ({@code null} permitted). 157 * 158 * @return The range (or {@code null} if the dataset is empty). 159 */ 160 @Override 161 public Range findRangeBounds(CategoryDataset dataset) { 162 if (dataset == null) { 163 return null; 164 } 165 if (this.renderAsPercentages) { 166 return new Range(0.0, 1.0); 167 } 168 else { 169 return DatasetUtils.findStackedRangeBounds(dataset, getBase()); 170 } 171 } 172 173 /** 174 * Calculates the bar width and stores it in the renderer state. 175 * 176 * @param plot the plot. 177 * @param dataArea the data area. 178 * @param rendererIndex the renderer index. 179 * @param state the renderer state. 180 */ 181 @Override 182 protected void calculateBarWidth(CategoryPlot plot, Rectangle2D dataArea, 183 int rendererIndex, CategoryItemRendererState state) { 184 185 // calculate the bar width 186 CategoryAxis xAxis = plot.getDomainAxisForDataset(rendererIndex); 187 CategoryDataset data = plot.getDataset(rendererIndex); 188 if (data != null) { 189 PlotOrientation orientation = plot.getOrientation(); 190 double space = 0.0; 191 if (orientation == PlotOrientation.HORIZONTAL) { 192 space = dataArea.getHeight(); 193 } 194 else if (orientation == PlotOrientation.VERTICAL) { 195 space = dataArea.getWidth(); 196 } 197 double maxWidth = space * getMaximumBarWidth(); 198 int columns = data.getColumnCount(); 199 double categoryMargin = 0.0; 200 if (columns > 1) { 201 categoryMargin = xAxis.getCategoryMargin(); 202 } 203 204 double used = space * (1 - xAxis.getLowerMargin() 205 - xAxis.getUpperMargin() 206 - categoryMargin); 207 if (columns > 0) { 208 state.setBarWidth(Math.min(used / columns, maxWidth)); 209 } 210 else { 211 state.setBarWidth(Math.min(used, maxWidth)); 212 } 213 } 214 215 } 216 217 /** 218 * Draws a stacked bar for a specific item. 219 * 220 * @param g2 the graphics device. 221 * @param state the renderer state. 222 * @param dataArea the plot area. 223 * @param plot the plot. 224 * @param domainAxis the domain (category) axis. 225 * @param rangeAxis the range (value) axis. 226 * @param dataset the data. 227 * @param row the row index (zero-based). 228 * @param column the column index (zero-based). 229 * @param pass the pass index. 230 */ 231 @Override 232 public void drawItem(Graphics2D g2, CategoryItemRendererState state, 233 Rectangle2D dataArea, CategoryPlot plot, CategoryAxis domainAxis, 234 ValueAxis rangeAxis, CategoryDataset dataset, int row, 235 int column, int pass) { 236 237 if (!isSeriesVisible(row)) { 238 return; 239 } 240 241 // nothing is drawn for null values... 242 Number dataValue = dataset.getValue(row, column); 243 if (dataValue == null) { 244 return; 245 } 246 247 double value = dataValue.doubleValue(); 248 double total = 0.0; // only needed if calculating percentages 249 if (this.renderAsPercentages) { 250 total = DataUtils.calculateColumnTotal(dataset, column, 251 state.getVisibleSeriesArray()); 252 value = value / total; 253 } 254 255 PlotOrientation orientation = plot.getOrientation(); 256 double barW0 = domainAxis.getCategoryMiddle(column, getColumnCount(), 257 dataArea, plot.getDomainAxisEdge()) 258 - state.getBarWidth() / 2.0; 259 260 double positiveBase = getBase(); 261 double negativeBase = positiveBase; 262 263 for (int i = 0; i < row; i++) { 264 Number v = dataset.getValue(i, column); 265 if (v != null && isSeriesVisible(i)) { 266 double d = v.doubleValue(); 267 if (this.renderAsPercentages) { 268 d = d / total; 269 } 270 if (d > 0) { 271 positiveBase = positiveBase + d; 272 } 273 else { 274 negativeBase = negativeBase + d; 275 } 276 } 277 } 278 279 double translatedBase; 280 double translatedValue; 281 boolean positive = (value > 0.0); 282 boolean inverted = rangeAxis.isInverted(); 283 RectangleEdge barBase; 284 if (orientation == PlotOrientation.HORIZONTAL) { 285 if (positive && inverted || !positive && !inverted) { 286 barBase = RectangleEdge.RIGHT; 287 } 288 else { 289 barBase = RectangleEdge.LEFT; 290 } 291 } 292 else { 293 if (positive && !inverted || !positive && inverted) { 294 barBase = RectangleEdge.BOTTOM; 295 } 296 else { 297 barBase = RectangleEdge.TOP; 298 } 299 } 300 301 RectangleEdge location = plot.getRangeAxisEdge(); 302 if (positive) { 303 translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea, 304 location); 305 translatedValue = rangeAxis.valueToJava2D(positiveBase + value, 306 dataArea, location); 307 } 308 else { 309 translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea, 310 location); 311 translatedValue = rangeAxis.valueToJava2D(negativeBase + value, 312 dataArea, location); 313 } 314 double barL0 = Math.min(translatedBase, translatedValue); 315 double barLength = Math.max(Math.abs(translatedValue - translatedBase), 316 getMinimumBarLength()); 317 318 Rectangle2D bar; 319 if (orientation == PlotOrientation.HORIZONTAL) { 320 bar = new Rectangle2D.Double(barL0, barW0, barLength, 321 state.getBarWidth()); 322 } 323 else { 324 bar = new Rectangle2D.Double(barW0, barL0, state.getBarWidth(), 325 barLength); 326 } 327 if (pass == 0) { 328 if (getShadowsVisible()) { 329 boolean pegToBase = (positive && (positiveBase == getBase())) 330 || (!positive && (negativeBase == getBase())); 331 getBarPainter().paintBarShadow(g2, this, row, column, bar, 332 barBase, pegToBase); 333 } 334 } 335 else if (pass == 1) { 336 getBarPainter().paintBar(g2, this, row, column, bar, barBase); 337 338 // add an item entity, if this information is being collected 339 EntityCollection entities = state.getEntityCollection(); 340 if (entities != null) { 341 addItemEntity(entities, dataset, row, column, bar); 342 } 343 } 344 else if (pass == 2) { 345 CategoryItemLabelGenerator generator = getItemLabelGenerator(row, 346 column); 347 if (generator != null && isItemLabelVisible(row, column)) { 348 drawItemLabel(g2, dataset, row, column, plot, generator, bar, 349 (value < 0.0)); 350 } 351 } 352 } 353 354 /** 355 * Tests this renderer for equality with an arbitrary object. 356 * 357 * @param obj the object ({@code null} permitted). 358 * 359 * @return A boolean. 360 */ 361 @Override 362 public boolean equals(Object obj) { 363 if (obj == this) { 364 return true; 365 } 366 if (!(obj instanceof StackedBarRenderer)) { 367 return false; 368 } 369 StackedBarRenderer that = (StackedBarRenderer) obj; 370 if (this.renderAsPercentages != that.renderAsPercentages) { 371 return false; 372 } 373 return super.equals(obj); 374 } 375 376}