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