001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2020, 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 * [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-2020, by Andreas Schroeder and Contributors. 031 * 032 * Original Author: Andreas Schroeder; 033 * Contributor(s): David Gilbert (for Object Refinery Limited); 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 * @since 1.0.5 119 */ 120 public boolean getRenderAsPercentages() { 121 return this.renderAsPercentages; 122 } 123 124 /** 125 * Sets the flag that controls whether the renderer displays each item 126 * value as a percentage (so that the stacked bars add to 100%), and sends 127 * a {@link RendererChangeEvent} to all registered listeners. 128 * 129 * @param asPercentages the flag. 130 * 131 * @see #getRenderAsPercentages() 132 * 133 * @since 1.0.5 134 */ 135 public void setRenderAsPercentages(boolean asPercentages) { 136 this.renderAsPercentages = asPercentages; 137 fireChangeEvent(); 138 } 139 140 /** 141 * Returns {@code 3} to indicate that this renderer requires three 142 * passes for drawing (shadows are drawn in the first pass, the bars in the 143 * second, and item labels are drawn in the third pass so that 144 * they always appear in front of all the bars). 145 * 146 * @return {@code 2}. 147 */ 148 @Override 149 public int getPassCount() { 150 return 3; 151 } 152 153 /** 154 * Initialises the renderer and returns a state object that should be 155 * passed to all subsequent calls to the drawItem() method. Here there is 156 * nothing to do. 157 * 158 * @param g2 the graphics device. 159 * @param dataArea the area inside the axes. 160 * @param plot the plot. 161 * @param data the data. 162 * @param info an optional info collection object to return data back to 163 * the caller. 164 * 165 * @return A state object. 166 */ 167 @Override 168 public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea, 169 XYPlot plot, XYDataset data, PlotRenderingInfo info) { 170 return new XYBarRendererState(info); 171 } 172 173 /** 174 * Returns the range of values the renderer requires to display all the 175 * items from the specified dataset. 176 * 177 * @param dataset the dataset ({@code null} permitted). 178 * 179 * @return The range ({@code null} if the dataset is {@code null} 180 * or empty). 181 */ 182 @Override 183 public Range findRangeBounds(XYDataset dataset) { 184 if (dataset != null) { 185 if (this.renderAsPercentages) { 186 return new Range(0.0, 1.0); 187 } 188 else { 189 return DatasetUtils.findStackedRangeBounds( 190 (TableXYDataset) dataset); 191 } 192 } 193 else { 194 return null; 195 } 196 } 197 198 /** 199 * Draws the visual representation of a single data item. 200 * 201 * @param g2 the graphics device. 202 * @param state the renderer state. 203 * @param dataArea the area within which the plot is being drawn. 204 * @param info collects information about the drawing. 205 * @param plot the plot (can be used to obtain standard color information 206 * etc). 207 * @param domainAxis the domain axis. 208 * @param rangeAxis the range axis. 209 * @param dataset the dataset. 210 * @param series the series index (zero-based). 211 * @param item the item index (zero-based). 212 * @param crosshairState crosshair information for the plot 213 * ({@code null} permitted). 214 * @param pass the pass index. 215 */ 216 @Override 217 public void drawItem(Graphics2D g2, XYItemRendererState state, 218 Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, 219 ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, 220 int series, int item, CrosshairState crosshairState, int pass) { 221 222 if (!getItemVisible(series, item)) { 223 return; 224 } 225 226 if (!(dataset instanceof IntervalXYDataset 227 && dataset instanceof TableXYDataset)) { 228 String message = "dataset (type " + dataset.getClass().getName() 229 + ") has wrong type:"; 230 boolean and = false; 231 if (!IntervalXYDataset.class.isAssignableFrom(dataset.getClass())) { 232 message += " it is no IntervalXYDataset"; 233 and = true; 234 } 235 if (!TableXYDataset.class.isAssignableFrom(dataset.getClass())) { 236 if (and) { 237 message += " and"; 238 } 239 message += " it is no TableXYDataset"; 240 } 241 242 throw new IllegalArgumentException(message); 243 } 244 245 IntervalXYDataset intervalDataset = (IntervalXYDataset) dataset; 246 double value = intervalDataset.getYValue(series, item); 247 if (Double.isNaN(value)) { 248 return; 249 } 250 251 // if we are rendering the values as percentages, we need to calculate 252 // the total for the current item. Unfortunately here we end up 253 // repeating the calculation more times than is strictly necessary - 254 // hopefully I'll come back to this and find a way to add the 255 // total(s) to the renderer state. The other problem is we implicitly 256 // assume the dataset has no negative values...perhaps that can be 257 // fixed too. 258 double total = 0.0; 259 if (this.renderAsPercentages) { 260 total = DatasetUtils.calculateStackTotal( 261 (TableXYDataset) dataset, item); 262 value = value / total; 263 } 264 265 double positiveBase = 0.0; 266 double negativeBase = 0.0; 267 268 for (int i = 0; i < series; i++) { 269 double v = dataset.getYValue(i, item); 270 if (!Double.isNaN(v) && isSeriesVisible(i)) { 271 if (this.renderAsPercentages) { 272 v = v / total; 273 } 274 if (v > 0) { 275 positiveBase = positiveBase + v; 276 } 277 else { 278 negativeBase = negativeBase + v; 279 } 280 } 281 } 282 283 double translatedBase; 284 double translatedValue; 285 RectangleEdge edgeR = plot.getRangeAxisEdge(); 286 if (value > 0.0) { 287 translatedBase = rangeAxis.valueToJava2D(positiveBase, dataArea, 288 edgeR); 289 translatedValue = rangeAxis.valueToJava2D(positiveBase + value, 290 dataArea, edgeR); 291 } 292 else { 293 translatedBase = rangeAxis.valueToJava2D(negativeBase, dataArea, 294 edgeR); 295 translatedValue = rangeAxis.valueToJava2D(negativeBase + value, 296 dataArea, edgeR); 297 } 298 299 RectangleEdge edgeD = plot.getDomainAxisEdge(); 300 double startX = intervalDataset.getStartXValue(series, item); 301 if (Double.isNaN(startX)) { 302 return; 303 } 304 double translatedStartX = domainAxis.valueToJava2D(startX, dataArea, 305 edgeD); 306 307 double endX = intervalDataset.getEndXValue(series, item); 308 if (Double.isNaN(endX)) { 309 return; 310 } 311 double translatedEndX = domainAxis.valueToJava2D(endX, dataArea, edgeD); 312 313 double translatedWidth = Math.max(1, Math.abs(translatedEndX 314 - translatedStartX)); 315 double translatedHeight = Math.abs(translatedValue - translatedBase); 316 if (getMargin() > 0.0) { 317 double cut = translatedWidth * getMargin(); 318 translatedWidth = translatedWidth - cut; 319 translatedStartX = translatedStartX + cut / 2; 320 } 321 322 Rectangle2D bar = null; 323 PlotOrientation orientation = plot.getOrientation(); 324 if (orientation == PlotOrientation.HORIZONTAL) { 325 bar = new Rectangle2D.Double(Math.min(translatedBase, 326 translatedValue), Math.min(translatedEndX, 327 translatedStartX), translatedHeight, translatedWidth); 328 } 329 else if (orientation == PlotOrientation.VERTICAL) { 330 bar = new Rectangle2D.Double(Math.min(translatedStartX, 331 translatedEndX), Math.min(translatedBase, translatedValue), 332 translatedWidth, translatedHeight); 333 } else { 334 throw new IllegalStateException(); 335 } 336 boolean positive = (value > 0.0); 337 boolean inverted = rangeAxis.isInverted(); 338 RectangleEdge barBase; 339 if (orientation == PlotOrientation.HORIZONTAL) { 340 if (positive && inverted || !positive && !inverted) { 341 barBase = RectangleEdge.RIGHT; 342 } 343 else { 344 barBase = RectangleEdge.LEFT; 345 } 346 } 347 else { 348 if (positive && !inverted || !positive && inverted) { 349 barBase = RectangleEdge.BOTTOM; 350 } 351 else { 352 barBase = RectangleEdge.TOP; 353 } 354 } 355 356 if (pass == 0) { 357 if (getShadowsVisible()) { 358 getBarPainter().paintBarShadow(g2, this, series, item, bar, 359 barBase, false); 360 } 361 } 362 else if (pass == 1) { 363 getBarPainter().paintBar(g2, this, series, item, bar, barBase); 364 365 // add an entity for the item... 366 if (info != null) { 367 EntityCollection entities = info.getOwner() 368 .getEntityCollection(); 369 if (entities != null) { 370 addEntity(entities, bar, dataset, series, item, 371 bar.getCenterX(), bar.getCenterY()); 372 } 373 } 374 } 375 else if (pass == 2) { 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} permitted). 392 * 393 * @return A boolean. 394 */ 395 @Override 396 public boolean equals(Object obj) { 397 if (obj == this) { 398 return true; 399 } 400 if (!(obj instanceof StackedXYBarRenderer)) { 401 return false; 402 } 403 StackedXYBarRenderer that = (StackedXYBarRenderer) obj; 404 if (this.renderAsPercentages != that.renderAsPercentages) { 405 return false; 406 } 407 return super.equals(obj); 408 } 409 410 /** 411 * Returns a hash code for this instance. 412 * 413 * @return A hash code. 414 */ 415 @Override 416 public int hashCode() { 417 int result = super.hashCode(); 418 result = result * 37 + (this.renderAsPercentages ? 1 : 0); 419 return result; 420 } 421 422}