001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2007, 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-2007, 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 * 054 */ 055 056 package org.jfree.chart.renderer.xy; 057 058 import java.awt.Graphics2D; 059 import java.awt.Paint; 060 import java.awt.Shape; 061 import java.awt.geom.GeneralPath; 062 import java.awt.geom.Rectangle2D; 063 import java.io.Serializable; 064 065 import org.jfree.chart.axis.ValueAxis; 066 import org.jfree.chart.entity.EntityCollection; 067 import org.jfree.chart.event.RendererChangeEvent; 068 import org.jfree.chart.labels.XYToolTipGenerator; 069 import org.jfree.chart.plot.CrosshairState; 070 import org.jfree.chart.plot.PlotRenderingInfo; 071 import org.jfree.chart.plot.XYPlot; 072 import org.jfree.chart.urls.XYURLGenerator; 073 import org.jfree.data.Range; 074 import org.jfree.data.xy.TableXYDataset; 075 import org.jfree.data.xy.XYDataset; 076 import org.jfree.ui.RectangleEdge; 077 import org.jfree.util.PublicCloneable; 078 079 /** 080 * A stacked area renderer for the {@link XYPlot} class. 081 */ 082 public class StackedXYAreaRenderer2 extends XYAreaRenderer2 083 implements Cloneable, 084 PublicCloneable, 085 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 228 // get the data point... 229 double x1 = dataset.getXValue(series, item); 230 double y1 = dataset.getYValue(series, item); 231 if (Double.isNaN(y1)) { 232 y1 = 0.0; 233 } 234 double[] stack1 = getStackValues(tdataset, series, item); 235 236 // get the previous point and the next point so we can calculate a 237 // "hot spot" for the area (used by the chart entity)... 238 double x0 = dataset.getXValue(series, Math.max(item - 1, 0)); 239 double y0 = dataset.getYValue(series, Math.max(item - 1, 0)); 240 if (Double.isNaN(y0)) { 241 y0 = 0.0; 242 } 243 double[] stack0 = getStackValues(tdataset, series, Math.max(item - 1, 244 0)); 245 246 int itemCount = dataset.getItemCount(series); 247 double x2 = dataset.getXValue(series, Math.min(item + 1, 248 itemCount - 1)); 249 double y2 = dataset.getYValue(series, Math.min(item + 1, 250 itemCount - 1)); 251 if (Double.isNaN(y2)) { 252 y2 = 0.0; 253 } 254 double[] stack2 = getStackValues(tdataset, series, Math.min(item + 1, 255 itemCount - 1)); 256 257 double xleft = (x0 + x1) / 2.0; 258 double xright = (x1 + x2) / 2.0; 259 double[] stackLeft = averageStackValues(stack0, stack1); 260 double[] stackRight = averageStackValues(stack1, stack2); 261 double[] adjStackLeft = adjustedStackValues(stack0, stack1); 262 double[] adjStackRight = adjustedStackValues(stack1, stack2); 263 264 RectangleEdge edge0 = plot.getDomainAxisEdge(); 265 266 float transX1 = (float) domainAxis.valueToJava2D(x1, dataArea, edge0); 267 float transXLeft = (float) domainAxis.valueToJava2D(xleft, dataArea, 268 edge0); 269 float transXRight = (float) domainAxis.valueToJava2D(xright, dataArea, 270 edge0); 271 272 if (this.roundXCoordinates) { 273 transX1 = Math.round(transX1); 274 transXLeft = Math.round(transXLeft); 275 transXRight = Math.round(transXRight); 276 } 277 float transY1; 278 279 RectangleEdge edge1 = plot.getRangeAxisEdge(); 280 281 GeneralPath left = new GeneralPath(); 282 GeneralPath right = new GeneralPath(); 283 if (y1 >= 0.0) { // handle positive value 284 transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[1], dataArea, 285 edge1); 286 float transStack1 = (float) rangeAxis.valueToJava2D(stack1[1], 287 dataArea, edge1); 288 float transStackLeft = (float) rangeAxis.valueToJava2D( 289 adjStackLeft[1], dataArea, edge1); 290 291 // LEFT POLYGON 292 if (y0 >= 0.0) { 293 double yleft = (y0 + y1) / 2.0 + stackLeft[1]; 294 float transYLeft 295 = (float) rangeAxis.valueToJava2D(yleft, dataArea, edge1); 296 left.moveTo(transX1, transY1); 297 left.lineTo(transX1, transStack1); 298 left.lineTo(transXLeft, transStackLeft); 299 left.lineTo(transXLeft, transYLeft); 300 left.closePath(); 301 } 302 else { 303 left.moveTo(transX1, transStack1); 304 left.lineTo(transX1, transY1); 305 left.lineTo(transXLeft, transStackLeft); 306 left.closePath(); 307 } 308 309 float transStackRight = (float) rangeAxis.valueToJava2D( 310 adjStackRight[1], dataArea, edge1); 311 // RIGHT POLYGON 312 if (y2 >= 0.0) { 313 double yright = (y1 + y2) / 2.0 + stackRight[1]; 314 float transYRight 315 = (float) rangeAxis.valueToJava2D(yright, dataArea, edge1); 316 right.moveTo(transX1, transStack1); 317 right.lineTo(transX1, transY1); 318 right.lineTo(transXRight, transYRight); 319 right.lineTo(transXRight, transStackRight); 320 right.closePath(); 321 } 322 else { 323 right.moveTo(transX1, transStack1); 324 right.lineTo(transX1, transY1); 325 right.lineTo(transXRight, transStackRight); 326 right.closePath(); 327 } 328 } 329 else { // handle negative value 330 transY1 = (float) rangeAxis.valueToJava2D(y1 + stack1[0], dataArea, 331 edge1); 332 float transStack1 = (float) rangeAxis.valueToJava2D(stack1[0], 333 dataArea, edge1); 334 float transStackLeft = (float) rangeAxis.valueToJava2D( 335 adjStackLeft[0], dataArea, edge1); 336 337 // LEFT POLYGON 338 if (y0 >= 0.0) { 339 left.moveTo(transX1, transStack1); 340 left.lineTo(transX1, transY1); 341 left.lineTo(transXLeft, transStackLeft); 342 left.clone(); 343 } 344 else { 345 double yleft = (y0 + y1) / 2.0 + stackLeft[0]; 346 float transYLeft = (float) rangeAxis.valueToJava2D(yleft, 347 dataArea, edge1); 348 left.moveTo(transX1, transY1); 349 left.lineTo(transX1, transStack1); 350 left.lineTo(transXLeft, transStackLeft); 351 left.lineTo(transXLeft, transYLeft); 352 left.closePath(); 353 } 354 float transStackRight = (float) rangeAxis.valueToJava2D( 355 adjStackRight[0], dataArea, edge1); 356 357 // RIGHT POLYGON 358 if (y2 >= 0.0) { 359 right.moveTo(transX1, transStack1); 360 right.lineTo(transX1, transY1); 361 right.lineTo(transXRight, transStackRight); 362 right.closePath(); 363 } 364 else { 365 double yright = (y1 + y2) / 2.0 + stackRight[0]; 366 float transYRight = (float) rangeAxis.valueToJava2D(yright, 367 dataArea, edge1); 368 right.moveTo(transX1, transStack1); 369 right.lineTo(transX1, transY1); 370 right.lineTo(transXRight, transYRight); 371 right.lineTo(transXRight, transStackRight); 372 right.closePath(); 373 } 374 } 375 376 // Get series Paint and Stroke 377 Paint itemPaint = getItemPaint(series, item); 378 if (pass == 0) { 379 g2.setPaint(itemPaint); 380 g2.fill(left); 381 g2.fill(right); 382 } 383 384 // add an entity for the item... 385 if (entities != null) { 386 GeneralPath gp = new GeneralPath(left); 387 gp.append(right, false); 388 entityArea = gp; 389 addEntity(entities, entityArea, dataset, series, item, 390 transX1, transY1); 391 } 392 393 } 394 395 /** 396 * Calculates the stacked values (one positive and one negative) of all 397 * series up to, but not including, <code>series</code> for the specified 398 * item. It returns [0.0, 0.0] if <code>series</code> is the first series. 399 * 400 * @param dataset the dataset (<code>null</code> not permitted). 401 * @param series the series index. 402 * @param index the item index. 403 * 404 * @return An array containing the cumulative negative and positive values 405 * for all series values up to but excluding <code>series</code> 406 * for <code>index</code>. 407 */ 408 private double[] getStackValues(TableXYDataset dataset, 409 int series, int index) { 410 double[] result = new double[2]; 411 for (int i = 0; i < series; i++) { 412 double v = dataset.getYValue(i, index); 413 if (!Double.isNaN(v)) { 414 if (v >= 0.0) { 415 result[1] += v; 416 } 417 else { 418 result[0] += v; 419 } 420 } 421 } 422 return result; 423 } 424 425 /** 426 * Returns a pair of "stack" values calculated as the mean of the two 427 * specified stack value pairs. 428 * 429 * @param stack1 the first stack pair. 430 * @param stack2 the second stack pair. 431 * 432 * @return A pair of average stack values. 433 */ 434 private double[] averageStackValues(double[] stack1, double[] stack2) { 435 double[] result = new double[2]; 436 result[0] = (stack1[0] + stack2[0]) / 2.0; 437 result[1] = (stack1[1] + stack2[1]) / 2.0; 438 return result; 439 } 440 441 /** 442 * Calculates adjusted stack values from the supplied values. The value is 443 * the mean of the supplied values, unless either of the supplied values 444 * is zero, in which case the adjusted value is zero also. 445 * 446 * @param stack1 the first stack pair. 447 * @param stack2 the second stack pair. 448 * 449 * @return A pair of average stack values. 450 */ 451 private double[] adjustedStackValues(double[] stack1, double[] stack2) { 452 double[] result = new double[2]; 453 if (stack1[0] == 0.0 || stack2[0] == 0.0) { 454 result[0] = 0.0; 455 } 456 else { 457 result[0] = (stack1[0] + stack2[0]) / 2.0; 458 } 459 if (stack1[1] == 0.0 || stack2[1] == 0.0) { 460 result[1] = 0.0; 461 } 462 else { 463 result[1] = (stack1[1] + stack2[1]) / 2.0; 464 } 465 return result; 466 } 467 468 /** 469 * Tests this renderer for equality with an arbitrary object. 470 * 471 * @param obj the object (<code>null</code> permitted). 472 * 473 * @return A boolean. 474 */ 475 public boolean equals(Object obj) { 476 if (obj == this) { 477 return true; 478 } 479 if (!(obj instanceof StackedXYAreaRenderer2)) { 480 return false; 481 } 482 StackedXYAreaRenderer2 that = (StackedXYAreaRenderer2) obj; 483 if (this.roundXCoordinates != that.roundXCoordinates) { 484 return false; 485 } 486 return super.equals(obj); 487 } 488 489 /** 490 * Returns a clone of the renderer. 491 * 492 * @return A clone. 493 * 494 * @throws CloneNotSupportedException if the renderer cannot be cloned. 495 */ 496 public Object clone() throws CloneNotSupportedException { 497 return super.clone(); 498 } 499 500 }