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