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