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 * StackedXYBarRenderer.java 029 * ------------------------- 030 * (C) Copyright 2004-2014, by Andreas Schroeder and Contributors. 031 * 032 * Original Author: Andreas Schroeder; 033 * Contributor(s): David Gilbert (for Object Refinery Limited); 034 * 035 * Changes 036 * ------- 037 * 01-Apr-2004 : Version 1 (AS); 038 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 039 * getYValue() (DG); 040 * 15-Aug-2004 : Added drawBarOutline to control draw/don't-draw bar 041 * outlines (BN); 042 * 10-Sep-2004 : drawBarOutline attribute is now inherited from XYBarRenderer 043 * and double primitives are retrieved from the dataset rather 044 * than Number objects (DG); 045 * 07-Jan-2005 : Updated for method name change in DatasetUtilities (DG); 046 * 25-Jan-2005 : Modified to handle negative values correctly (DG); 047 * ------------- JFREECHART 1.0.x --------------------------------------------- 048 * 06-Dec-2006 : Added support for GradientPaint (DG); 049 * 15-Mar-2007 : Added renderAsPercentages option (DG); 050 * 24-Jun-2008 : Added new barPainter mechanism (DG); 051 * 23-Sep-2008 : Check shadow visibility before drawing shadow (DG); 052 * 28-May-2009 : Fixed bar positioning with inverted domain axis (DG); 053 * 07-Act-2011 : Fix for Bug #3035289: Patch #3035325 (MH); 054 */ 055 056package org.jfree.chart.renderer.xy; 057 058import java.awt.Graphics2D; 059import java.awt.geom.Rectangle2D; 060 061import org.jfree.chart.axis.ValueAxis; 062import org.jfree.chart.entity.EntityCollection; 063import org.jfree.chart.event.RendererChangeEvent; 064import org.jfree.chart.labels.ItemLabelAnchor; 065import org.jfree.chart.labels.ItemLabelPosition; 066import org.jfree.chart.labels.XYItemLabelGenerator; 067import org.jfree.chart.plot.CrosshairState; 068import org.jfree.chart.plot.PlotOrientation; 069import org.jfree.chart.plot.PlotRenderingInfo; 070import org.jfree.chart.plot.XYPlot; 071import org.jfree.data.Range; 072import org.jfree.data.general.DatasetUtilities; 073import org.jfree.data.xy.IntervalXYDataset; 074import org.jfree.data.xy.TableXYDataset; 075import org.jfree.data.xy.XYDataset; 076import org.jfree.ui.RectangleEdge; 077import org.jfree.ui.TextAnchor; 078 079/** 080 * A bar renderer that displays the series items stacked. 081 * The dataset used together with this renderer must be a 082 * {@link org.jfree.data.xy.IntervalXYDataset} and a 083 * {@link org.jfree.data.xy.TableXYDataset}. For example, the 084 * dataset class {@link org.jfree.data.xy.CategoryTableXYDataset} 085 * implements both interfaces. 086 * 087 * The example shown here is generated by the 088 * <code>StackedXYBarChartDemo2.java</code> program included in the 089 * JFreeChart demo collection: 090 * <br><br> 091 * <img src="../../../../../images/StackedXYBarRendererSample.png" 092 * alt="StackedXYBarRendererSample.png"> 093 094 */ 095public class StackedXYBarRenderer extends XYBarRenderer { 096 097 /** For serialization. */ 098 private static final long serialVersionUID = -7049101055533436444L; 099 100 /** A flag that controls whether the bars display values or percentages. */ 101 private boolean renderAsPercentages; 102 103 /** 104 * Creates a new renderer. 105 */ 106 public StackedXYBarRenderer() { 107 this(0.0); 108 } 109 110 /** 111 * Creates a new renderer. 112 * 113 * @param margin the percentual amount of the bars that are cut away. 114 */ 115 public StackedXYBarRenderer(double margin) { 116 super(margin); 117 this.renderAsPercentages = false; 118 119 // set the default item label positions, which will only be used if 120 // the user requests visible item labels... 121 ItemLabelPosition p = new ItemLabelPosition(ItemLabelAnchor.CENTER, 122 TextAnchor.CENTER); 123 setBasePositiveItemLabelPosition(p); 124 setBaseNegativeItemLabelPosition(p); 125 setPositiveItemLabelPositionFallback(null); 126 setNegativeItemLabelPositionFallback(null); 127 } 128 129 /** 130 * Returns <code>true</code> if the renderer displays each item value as 131 * a percentage (so that the stacked bars add to 100%), and 132 * <code>false</code> otherwise. 133 * 134 * @return A boolean. 135 * 136 * @see #setRenderAsPercentages(boolean) 137 * 138 * @since 1.0.5 139 */ 140 public boolean getRenderAsPercentages() { 141 return this.renderAsPercentages; 142 } 143 144 /** 145 * Sets the flag that controls whether the renderer displays each item 146 * value as a percentage (so that the stacked bars add to 100%), and sends 147 * a {@link RendererChangeEvent} to all registered listeners. 148 * 149 * @param asPercentages the flag. 150 * 151 * @see #getRenderAsPercentages() 152 * 153 * @since 1.0.5 154 */ 155 public void setRenderAsPercentages(boolean asPercentages) { 156 this.renderAsPercentages = asPercentages; 157 fireChangeEvent(); 158 } 159 160 /** 161 * Returns <code>3</code> to indicate that this renderer requires three 162 * passes for drawing (shadows are drawn in the first pass, the bars in the 163 * second, and item labels are drawn in the third pass so that 164 * they always appear in front of all the bars). 165 * 166 * @return <code>2</code>. 167 */ 168 @Override 169 public int getPassCount() { 170 return 3; 171 } 172 173 /** 174 * Initialises the renderer and returns a state object that should be 175 * passed to all subsequent calls to the drawItem() method. Here there is 176 * nothing to do. 177 * 178 * @param g2 the graphics device. 179 * @param dataArea the area inside the axes. 180 * @param plot the plot. 181 * @param data the data. 182 * @param info an optional info collection object to return data back to 183 * the caller. 184 * 185 * @return A state object. 186 */ 187 @Override 188 public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea, 189 XYPlot plot, XYDataset data, PlotRenderingInfo info) { 190 return new XYBarRendererState(info); 191 } 192 193 /** 194 * Returns the range of values the renderer requires to display all the 195 * items from the specified dataset. 196 * 197 * @param dataset the dataset (<code>null</code> permitted). 198 * 199 * @return The range (<code>null</code> if the dataset is <code>null</code> 200 * or empty). 201 */ 202 @Override 203 public Range findRangeBounds(XYDataset dataset) { 204 if (dataset != null) { 205 if (this.renderAsPercentages) { 206 return new Range(0.0, 1.0); 207 } 208 else { 209 return DatasetUtilities.findStackedRangeBounds( 210 (TableXYDataset) dataset); 211 } 212 } 213 else { 214 return null; 215 } 216 } 217 218 /** 219 * Draws the visual representation of a single data item. 220 * 221 * @param g2 the graphics device. 222 * @param state the renderer state. 223 * @param dataArea the area within which the plot is being drawn. 224 * @param info collects information about the drawing. 225 * @param plot the plot (can be used to obtain standard color information 226 * etc). 227 * @param domainAxis the domain axis. 228 * @param rangeAxis the range axis. 229 * @param dataset the dataset. 230 * @param series the series index (zero-based). 231 * @param item the item index (zero-based). 232 * @param crosshairState crosshair information for the plot 233 * (<code>null</code> permitted). 234 * @param pass the pass index. 235 */ 236 @Override 237 public void drawItem(Graphics2D g2, XYItemRendererState state, 238 Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, 239 ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, 240 int series, int item, CrosshairState crosshairState, int pass) { 241 242 if (!getItemVisible(series, item)) { 243 return; 244 } 245 246 if (!(dataset instanceof IntervalXYDataset 247 && dataset instanceof TableXYDataset)) { 248 String message = "dataset (type " + dataset.getClass().getName() 249 + ") has wrong type:"; 250 boolean and = false; 251 if (!IntervalXYDataset.class.isAssignableFrom(dataset.getClass())) { 252 message += " it is no IntervalXYDataset"; 253 and = true; 254 } 255 if (!TableXYDataset.class.isAssignableFrom(dataset.getClass())) { 256 if (and) { 257 message += " and"; 258 } 259 message += " it is no TableXYDataset"; 260 } 261 262 throw new IllegalArgumentException(message); 263 } 264 265 IntervalXYDataset intervalDataset = (IntervalXYDataset) dataset; 266 double value = intervalDataset.getYValue(series, item); 267 if (Double.isNaN(value)) { 268 return; 269 } 270 271 // if we are rendering the values as percentages, we need to calculate 272 // the total for the current item. Unfortunately here we end up 273 // repeating the calculation more times than is strictly necessary - 274 // hopefully I'll come back to this and find a way to add the 275 // total(s) to the renderer state. The other problem is we implicitly 276 // assume the dataset has no negative values...perhaps that can be 277 // fixed too. 278 double total = 0.0; 279 if (this.renderAsPercentages) { 280 total = DatasetUtilities.calculateStackTotal( 281 (TableXYDataset) dataset, item); 282 value = value / total; 283 } 284 285 double positiveBase = 0.0; 286 double negativeBase = 0.0; 287 288 for (int i = 0; i < series; i++) { 289 double v = dataset.getYValue(i, item); 290 if (!Double.isNaN(v) && isSeriesVisible(i)) { 291 if (this.renderAsPercentages) { 292 v = v / total; 293 } 294 if (v > 0) { 295 positiveBase = positiveBase + v; 296 } 297 else { 298 negativeBase = negativeBase + v; 299 } 300 } 301 } 302 303 double translatedBase; 304 double translatedValue; 305 RectangleEdge edgeR = plot.getRangeAxisEdge(); 306 if (value > 0.0) { 307 translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea, 308 edgeR); 309 translatedValue = rangeAxis.valueToJava2D(positiveBase + value, 310 dataArea, edgeR); 311 } 312 else { 313 translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea, 314 edgeR); 315 translatedValue = rangeAxis.valueToJava2D(negativeBase + value, 316 dataArea, edgeR); 317 } 318 319 RectangleEdge edgeD = plot.getDomainAxisEdge(); 320 double startX = intervalDataset.getStartXValue(series, item); 321 if (Double.isNaN(startX)) { 322 return; 323 } 324 double translatedStartX = domainAxis.valueToJava2D(startX, dataArea, 325 edgeD); 326 327 double endX = intervalDataset.getEndXValue(series, item); 328 if (Double.isNaN(endX)) { 329 return; 330 } 331 double translatedEndX = domainAxis.valueToJava2D(endX, dataArea, edgeD); 332 333 double translatedWidth = Math.max(1, Math.abs(translatedEndX 334 - translatedStartX)); 335 double translatedHeight = Math.abs(translatedValue - translatedBase); 336 if (getMargin() > 0.0) { 337 double cut = translatedWidth * getMargin(); 338 translatedWidth = translatedWidth - cut; 339 translatedStartX = translatedStartX + cut / 2; 340 } 341 342 Rectangle2D bar = null; 343 PlotOrientation orientation = plot.getOrientation(); 344 if (orientation == PlotOrientation.HORIZONTAL) { 345 bar = new Rectangle2D.Double(Math.min(translatedBase, 346 translatedValue), Math.min(translatedEndX, 347 translatedStartX), translatedHeight, translatedWidth); 348 } 349 else if (orientation == PlotOrientation.VERTICAL) { 350 bar = new Rectangle2D.Double(Math.min(translatedStartX, 351 translatedEndX), Math.min(translatedBase, translatedValue), 352 translatedWidth, translatedHeight); 353 } else { 354 throw new IllegalStateException(); 355 } 356 boolean positive = (value > 0.0); 357 boolean inverted = rangeAxis.isInverted(); 358 RectangleEdge barBase; 359 if (orientation == PlotOrientation.HORIZONTAL) { 360 if (positive && inverted || !positive && !inverted) { 361 barBase = RectangleEdge.RIGHT; 362 } 363 else { 364 barBase = RectangleEdge.LEFT; 365 } 366 } 367 else { 368 if (positive && !inverted || !positive && inverted) { 369 barBase = RectangleEdge.BOTTOM; 370 } 371 else { 372 barBase = RectangleEdge.TOP; 373 } 374 } 375 376 if (pass == 0) { 377 if (getShadowsVisible()) { 378 getBarPainter().paintBarShadow(g2, this, series, item, bar, 379 barBase, false); 380 } 381 } 382 else if (pass == 1) { 383 getBarPainter().paintBar(g2, this, series, item, bar, barBase); 384 385 // add an entity for the item... 386 if (info != null) { 387 EntityCollection entities = info.getOwner() 388 .getEntityCollection(); 389 if (entities != null) { 390 addEntity(entities, bar, dataset, series, item, 391 bar.getCenterX(), bar.getCenterY()); 392 } 393 } 394 } 395 else if (pass == 2) { 396 // handle item label drawing, now that we know all the bars have 397 // been drawn... 398 if (isItemLabelVisible(series, item)) { 399 XYItemLabelGenerator generator = getItemLabelGenerator(series, 400 item); 401 drawItemLabel(g2, dataset, series, item, plot, generator, bar, 402 value < 0.0); 403 } 404 } 405 406 } 407 408 /** 409 * Tests this renderer for equality with an arbitrary object. 410 * 411 * @param obj the object (<code>null</code> permitted). 412 * 413 * @return A boolean. 414 */ 415 @Override 416 public boolean equals(Object obj) { 417 if (obj == this) { 418 return true; 419 } 420 if (!(obj instanceof StackedXYBarRenderer)) { 421 return false; 422 } 423 StackedXYBarRenderer that = (StackedXYBarRenderer) obj; 424 if (this.renderAsPercentages != that.renderAsPercentages) { 425 return false; 426 } 427 return super.equals(obj); 428 } 429 430 /** 431 * Returns a hash code for this instance. 432 * 433 * @return A hash code. 434 */ 435 @Override 436 public int hashCode() { 437 int result = super.hashCode(); 438 result = result * 37 + (this.renderAsPercentages ? 1 : 0); 439 return result; 440 } 441 442}