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 * StackedXYAreaRenderer2.java 029 * --------------------------- 030 * (C) Copyright 2004-2020, by Object Refinery Limited and Contributors. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited), based on 033 * the StackedXYAreaRenderer class by Richard Atkinson; 034 * Contributor(s): Ulrich Voigt (patch #312); 035 * 036 */ 037 038package org.jfree.chart.renderer.xy; 039 040import java.awt.Graphics2D; 041import java.awt.Paint; 042import java.awt.geom.Area; 043import java.awt.geom.GeneralPath; 044import java.awt.geom.Rectangle2D; 045import java.io.Serializable; 046 047import org.jfree.chart.axis.ValueAxis; 048import org.jfree.chart.entity.EntityCollection; 049import org.jfree.chart.event.RendererChangeEvent; 050import org.jfree.chart.labels.XYToolTipGenerator; 051import org.jfree.chart.plot.CrosshairState; 052import org.jfree.chart.plot.PlotOrientation; 053import org.jfree.chart.plot.PlotRenderingInfo; 054import org.jfree.chart.plot.XYPlot; 055import org.jfree.chart.ui.RectangleEdge; 056import org.jfree.chart.urls.XYURLGenerator; 057import org.jfree.chart.util.PublicCloneable; 058import org.jfree.data.Range; 059import org.jfree.data.xy.TableXYDataset; 060import org.jfree.data.xy.XYDataset; 061 062/** 063 * A stacked area renderer for the {@link XYPlot} class. 064 * The example shown here is generated by the 065 * {@code StackedXYAreaChartDemo2.java} program included in the 066 * JFreeChart demo collection: 067 * <br><br> 068 * <img src="doc-files/StackedXYAreaRenderer2Sample.png" 069 * alt="StackedXYAreaRenderer2Sample.png"> 070 */ 071public class StackedXYAreaRenderer2 extends XYAreaRenderer2 072 implements Cloneable, PublicCloneable, Serializable { 073 074 /** For serialization. */ 075 private static final long serialVersionUID = 7752676509764539182L; 076 077 /** 078 * This flag controls whether or not the x-coordinates (in Java2D space) 079 * are rounded to integers. When set to true, this can avoid the vertical 080 * striping that anti-aliasing can generate. However, the rounding may not 081 * be appropriate for output in high resolution formats (for example, 082 * vector graphics formats such as SVG and PDF). 083 * 084 * @since 1.0.3 085 */ 086 private boolean roundXCoordinates; 087 088 /** 089 * Creates a new renderer. 090 */ 091 public StackedXYAreaRenderer2() { 092 this(null, null); 093 } 094 095 /** 096 * Constructs a new renderer. 097 * 098 * @param labelGenerator the tool tip generator to use ({@code null} 099 * permitted). 100 * @param urlGenerator the URL generator ({@code null} permitted). 101 */ 102 public StackedXYAreaRenderer2(XYToolTipGenerator labelGenerator, 103 XYURLGenerator urlGenerator) { 104 super(labelGenerator, urlGenerator); 105 this.roundXCoordinates = true; 106 } 107 108 /** 109 * Returns the flag that controls whether or not the x-coordinates (in 110 * Java2D space) are rounded to integer values. 111 * 112 * @return The flag. 113 * 114 * @since 1.0.4 115 * 116 * @see #setRoundXCoordinates(boolean) 117 */ 118 public boolean getRoundXCoordinates() { 119 return this.roundXCoordinates; 120 } 121 122 /** 123 * Sets the flag that controls whether or not the x-coordinates (in 124 * Java2D space) are rounded to integer values, and sends a 125 * {@link RendererChangeEvent} to all registered listeners. 126 * 127 * @param round the new flag value. 128 * 129 * @since 1.0.4 130 * 131 * @see #getRoundXCoordinates() 132 */ 133 public void setRoundXCoordinates(boolean round) { 134 this.roundXCoordinates = round; 135 fireChangeEvent(); 136 } 137 138 /** 139 * Returns the range of values the renderer requires to display all the 140 * items from the specified dataset. 141 * 142 * @param dataset the dataset ({@code null} permitted). 143 * 144 * @return The range (or {@code null} if the dataset is {@code null} or 145 * empty). 146 */ 147 @Override 148 public Range findRangeBounds(XYDataset dataset) { 149 if (dataset == null) { 150 return null; 151 } 152 double min = Double.POSITIVE_INFINITY; 153 double max = Double.NEGATIVE_INFINITY; 154 TableXYDataset d = (TableXYDataset) dataset; 155 int itemCount = d.getItemCount(); 156 for (int i = 0; i < itemCount; i++) { 157 double[] stackValues = getStackValues((TableXYDataset) dataset, 158 d.getSeriesCount(), i); 159 min = Math.min(min, stackValues[0]); 160 max = Math.max(max, stackValues[1]); 161 } 162 if (min == Double.POSITIVE_INFINITY) { 163 return null; 164 } 165 return new Range(min, max); 166 } 167 168 /** 169 * Returns the number of passes required by the renderer. 170 * 171 * @return 1. 172 */ 173 @Override 174 public int getPassCount() { 175 return 1; 176 } 177 178 /** 179 * Draws the visual representation of a single data item. 180 * 181 * @param g2 the graphics device. 182 * @param state the renderer state. 183 * @param dataArea the area within which the data is being drawn. 184 * @param info collects information about the drawing. 185 * @param plot the plot (can be used to obtain standard color information 186 * etc). 187 * @param domainAxis the domain axis. 188 * @param rangeAxis the range axis. 189 * @param dataset the dataset. 190 * @param series the series index (zero-based). 191 * @param item the item index (zero-based). 192 * @param crosshairState information about crosshairs on a plot. 193 * @param pass the pass index. 194 */ 195 @Override 196 public void drawItem(Graphics2D g2, XYItemRendererState state, 197 Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, 198 ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, 199 int series, int item, CrosshairState crosshairState, int pass) { 200 201 // setup for collecting optional entity info... 202 EntityCollection entities = null; 203 if (info != null) { 204 entities = info.getOwner().getEntityCollection(); 205 } 206 207 TableXYDataset tdataset = (TableXYDataset) dataset; 208 PlotOrientation orientation = plot.getOrientation(); 209 210 // get the data point... 211 double x1 = dataset.getXValue(series, item); 212 double y1 = dataset.getYValue(series, item); 213 if (Double.isNaN(y1)) { 214 y1 = 0.0; 215 } 216 double[] stack1 = getStackValues(tdataset, series, item); 217 218 // get the previous point and the next point so we can calculate a 219 // "hot spot" for the area (used by the chart entity)... 220 double x0 = dataset.getXValue(series, Math.max(item - 1, 0)); 221 double y0 = dataset.getYValue(series, Math.max(item - 1, 0)); 222 if (Double.isNaN(y0)) { 223 y0 = 0.0; 224 } 225 double[] stack0 = getStackValues(tdataset, series, Math.max(item - 1, 226 0)); 227 228 int itemCount = dataset.getItemCount(series); 229 double x2 = dataset.getXValue(series, Math.min(item + 1, 230 itemCount - 1)); 231 double y2 = dataset.getYValue(series, Math.min(item + 1, 232 itemCount - 1)); 233 if (Double.isNaN(y2)) { 234 y2 = 0.0; 235 } 236 double[] stack2 = getStackValues(tdataset, series, Math.min(item + 1, 237 itemCount - 1)); 238 239 double xleft = (x0 + x1) / 2.0; 240 double xright = (x1 + x2) / 2.0; 241 double[] stackLeft = averageStackValues(stack0, stack1); 242 double[] stackRight = averageStackValues(stack1, stack2); 243 double[] adjStackLeft = adjustedStackValues(stack0, stack1); 244 double[] adjStackRight = adjustedStackValues(stack1, stack2); 245 246 RectangleEdge edge0 = plot.getDomainAxisEdge(); 247 248 float transX1 = (float) domainAxis.valueToJava2D(x1, dataArea, edge0); 249 float transXLeft = (float) domainAxis.valueToJava2D(xleft, dataArea, 250 edge0); 251 float transXRight = (float) domainAxis.valueToJava2D(xright, dataArea, 252 edge0); 253 254 if (this.roundXCoordinates) { 255 transX1 = Math.round(transX1); 256 transXLeft = Math.round(transXLeft); 257 transXRight = Math.round(transXRight); 258 } 259 float transY1; 260 261 RectangleEdge edge1 = plot.getRangeAxisEdge(); 262 263 GeneralPath left = new GeneralPath(); 264 GeneralPath right = new GeneralPath(); 265 if (y1 >= 0.0) { // handle positive value 266 transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1], dataArea, 267 edge1); 268 float transStack1 = (float) rangeAxis.valueToJava2D(stack1[1], 269 dataArea, edge1); 270 float transStackLeft = (float) rangeAxis.valueToJava2D( 271 adjStackLeft[1], dataArea, edge1); 272 273 // LEFT POLYGON 274 if (y0 >= 0.0) { 275 double yleft = (y0 + y1) / 2.0 + stackLeft[1]; 276 float transYLeft 277 = (float) rangeAxis.valueToJava2D(yleft, dataArea, edge1); 278 if (orientation == PlotOrientation.VERTICAL) { 279 left.moveTo(transX1, transY1); 280 left.lineTo(transX1, transStack1); 281 left.lineTo(transXLeft, transStackLeft); 282 left.lineTo(transXLeft, transYLeft); 283 } else { 284 left.moveTo(transY1, transX1); 285 left.lineTo(transStack1, transX1); 286 left.lineTo(transStackLeft, transXLeft); 287 left.lineTo(transYLeft, transXLeft); 288 } 289 left.closePath(); 290 } else { 291 if (orientation == PlotOrientation.VERTICAL) { 292 left.moveTo(transX1, transStack1); 293 left.lineTo(transX1, transY1); 294 left.lineTo(transXLeft, transStackLeft); 295 } else { 296 left.moveTo(transStack1, transX1); 297 left.lineTo(transY1, transX1); 298 left.lineTo(transStackLeft, transXLeft); 299 } 300 left.closePath(); 301 } 302 303 float transStackRight = (float) rangeAxis.valueToJava2D( 304 adjStackRight[1], dataArea, edge1); 305 // RIGHT POLYGON 306 if (y2 >= 0.0) { 307 double yright = (y1 + y2) / 2.0 + stackRight[1]; 308 float transYRight 309 = (float) rangeAxis.valueToJava2D(yright, dataArea, edge1); 310 if (orientation == PlotOrientation.VERTICAL) { 311 right.moveTo(transX1, transStack1); 312 right.lineTo(transX1, transY1); 313 right.lineTo(transXRight, transYRight); 314 right.lineTo(transXRight, transStackRight); 315 } else { 316 right.moveTo(transStack1, transX1); 317 right.lineTo(transY1, transX1); 318 right.lineTo(transYRight, transXRight); 319 right.lineTo(transStackRight, transXRight); 320 } 321 right.closePath(); 322 } 323 else { 324 if (orientation == PlotOrientation.VERTICAL) { 325 right.moveTo(transX1, transStack1); 326 right.lineTo(transX1, transY1); 327 right.lineTo(transXRight, transStackRight); 328 } else { 329 right.moveTo(transStack1, transX1); 330 right.lineTo(transY1, transX1); 331 right.lineTo(transStackRight, transXRight); 332 } 333 right.closePath(); 334 } 335 } 336 else { // handle negative value 337 transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0], dataArea, 338 edge1); 339 float transStack1 = (float) rangeAxis.valueToJava2D(stack1[0], 340 dataArea, edge1); 341 float transStackLeft = (float) rangeAxis.valueToJava2D( 342 adjStackLeft[0], dataArea, edge1); 343 344 // LEFT POLYGON 345 if (y0 >= 0.0) { 346 if (orientation == PlotOrientation.VERTICAL) { 347 left.moveTo(transX1, transStack1); 348 left.lineTo(transX1, transY1); 349 left.lineTo(transXLeft, transStackLeft); 350 } else { 351 left.moveTo(transStack1, transX1); 352 left.lineTo(transY1, transX1); 353 left.lineTo(transStackLeft, transXLeft); 354 } 355 left.clone(); 356 } else { 357 double yleft = (y0 + y1) / 2.0 + stackLeft[0]; 358 float transYLeft = (float) rangeAxis.valueToJava2D(yleft, 359 dataArea, edge1); 360 if (orientation == PlotOrientation.VERTICAL) { 361 left.moveTo(transX1, transY1); 362 left.lineTo(transX1, transStack1); 363 left.lineTo(transXLeft, transStackLeft); 364 left.lineTo(transXLeft, transYLeft); 365 } else { 366 left.moveTo(transY1, transX1); 367 left.lineTo(transStack1, transX1); 368 left.lineTo(transStackLeft, transXLeft); 369 left.lineTo(transYLeft, transXLeft); 370 } 371 left.closePath(); 372 } 373 float transStackRight = (float) rangeAxis.valueToJava2D( 374 adjStackRight[0], dataArea, edge1); 375 376 // RIGHT POLYGON 377 if (y2 >= 0.0) { 378 if (orientation == PlotOrientation.VERTICAL) { 379 right.moveTo(transX1, transStack1); 380 right.lineTo(transX1, transY1); 381 right.lineTo(transXRight, transStackRight); 382 } else { 383 right.moveTo(transStack1, transX1); 384 right.lineTo(transY1, transX1); 385 right.lineTo(transStackRight, transXRight); 386 } 387 right.closePath(); 388 } else { 389 double yright = (y1 + y2) / 2.0 + stackRight[0]; 390 float transYRight = (float) rangeAxis.valueToJava2D(yright, 391 dataArea, edge1); 392 if (orientation == PlotOrientation.VERTICAL) { 393 right.moveTo(transX1, transStack1); 394 right.lineTo(transX1, transY1); 395 right.lineTo(transXRight, transYRight); 396 right.lineTo(transXRight, transStackRight); 397 } else { 398 right.moveTo(transStack1, transX1); 399 right.lineTo(transY1, transX1); 400 right.lineTo(transYRight, transXRight); 401 right.lineTo(transStackRight, transXRight); 402 } 403 right.closePath(); 404 } 405 } 406 407 // Get series Paint and Stroke 408 Paint itemPaint = getItemPaint(series, item); 409 if (pass == 0) { 410 g2.setPaint(itemPaint); 411 g2.fill(left); 412 g2.fill(right); 413 } 414 415 // add an entity for the item... 416 if (entities != null) { 417 // Create the entity area and limit it to the data area 418 Area dataAreaHotspot = new Area(left); 419 dataAreaHotspot.add(new Area(right)); 420 dataAreaHotspot.intersect(new Area(dataArea)); 421 422 if (!dataAreaHotspot.isEmpty()) { 423 addEntity(entities, dataAreaHotspot, dataset, series, item, 424 0.0, 0.0); 425 } 426 } 427 } 428 429 /** 430 * Calculates the stacked values (one positive and one negative) of all 431 * series up to, but not including, {@code series} for the specified 432 * item. It returns [0.0, 0.0] if {@code series} is the first series. 433 * 434 * @param dataset the dataset ({@code null} not permitted). 435 * @param series the series index. 436 * @param index the item index. 437 * 438 * @return An array containing the cumulative negative and positive values 439 * for all series values up to but excluding {@code series} 440 * for {@code index}. 441 */ 442 private double[] getStackValues(TableXYDataset dataset, 443 int series, int index) { 444 double[] result = new double[2]; 445 for (int i = 0; i < series; i++) { 446 double v = dataset.getYValue(i, index); 447 if (!Double.isNaN(v)) { 448 if (v >= 0.0) { 449 result[1] += v; 450 } 451 else { 452 result[0] += v; 453 } 454 } 455 } 456 return result; 457 } 458 459 /** 460 * Returns a pair of "stack" values calculated as the mean of the two 461 * specified stack value pairs. 462 * 463 * @param stack1 the first stack pair. 464 * @param stack2 the second stack pair. 465 * 466 * @return A pair of average stack values. 467 */ 468 private double[] averageStackValues(double[] stack1, double[] stack2) { 469 double[] result = new double[2]; 470 result[0] = (stack1[0] + stack2[0]) / 2.0; 471 result[1] = (stack1[1] + stack2[1]) / 2.0; 472 return result; 473 } 474 475 /** 476 * Calculates adjusted stack values from the supplied values. The value is 477 * the mean of the supplied values, unless either of the supplied values 478 * is zero, in which case the adjusted value is zero also. 479 * 480 * @param stack1 the first stack pair. 481 * @param stack2 the second stack pair. 482 * 483 * @return A pair of average stack values. 484 */ 485 private double[] adjustedStackValues(double[] stack1, double[] stack2) { 486 double[] result = new double[2]; 487 if (stack1[0] == 0.0 || stack2[0] == 0.0) { 488 result[0] = 0.0; 489 } 490 else { 491 result[0] = (stack1[0] + stack2[0]) / 2.0; 492 } 493 if (stack1[1] == 0.0 || stack2[1] == 0.0) { 494 result[1] = 0.0; 495 } 496 else { 497 result[1] = (stack1[1] + stack2[1]) / 2.0; 498 } 499 return result; 500 } 501 502 /** 503 * Tests this renderer for equality with an arbitrary object. 504 * 505 * @param obj the object ({@code null} permitted). 506 * 507 * @return A boolean. 508 */ 509 @Override 510 public boolean equals(Object obj) { 511 if (obj == this) { 512 return true; 513 } 514 if (!(obj instanceof StackedXYAreaRenderer2)) { 515 return false; 516 } 517 StackedXYAreaRenderer2 that = (StackedXYAreaRenderer2) obj; 518 if (this.roundXCoordinates != that.roundXCoordinates) { 519 return false; 520 } 521 return super.equals(obj); 522 } 523 524 /** 525 * Returns a clone of the renderer. 526 * 527 * @return A clone. 528 * 529 * @throws CloneNotSupportedException if the renderer cannot be cloned. 530 */ 531 @Override 532 public Object clone() throws CloneNotSupportedException { 533 return super.clone(); 534 } 535 536}