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