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