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