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