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