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 * StackedXYBarRenderer.java 029 * ------------------------- 030 * (C) Copyright 2004-2008, 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 * 053 */ 054 055 package org.jfree.chart.renderer.xy; 056 057 import java.awt.Graphics2D; 058 import java.awt.geom.Rectangle2D; 059 060 import org.jfree.chart.axis.ValueAxis; 061 import org.jfree.chart.entity.EntityCollection; 062 import org.jfree.chart.event.RendererChangeEvent; 063 import org.jfree.chart.labels.ItemLabelAnchor; 064 import org.jfree.chart.labels.ItemLabelPosition; 065 import org.jfree.chart.labels.XYItemLabelGenerator; 066 import org.jfree.chart.plot.CrosshairState; 067 import org.jfree.chart.plot.PlotOrientation; 068 import org.jfree.chart.plot.PlotRenderingInfo; 069 import org.jfree.chart.plot.XYPlot; 070 import org.jfree.data.Range; 071 import org.jfree.data.general.DatasetUtilities; 072 import org.jfree.data.xy.IntervalXYDataset; 073 import org.jfree.data.xy.TableXYDataset; 074 import org.jfree.data.xy.XYDataset; 075 import org.jfree.ui.RectangleEdge; 076 import org.jfree.ui.TextAnchor; 077 078 /** 079 * A bar renderer that displays the series items stacked. 080 * The dataset used together with this renderer must be a 081 * {@link org.jfree.data.xy.IntervalXYDataset} and a 082 * {@link org.jfree.data.xy.TableXYDataset}. For example, the 083 * dataset class {@link org.jfree.data.xy.CategoryTableXYDataset} 084 * implements both interfaces. 085 * 086 * The example shown here is generated by the 087 * <code>StackedXYBarChartDemo2.java</code> program included in the 088 * JFreeChart demo collection: 089 * <br><br> 090 * <img src="../../../../../images/StackedXYBarRendererSample.png" 091 * alt="StackedXYBarRendererSample.png" /> 092 093 */ 094 public class StackedXYBarRenderer extends XYBarRenderer { 095 096 /** For serialization. */ 097 private static final long serialVersionUID = -7049101055533436444L; 098 099 /** A flag that controls whether the bars display values or percentages. */ 100 private boolean renderAsPercentages; 101 102 /** 103 * Creates a new renderer. 104 */ 105 public StackedXYBarRenderer() { 106 this(0.0); 107 } 108 109 /** 110 * Creates a new renderer. 111 * 112 * @param margin the percentual amount of the bars that are cut away. 113 */ 114 public StackedXYBarRenderer(double margin) { 115 super(margin); 116 this.renderAsPercentages = false; 117 118 // set the default item label positions, which will only be used if 119 // the user requests visible item labels... 120 ItemLabelPosition p = new ItemLabelPosition(ItemLabelAnchor.CENTER, 121 TextAnchor.CENTER); 122 setBasePositiveItemLabelPosition(p); 123 setBaseNegativeItemLabelPosition(p); 124 setPositiveItemLabelPositionFallback(null); 125 setNegativeItemLabelPositionFallback(null); 126 } 127 128 /** 129 * Returns <code>true</code> if the renderer displays each item value as 130 * a percentage (so that the stacked bars add to 100%), and 131 * <code>false</code> otherwise. 132 * 133 * @return A boolean. 134 * 135 * @see #setRenderAsPercentages(boolean) 136 * 137 * @since 1.0.5 138 */ 139 public boolean getRenderAsPercentages() { 140 return this.renderAsPercentages; 141 } 142 143 /** 144 * Sets the flag that controls whether the renderer displays each item 145 * value as a percentage (so that the stacked bars add to 100%), and sends 146 * a {@link RendererChangeEvent} to all registered listeners. 147 * 148 * @param asPercentages the flag. 149 * 150 * @see #getRenderAsPercentages() 151 * 152 * @since 1.0.5 153 */ 154 public void setRenderAsPercentages(boolean asPercentages) { 155 this.renderAsPercentages = asPercentages; 156 fireChangeEvent(); 157 } 158 159 /** 160 * Returns <code>3</code> to indicate that this renderer requires three 161 * passes for drawing (shadows are drawn in the first pass, the bars in the 162 * second, and item labels are drawn in the third pass so that 163 * they always appear in front of all the bars). 164 * 165 * @return <code>2</code>. 166 */ 167 public int getPassCount() { 168 return 3; 169 } 170 171 /** 172 * Initialises the renderer and returns a state object that should be 173 * passed to all subsequent calls to the drawItem() method. Here there is 174 * nothing to do. 175 * 176 * @param g2 the graphics device. 177 * @param dataArea the area inside the axes. 178 * @param plot the plot. 179 * @param data the data. 180 * @param info an optional info collection object to return data back to 181 * the caller. 182 * 183 * @return A state object. 184 */ 185 public XYItemRendererState initialise(Graphics2D g2, 186 Rectangle2D dataArea, 187 XYPlot plot, 188 XYDataset data, 189 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 public Range findRangeBounds(XYDataset dataset) { 203 if (dataset != null) { 204 if (this.renderAsPercentages) { 205 return new Range(0.0, 1.0); 206 } 207 else { 208 return DatasetUtilities.findStackedRangeBounds( 209 (TableXYDataset) dataset); 210 } 211 } 212 else { 213 return null; 214 } 215 } 216 217 /** 218 * Draws the visual representation of a single data item. 219 * 220 * @param g2 the graphics device. 221 * @param state the renderer state. 222 * @param dataArea the area within which the plot is being drawn. 223 * @param info collects information about the drawing. 224 * @param plot the plot (can be used to obtain standard color information 225 * etc). 226 * @param domainAxis the domain axis. 227 * @param rangeAxis the range axis. 228 * @param dataset the dataset. 229 * @param series the series index (zero-based). 230 * @param item the item index (zero-based). 231 * @param crosshairState crosshair information for the plot 232 * (<code>null</code> permitted). 233 * @param pass the pass index. 234 */ 235 public void drawItem(Graphics2D g2, 236 XYItemRendererState state, 237 Rectangle2D dataArea, 238 PlotRenderingInfo info, 239 XYPlot plot, 240 ValueAxis domainAxis, 241 ValueAxis rangeAxis, 242 XYDataset dataset, 243 int series, 244 int item, 245 CrosshairState crosshairState, 246 int pass) { 247 248 if (!(dataset instanceof IntervalXYDataset 249 && dataset instanceof TableXYDataset)) { 250 String message = "dataset (type " + dataset.getClass().getName() 251 + ") has wrong type:"; 252 boolean and = false; 253 if (!IntervalXYDataset.class.isAssignableFrom(dataset.getClass())) { 254 message += " it is no IntervalXYDataset"; 255 and = true; 256 } 257 if (!TableXYDataset.class.isAssignableFrom(dataset.getClass())) { 258 if (and) { 259 message += " and"; 260 } 261 message += " it is no TableXYDataset"; 262 } 263 264 throw new IllegalArgumentException(message); 265 } 266 267 IntervalXYDataset intervalDataset = (IntervalXYDataset) dataset; 268 double value = intervalDataset.getYValue(series, item); 269 if (Double.isNaN(value)) { 270 return; 271 } 272 273 // if we are rendering the values as percentages, we need to calculate 274 // the total for the current item. Unfortunately here we end up 275 // repeating the calculation more times than is strictly necessary - 276 // hopefully I'll come back to this and find a way to add the 277 // total(s) to the renderer state. The other problem is we implicitly 278 // assume the dataset has no negative values...perhaps that can be 279 // fixed too. 280 double total = 0.0; 281 if (this.renderAsPercentages) { 282 total = DatasetUtilities.calculateStackTotal( 283 (TableXYDataset) dataset, item); 284 value = value / total; 285 } 286 287 double positiveBase = 0.0; 288 double negativeBase = 0.0; 289 290 for (int i = 0; i < series; i++) { 291 double v = dataset.getYValue(i, item); 292 if (!Double.isNaN(v)) { 293 if (this.renderAsPercentages) { 294 v = v / total; 295 } 296 if (v > 0) { 297 positiveBase = positiveBase + v; 298 } 299 else { 300 negativeBase = negativeBase + v; 301 } 302 } 303 } 304 305 double translatedBase; 306 double translatedValue; 307 RectangleEdge edgeR = plot.getRangeAxisEdge(); 308 if (value > 0.0) { 309 translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea, 310 edgeR); 311 translatedValue = rangeAxis.valueToJava2D(positiveBase + value, 312 dataArea, edgeR); 313 } 314 else { 315 translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea, 316 edgeR); 317 translatedValue = rangeAxis.valueToJava2D(negativeBase + value, 318 dataArea, edgeR); 319 } 320 321 RectangleEdge edgeD = plot.getDomainAxisEdge(); 322 double startX = intervalDataset.getStartXValue(series, item); 323 if (Double.isNaN(startX)) { 324 return; 325 } 326 double translatedStartX = domainAxis.valueToJava2D(startX, dataArea, 327 edgeD); 328 329 double endX = intervalDataset.getEndXValue(series, item); 330 if (Double.isNaN(endX)) { 331 return; 332 } 333 double translatedEndX = domainAxis.valueToJava2D(endX, dataArea, edgeD); 334 335 double translatedWidth = Math.max(1, Math.abs(translatedEndX 336 - translatedStartX)); 337 double translatedHeight = Math.abs(translatedValue - translatedBase); 338 if (getMargin() > 0.0) { 339 double cut = translatedWidth * getMargin(); 340 translatedWidth = translatedWidth - cut; 341 translatedStartX = translatedStartX + cut / 2; 342 } 343 344 Rectangle2D bar = null; 345 PlotOrientation orientation = plot.getOrientation(); 346 if (orientation == PlotOrientation.HORIZONTAL) { 347 bar = new Rectangle2D.Double(Math.min(translatedBase, 348 translatedValue), translatedEndX, translatedHeight, 349 translatedWidth); 350 } 351 else if (orientation == PlotOrientation.VERTICAL) { 352 bar = new Rectangle2D.Double(translatedStartX, 353 Math.min(translatedBase, translatedValue), 354 translatedWidth, translatedHeight); 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 public boolean equals(Object obj) { 416 if (obj == this) { 417 return true; 418 } 419 if (!(obj instanceof StackedXYBarRenderer)) { 420 return false; 421 } 422 StackedXYBarRenderer that = (StackedXYBarRenderer) obj; 423 if (this.renderAsPercentages != that.renderAsPercentages) { 424 return false; 425 } 426 return super.equals(obj); 427 } 428 429 /** 430 * Returns a hash code for this instance. 431 * 432 * @return A hash code. 433 */ 434 public int hashCode() { 435 int result = super.hashCode(); 436 result = result * 37 + (this.renderAsPercentages ? 1 : 0); 437 return result; 438 } 439 440 }