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 * XYDifferenceRenderer.java 029 * ------------------------- 030 * (C) Copyright 2003-2008, by Object Refinery Limited and Contributors. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): Richard West, Advanced Micro Devices, Inc. (major rewrite 034 * of difference drawing algorithm); 035 * 036 * Changes: 037 * -------- 038 * 30-Apr-2003 : Version 1 (DG); 039 * 30-Jul-2003 : Modified entity constructor (CZ); 040 * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG); 041 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG); 042 * 09-Feb-2004 : Updated to support horizontal plot orientation (DG); 043 * 10-Feb-2004 : Added default constructor, setter methods and updated 044 * Javadocs (DG); 045 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState (DG); 046 * 30-Mar-2004 : Fixed bug in getNegativePaint() method (DG); 047 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 048 * getYValue() (DG); 049 * 25-Aug-2004 : Fixed a bug preventing the use of crosshairs (DG); 050 * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG); 051 * 19-Jan-2005 : Now accesses only primitive values from dataset (DG); 052 * 22-Feb-2005 : Override getLegendItem(int, int) to return "line" items (DG); 053 * 13-Apr-2005 : Fixed shape positioning bug (id = 1182062) (DG); 054 * 20-Apr-2005 : Use generators for legend tooltips and URLs (DG); 055 * 04-May-2005 : Override equals() method, renamed get/setPlotShapes() --> 056 * get/setShapesVisible (DG); 057 * 09-Jun-2005 : Updated equals() to handle GradientPaint (DG); 058 * 16-Jun-2005 : Fix bug (1221021) affecting stroke used for each series (DG); 059 * ------------- JFREECHART 1.0.x --------------------------------------------- 060 * 24-Jan-2007 : Added flag to allow rounding of x-coordinates, and fixed 061 * bug in clone() (DG); 062 * 05-Feb-2007 : Added an extra call to updateCrosshairValues() in 063 * drawItemPass1(), to fix bug 1564967 (DG); 064 * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG); 065 * 08-Mar-2007 : Fixed entity generation (DG); 066 * 20-Apr-2007 : Updated getLegendItem() for renderer change (DG); 067 * 23-Apr-2007 : Rewrite of difference drawing algorithm to allow use of 068 * series with disjoint x-values (RW); 069 * 04-May-2007 : Set processVisibleItemsOnly flag to false (DG); 070 * 17-May-2007 : Set datasetIndex and seriesIndex in getLegendItem() (DG); 071 * 18-May-2007 : Set dataset and seriesKey for LegendItem (DG); 072 * 05-Nov-2007 : Draw item labels if visible (RW); 073 * 17-Jun-2008 : Apply legend shape, font and paint attributes (DG); 074 * 075 */ 076 077 package org.jfree.chart.renderer.xy; 078 079 import java.awt.Color; 080 import java.awt.Graphics2D; 081 import java.awt.Paint; 082 import java.awt.Shape; 083 import java.awt.Stroke; 084 import java.awt.geom.GeneralPath; 085 import java.awt.geom.Line2D; 086 import java.awt.geom.Rectangle2D; 087 import java.io.IOException; 088 import java.io.ObjectInputStream; 089 import java.io.ObjectOutputStream; 090 import java.io.Serializable; 091 import java.util.Collections; 092 import java.util.LinkedList; 093 094 import org.jfree.chart.LegendItem; 095 import org.jfree.chart.axis.ValueAxis; 096 import org.jfree.chart.entity.EntityCollection; 097 import org.jfree.chart.entity.XYItemEntity; 098 import org.jfree.chart.event.RendererChangeEvent; 099 import org.jfree.chart.labels.XYToolTipGenerator; 100 import org.jfree.chart.plot.CrosshairState; 101 import org.jfree.chart.plot.PlotOrientation; 102 import org.jfree.chart.plot.PlotRenderingInfo; 103 import org.jfree.chart.plot.XYPlot; 104 import org.jfree.chart.urls.XYURLGenerator; 105 import org.jfree.data.xy.XYDataset; 106 import org.jfree.io.SerialUtilities; 107 import org.jfree.ui.RectangleEdge; 108 import org.jfree.util.PaintUtilities; 109 import org.jfree.util.PublicCloneable; 110 import org.jfree.util.ShapeUtilities; 111 112 /** 113 * A renderer for an {@link XYPlot} that highlights the differences between two 114 * series. 115 */ 116 public class XYDifferenceRenderer extends AbstractXYItemRenderer 117 implements XYItemRenderer, 118 Cloneable, 119 PublicCloneable, 120 Serializable { 121 122 /** For serialization. */ 123 private static final long serialVersionUID = -8447915602375584857L; 124 125 /** The paint used to highlight positive differences (y(0) > y(1)). */ 126 private transient Paint positivePaint; 127 128 /** The paint used to highlight negative differences (y(0) < y(1)). */ 129 private transient Paint negativePaint; 130 131 /** Display shapes at each point? */ 132 private boolean shapesVisible; 133 134 /** The shape to display in the legend item. */ 135 private transient Shape legendLine; 136 137 /** 138 * This flag controls whether or not the x-coordinates (in Java2D space) 139 * are rounded to integers. When set to true, this can avoid the vertical 140 * striping that anti-aliasing can generate. However, the rounding may not 141 * be appropriate for output in high resolution formats (for example, 142 * vector graphics formats such as SVG and PDF). 143 * 144 * @since 1.0.4 145 */ 146 private boolean roundXCoordinates; 147 148 /** 149 * Creates a new renderer with default attributes. 150 */ 151 public XYDifferenceRenderer() { 152 this(Color.green, Color.red, false); 153 } 154 155 /** 156 * Creates a new renderer. 157 * 158 * @param positivePaint the highlight color for positive differences 159 * (<code>null</code> not permitted). 160 * @param negativePaint the highlight color for negative differences 161 * (<code>null</code> not permitted). 162 * @param shapes draw shapes? 163 */ 164 public XYDifferenceRenderer(Paint positivePaint, Paint negativePaint, 165 boolean shapes) { 166 if (positivePaint == null) { 167 throw new IllegalArgumentException( 168 "Null 'positivePaint' argument."); 169 } 170 if (negativePaint == null) { 171 throw new IllegalArgumentException( 172 "Null 'negativePaint' argument."); 173 } 174 this.positivePaint = positivePaint; 175 this.negativePaint = negativePaint; 176 this.shapesVisible = shapes; 177 this.legendLine = new Line2D.Double(-7.0, 0.0, 7.0, 0.0); 178 this.roundXCoordinates = false; 179 } 180 181 /** 182 * Returns the paint used to highlight positive differences. 183 * 184 * @return The paint (never <code>null</code>). 185 * 186 * @see #setPositivePaint(Paint) 187 */ 188 public Paint getPositivePaint() { 189 return this.positivePaint; 190 } 191 192 /** 193 * Sets the paint used to highlight positive differences and sends a 194 * {@link RendererChangeEvent} to all registered listeners. 195 * 196 * @param paint the paint (<code>null</code> not permitted). 197 * 198 * @see #getPositivePaint() 199 */ 200 public void setPositivePaint(Paint paint) { 201 if (paint == null) { 202 throw new IllegalArgumentException("Null 'paint' argument."); 203 } 204 this.positivePaint = paint; 205 fireChangeEvent(); 206 } 207 208 /** 209 * Returns the paint used to highlight negative differences. 210 * 211 * @return The paint (never <code>null</code>). 212 * 213 * @see #setNegativePaint(Paint) 214 */ 215 public Paint getNegativePaint() { 216 return this.negativePaint; 217 } 218 219 /** 220 * Sets the paint used to highlight negative differences. 221 * 222 * @param paint the paint (<code>null</code> not permitted). 223 * 224 * @see #getNegativePaint() 225 */ 226 public void setNegativePaint(Paint paint) { 227 if (paint == null) { 228 throw new IllegalArgumentException("Null 'paint' argument."); 229 } 230 this.negativePaint = paint; 231 notifyListeners(new RendererChangeEvent(this)); 232 } 233 234 /** 235 * Returns a flag that controls whether or not shapes are drawn for each 236 * data value. 237 * 238 * @return A boolean. 239 * 240 * @see #setShapesVisible(boolean) 241 */ 242 public boolean getShapesVisible() { 243 return this.shapesVisible; 244 } 245 246 /** 247 * Sets a flag that controls whether or not shapes are drawn for each 248 * data value, and sends a {@link RendererChangeEvent} to all registered 249 * listeners. 250 * 251 * @param flag the flag. 252 * 253 * @see #getShapesVisible() 254 */ 255 public void setShapesVisible(boolean flag) { 256 this.shapesVisible = flag; 257 fireChangeEvent(); 258 } 259 260 /** 261 * Returns the shape used to represent a line in the legend. 262 * 263 * @return The legend line (never <code>null</code>). 264 * 265 * @see #setLegendLine(Shape) 266 */ 267 public Shape getLegendLine() { 268 return this.legendLine; 269 } 270 271 /** 272 * Sets the shape used as a line in each legend item and sends a 273 * {@link RendererChangeEvent} to all registered listeners. 274 * 275 * @param line the line (<code>null</code> not permitted). 276 * 277 * @see #getLegendLine() 278 */ 279 public void setLegendLine(Shape line) { 280 if (line == null) { 281 throw new IllegalArgumentException("Null 'line' argument."); 282 } 283 this.legendLine = line; 284 fireChangeEvent(); 285 } 286 287 /** 288 * Returns the flag that controls whether or not the x-coordinates (in 289 * Java2D space) are rounded to integer values. 290 * 291 * @return The flag. 292 * 293 * @since 1.0.4 294 * 295 * @see #setRoundXCoordinates(boolean) 296 */ 297 public boolean getRoundXCoordinates() { 298 return this.roundXCoordinates; 299 } 300 301 /** 302 * Sets the flag that controls whether or not the x-coordinates (in 303 * Java2D space) are rounded to integer values, and sends a 304 * {@link RendererChangeEvent} to all registered listeners. 305 * 306 * @param round the new flag value. 307 * 308 * @since 1.0.4 309 * 310 * @see #getRoundXCoordinates() 311 */ 312 public void setRoundXCoordinates(boolean round) { 313 this.roundXCoordinates = round; 314 fireChangeEvent(); 315 } 316 317 /** 318 * Initialises the renderer and returns a state object that should be 319 * passed to subsequent calls to the drawItem() method. This method will 320 * be called before the first item is rendered, giving the renderer an 321 * opportunity to initialise any state information it wants to maintain. 322 * The renderer can do nothing if it chooses. 323 * 324 * @param g2 the graphics device. 325 * @param dataArea the area inside the axes. 326 * @param plot the plot. 327 * @param data the data. 328 * @param info an optional info collection object to return data back to 329 * the caller. 330 * 331 * @return A state object. 332 */ 333 public XYItemRendererState initialise(Graphics2D g2, 334 Rectangle2D dataArea, 335 XYPlot plot, 336 XYDataset data, 337 PlotRenderingInfo info) { 338 339 XYItemRendererState state = super.initialise(g2, dataArea, plot, data, 340 info); 341 state.setProcessVisibleItemsOnly(false); 342 return state; 343 344 } 345 346 /** 347 * Returns <code>2</code>, the number of passes required by the renderer. 348 * The {@link XYPlot} will run through the dataset this number of times. 349 * 350 * @return The number of passes required by the renderer. 351 */ 352 public int getPassCount() { 353 return 2; 354 } 355 356 /** 357 * Draws the visual representation of a single data item. 358 * 359 * @param g2 the graphics device. 360 * @param state the renderer state. 361 * @param dataArea the area within which the data is being drawn. 362 * @param info collects information about the drawing. 363 * @param plot the plot (can be used to obtain standard color 364 * information etc). 365 * @param domainAxis the domain (horizontal) axis. 366 * @param rangeAxis the range (vertical) axis. 367 * @param dataset the dataset. 368 * @param series the series index (zero-based). 369 * @param item the item index (zero-based). 370 * @param crosshairState crosshair information for the plot 371 * (<code>null</code> permitted). 372 * @param pass the pass index. 373 */ 374 public void drawItem(Graphics2D g2, 375 XYItemRendererState state, 376 Rectangle2D dataArea, 377 PlotRenderingInfo info, 378 XYPlot plot, 379 ValueAxis domainAxis, 380 ValueAxis rangeAxis, 381 XYDataset dataset, 382 int series, 383 int item, 384 CrosshairState crosshairState, 385 int pass) { 386 387 if (pass == 0) { 388 drawItemPass0(g2, dataArea, info, plot, domainAxis, rangeAxis, 389 dataset, series, item, crosshairState); 390 } 391 else if (pass == 1) { 392 drawItemPass1(g2, dataArea, info, plot, domainAxis, rangeAxis, 393 dataset, series, item, crosshairState); 394 } 395 396 } 397 398 /** 399 * Draws the visual representation of a single data item, first pass. 400 * 401 * @param x_graphics the graphics device. 402 * @param x_dataArea the area within which the data is being drawn. 403 * @param x_info collects information about the drawing. 404 * @param x_plot the plot (can be used to obtain standard color 405 * information etc). 406 * @param x_domainAxis the domain (horizontal) axis. 407 * @param x_rangeAxis the range (vertical) axis. 408 * @param x_dataset the dataset. 409 * @param x_series the series index (zero-based). 410 * @param x_item the item index (zero-based). 411 * @param x_crosshairState crosshair information for the plot 412 * (<code>null</code> permitted). 413 */ 414 protected void drawItemPass0(Graphics2D x_graphics, 415 Rectangle2D x_dataArea, 416 PlotRenderingInfo x_info, 417 XYPlot x_plot, 418 ValueAxis x_domainAxis, 419 ValueAxis x_rangeAxis, 420 XYDataset x_dataset, 421 int x_series, 422 int x_item, 423 CrosshairState x_crosshairState) { 424 425 if (!((0 == x_series) && (0 == x_item))) { 426 return; 427 } 428 429 boolean b_impliedZeroSubtrahend = (1 == x_dataset.getSeriesCount()); 430 431 // check if either series is a degenerate case (i.e. less than 2 points) 432 if (isEitherSeriesDegenerate(x_dataset, b_impliedZeroSubtrahend)) { 433 return; 434 } 435 436 // check if series are disjoint (i.e. domain-spans do not overlap) 437 if (!b_impliedZeroSubtrahend && areSeriesDisjoint(x_dataset)) { 438 return; 439 } 440 441 // polygon definitions 442 LinkedList l_minuendXs = new LinkedList(); 443 LinkedList l_minuendYs = new LinkedList(); 444 LinkedList l_subtrahendXs = new LinkedList(); 445 LinkedList l_subtrahendYs = new LinkedList(); 446 LinkedList l_polygonXs = new LinkedList(); 447 LinkedList l_polygonYs = new LinkedList(); 448 449 // state 450 int l_minuendItem = 0; 451 int l_minuendItemCount = x_dataset.getItemCount(0); 452 Double l_minuendCurX = null; 453 Double l_minuendNextX = null; 454 Double l_minuendCurY = null; 455 Double l_minuendNextY = null; 456 double l_minuendMaxY = Double.NEGATIVE_INFINITY; 457 double l_minuendMinY = Double.POSITIVE_INFINITY; 458 459 int l_subtrahendItem = 0; 460 int l_subtrahendItemCount = 0; // actual value set below 461 Double l_subtrahendCurX = null; 462 Double l_subtrahendNextX = null; 463 Double l_subtrahendCurY = null; 464 Double l_subtrahendNextY = null; 465 double l_subtrahendMaxY = Double.NEGATIVE_INFINITY; 466 double l_subtrahendMinY = Double.POSITIVE_INFINITY; 467 468 // if a subtrahend is not specified, assume it is zero 469 if (b_impliedZeroSubtrahend) { 470 l_subtrahendItem = 0; 471 l_subtrahendItemCount = 2; 472 l_subtrahendCurX = new Double(x_dataset.getXValue(0, 0)); 473 l_subtrahendNextX = new Double(x_dataset.getXValue(0, 474 (l_minuendItemCount - 1))); 475 l_subtrahendCurY = new Double(0.0); 476 l_subtrahendNextY = new Double(0.0); 477 l_subtrahendMaxY = 0.0; 478 l_subtrahendMinY = 0.0; 479 480 l_subtrahendXs.add(l_subtrahendCurX); 481 l_subtrahendYs.add(l_subtrahendCurY); 482 } 483 else { 484 l_subtrahendItemCount = x_dataset.getItemCount(1); 485 } 486 487 boolean b_minuendDone = false; 488 boolean b_minuendAdvanced = true; 489 boolean b_minuendAtIntersect = false; 490 boolean b_minuendFastForward = false; 491 boolean b_subtrahendDone = false; 492 boolean b_subtrahendAdvanced = true; 493 boolean b_subtrahendAtIntersect = false; 494 boolean b_subtrahendFastForward = false; 495 boolean b_colinear = false; 496 497 boolean b_positive; 498 499 // coordinate pairs 500 double l_x1 = 0.0, l_y1 = 0.0; // current minuend point 501 double l_x2 = 0.0, l_y2 = 0.0; // next minuend point 502 double l_x3 = 0.0, l_y3 = 0.0; // current subtrahend point 503 double l_x4 = 0.0, l_y4 = 0.0; // next subtrahend point 504 505 // fast-forward through leading tails 506 boolean b_fastForwardDone = false; 507 while (!b_fastForwardDone) { 508 // get the x and y coordinates 509 l_x1 = x_dataset.getXValue(0, l_minuendItem); 510 l_y1 = x_dataset.getYValue(0, l_minuendItem); 511 l_x2 = x_dataset.getXValue(0, l_minuendItem + 1); 512 l_y2 = x_dataset.getYValue(0, l_minuendItem + 1); 513 514 l_minuendCurX = new Double(l_x1); 515 l_minuendCurY = new Double(l_y1); 516 l_minuendNextX = new Double(l_x2); 517 l_minuendNextY = new Double(l_y2); 518 519 if (b_impliedZeroSubtrahend) { 520 l_x3 = l_subtrahendCurX.doubleValue(); 521 l_y3 = l_subtrahendCurY.doubleValue(); 522 l_x4 = l_subtrahendNextX.doubleValue(); 523 l_y4 = l_subtrahendNextY.doubleValue(); 524 } 525 else { 526 l_x3 = x_dataset.getXValue(1, l_subtrahendItem); 527 l_y3 = x_dataset.getYValue(1, l_subtrahendItem); 528 l_x4 = x_dataset.getXValue(1, l_subtrahendItem + 1); 529 l_y4 = x_dataset.getYValue(1, l_subtrahendItem + 1); 530 531 l_subtrahendCurX = new Double(l_x3); 532 l_subtrahendCurY = new Double(l_y3); 533 l_subtrahendNextX = new Double(l_x4); 534 l_subtrahendNextY = new Double(l_y4); 535 } 536 537 if (l_x2 <= l_x3) { 538 // minuend needs to be fast forwarded 539 l_minuendItem++; 540 b_minuendFastForward = true; 541 continue; 542 } 543 544 if (l_x4 <= l_x1) { 545 // subtrahend needs to be fast forwarded 546 l_subtrahendItem++; 547 b_subtrahendFastForward = true; 548 continue; 549 } 550 551 // check if initial polygon needs to be clipped 552 if ((l_x3 < l_x1) && (l_x1 < l_x4)) { 553 // project onto subtrahend 554 double l_slope = (l_y4 - l_y3) / (l_x4 - l_x3); 555 l_subtrahendCurX = l_minuendCurX; 556 l_subtrahendCurY = new Double((l_slope * l_x1) 557 + (l_y3 - (l_slope * l_x3))); 558 559 l_subtrahendXs.add(l_subtrahendCurX); 560 l_subtrahendYs.add(l_subtrahendCurY); 561 } 562 563 if ((l_x1 < l_x3) && (l_x3 < l_x2)) { 564 // project onto minuend 565 double l_slope = (l_y2 - l_y1) / (l_x2 - l_x1); 566 l_minuendCurX = l_subtrahendCurX; 567 l_minuendCurY = new Double((l_slope * l_x3) 568 + (l_y1 - (l_slope * l_x1))); 569 570 l_minuendXs.add(l_minuendCurX); 571 l_minuendYs.add(l_minuendCurY); 572 } 573 574 l_minuendMaxY = l_minuendCurY.doubleValue(); 575 l_minuendMinY = l_minuendCurY.doubleValue(); 576 l_subtrahendMaxY = l_subtrahendCurY.doubleValue(); 577 l_subtrahendMinY = l_subtrahendCurY.doubleValue(); 578 579 b_fastForwardDone = true; 580 } 581 582 // start of algorithm 583 while (!b_minuendDone && !b_subtrahendDone) { 584 if (!b_minuendDone && !b_minuendFastForward && b_minuendAdvanced) { 585 l_x1 = x_dataset.getXValue(0, l_minuendItem); 586 l_y1 = x_dataset.getYValue(0, l_minuendItem); 587 l_minuendCurX = new Double(l_x1); 588 l_minuendCurY = new Double(l_y1); 589 590 if (!b_minuendAtIntersect) { 591 l_minuendXs.add(l_minuendCurX); 592 l_minuendYs.add(l_minuendCurY); 593 } 594 595 l_minuendMaxY = Math.max(l_minuendMaxY, l_y1); 596 l_minuendMinY = Math.min(l_minuendMinY, l_y1); 597 598 l_x2 = x_dataset.getXValue(0, l_minuendItem + 1); 599 l_y2 = x_dataset.getYValue(0, l_minuendItem + 1); 600 l_minuendNextX = new Double(l_x2); 601 l_minuendNextY = new Double(l_y2); 602 } 603 604 // never updated the subtrahend if it is implied to be zero 605 if (!b_impliedZeroSubtrahend && !b_subtrahendDone 606 && !b_subtrahendFastForward && b_subtrahendAdvanced) { 607 l_x3 = x_dataset.getXValue(1, l_subtrahendItem); 608 l_y3 = x_dataset.getYValue(1, l_subtrahendItem); 609 l_subtrahendCurX = new Double(l_x3); 610 l_subtrahendCurY = new Double(l_y3); 611 612 if (!b_subtrahendAtIntersect) { 613 l_subtrahendXs.add(l_subtrahendCurX); 614 l_subtrahendYs.add(l_subtrahendCurY); 615 } 616 617 l_subtrahendMaxY = Math.max(l_subtrahendMaxY, l_y3); 618 l_subtrahendMinY = Math.min(l_subtrahendMinY, l_y3); 619 620 l_x4 = x_dataset.getXValue(1, l_subtrahendItem + 1); 621 l_y4 = x_dataset.getYValue(1, l_subtrahendItem + 1); 622 l_subtrahendNextX = new Double(l_x4); 623 l_subtrahendNextY = new Double(l_y4); 624 } 625 626 // deassert b_*FastForward (only matters for 1st time through loop) 627 b_minuendFastForward = false; 628 b_subtrahendFastForward = false; 629 630 Double l_intersectX = null; 631 Double l_intersectY = null; 632 boolean b_intersect = false; 633 634 b_minuendAtIntersect = false; 635 b_subtrahendAtIntersect = false; 636 637 // check for intersect 638 if ((l_x2 == l_x4) && (l_y2 == l_y4)) { 639 // check if line segments are colinear 640 if ((l_x1 == l_x3) && (l_y1 == l_y3)) { 641 b_colinear = true; 642 } 643 else { 644 // the intersect is at the next point for both the minuend 645 // and subtrahend 646 l_intersectX = new Double(l_x2); 647 l_intersectY = new Double(l_y2); 648 649 b_intersect = true; 650 b_minuendAtIntersect = true; 651 b_subtrahendAtIntersect = true; 652 } 653 } 654 else { 655 // compute common denominator 656 double l_denominator = ((l_y4 - l_y3) * (l_x2 - l_x1)) 657 - ((l_x4 - l_x3) * (l_y2 - l_y1)); 658 659 // compute common deltas 660 double l_deltaY = l_y1 - l_y3; 661 double l_deltaX = l_x1 - l_x3; 662 663 // compute numerators 664 double l_numeratorA = ((l_x4 - l_x3) * l_deltaY) 665 - ((l_y4 - l_y3) * l_deltaX); 666 double l_numeratorB = ((l_x2 - l_x1) * l_deltaY) 667 - ((l_y2 - l_y1) * l_deltaX); 668 669 // check if line segments are colinear 670 if ((0 == l_numeratorA) && (0 == l_numeratorB) 671 && (0 == l_denominator)) { 672 b_colinear = true; 673 } 674 else { 675 // check if previously colinear 676 if (b_colinear) { 677 // clear colinear points and flag 678 l_minuendXs.clear(); 679 l_minuendYs.clear(); 680 l_subtrahendXs.clear(); 681 l_subtrahendYs.clear(); 682 l_polygonXs.clear(); 683 l_polygonYs.clear(); 684 685 b_colinear = false; 686 687 // set new starting point for the polygon 688 boolean b_useMinuend = ((l_x3 <= l_x1) 689 && (l_x1 <= l_x4)); 690 l_polygonXs.add(b_useMinuend ? l_minuendCurX 691 : l_subtrahendCurX); 692 l_polygonYs.add(b_useMinuend ? l_minuendCurY 693 : l_subtrahendCurY); 694 } 695 696 // compute slope components 697 double l_slopeA = l_numeratorA / l_denominator; 698 double l_slopeB = l_numeratorB / l_denominator; 699 700 // check if the line segments intersect 701 if ((0 < l_slopeA) && (l_slopeA <= 1) && (0 < l_slopeB) 702 && (l_slopeB <= 1)) { 703 // compute the point of intersection 704 double l_xi = l_x1 + (l_slopeA * (l_x2 - l_x1)); 705 double l_yi = l_y1 + (l_slopeA * (l_y2 - l_y1)); 706 707 l_intersectX = new Double(l_xi); 708 l_intersectY = new Double(l_yi); 709 b_intersect = true; 710 b_minuendAtIntersect = ((l_xi == l_x2) 711 && (l_yi == l_y2)); 712 b_subtrahendAtIntersect = ((l_xi == l_x4) 713 && (l_yi == l_y4)); 714 715 // advance minuend and subtrahend to intesect 716 l_minuendCurX = l_intersectX; 717 l_minuendCurY = l_intersectY; 718 l_subtrahendCurX = l_intersectX; 719 l_subtrahendCurY = l_intersectY; 720 } 721 } 722 } 723 724 if (b_intersect) { 725 // create the polygon 726 // add the minuend's points to polygon 727 l_polygonXs.addAll(l_minuendXs); 728 l_polygonYs.addAll(l_minuendYs); 729 730 // add intersection point to the polygon 731 l_polygonXs.add(l_intersectX); 732 l_polygonYs.add(l_intersectY); 733 734 // add the subtrahend's points to the polygon in reverse 735 Collections.reverse(l_subtrahendXs); 736 Collections.reverse(l_subtrahendYs); 737 l_polygonXs.addAll(l_subtrahendXs); 738 l_polygonYs.addAll(l_subtrahendYs); 739 740 // create an actual polygon 741 b_positive = (l_subtrahendMaxY <= l_minuendMaxY) 742 && (l_subtrahendMinY <= l_minuendMinY); 743 createPolygon(x_graphics, x_dataArea, x_plot, x_domainAxis, 744 x_rangeAxis, b_positive, l_polygonXs, l_polygonYs); 745 746 // clear the point vectors 747 l_minuendXs.clear(); 748 l_minuendYs.clear(); 749 l_subtrahendXs.clear(); 750 l_subtrahendYs.clear(); 751 l_polygonXs.clear(); 752 l_polygonYs.clear(); 753 754 // set the maxY and minY values to intersect y-value 755 double l_y = l_intersectY.doubleValue(); 756 l_minuendMaxY = l_y; 757 l_subtrahendMaxY = l_y; 758 l_minuendMinY = l_y; 759 l_subtrahendMinY = l_y; 760 761 // add interection point to new polygon 762 l_polygonXs.add(l_intersectX); 763 l_polygonYs.add(l_intersectY); 764 } 765 766 // advance the minuend if needed 767 if (l_x2 <= l_x4) { 768 l_minuendItem++; 769 b_minuendAdvanced = true; 770 } 771 else { 772 b_minuendAdvanced = false; 773 } 774 775 // advance the subtrahend if needed 776 if (l_x4 <= l_x2) { 777 l_subtrahendItem++; 778 b_subtrahendAdvanced = true; 779 } 780 else { 781 b_subtrahendAdvanced = false; 782 } 783 784 b_minuendDone = (l_minuendItem == (l_minuendItemCount - 1)); 785 b_subtrahendDone = (l_subtrahendItem == (l_subtrahendItemCount 786 - 1)); 787 } 788 789 // check if the final polygon needs to be clipped 790 if (b_minuendDone && (l_x3 < l_x2) && (l_x2 < l_x4)) { 791 // project onto subtrahend 792 double l_slope = (l_y4 - l_y3) / (l_x4 - l_x3); 793 l_subtrahendNextX = l_minuendNextX; 794 l_subtrahendNextY = new Double((l_slope * l_x2) 795 + (l_y3 - (l_slope * l_x3))); 796 } 797 798 if (b_subtrahendDone && (l_x1 < l_x4) && (l_x4 < l_x2)) { 799 // project onto minuend 800 double l_slope = (l_y2 - l_y1) / (l_x2 - l_x1); 801 l_minuendNextX = l_subtrahendNextX; 802 l_minuendNextY = new Double((l_slope * l_x4) 803 + (l_y1 - (l_slope * l_x1))); 804 } 805 806 // consider last point of minuend and subtrahend for determining 807 // positivity 808 l_minuendMaxY = Math.max(l_minuendMaxY, 809 l_minuendNextY.doubleValue()); 810 l_subtrahendMaxY = Math.max(l_subtrahendMaxY, 811 l_subtrahendNextY.doubleValue()); 812 l_minuendMinY = Math.min(l_minuendMinY, 813 l_minuendNextY.doubleValue()); 814 l_subtrahendMinY = Math.min(l_subtrahendMinY, 815 l_subtrahendNextY.doubleValue()); 816 817 // add the last point of the minuned and subtrahend 818 l_minuendXs.add(l_minuendNextX); 819 l_minuendYs.add(l_minuendNextY); 820 l_subtrahendXs.add(l_subtrahendNextX); 821 l_subtrahendYs.add(l_subtrahendNextY); 822 823 // create the polygon 824 // add the minuend's points to polygon 825 l_polygonXs.addAll(l_minuendXs); 826 l_polygonYs.addAll(l_minuendYs); 827 828 // add the subtrahend's points to the polygon in reverse 829 Collections.reverse(l_subtrahendXs); 830 Collections.reverse(l_subtrahendYs); 831 l_polygonXs.addAll(l_subtrahendXs); 832 l_polygonYs.addAll(l_subtrahendYs); 833 834 // create an actual polygon 835 b_positive = (l_subtrahendMaxY <= l_minuendMaxY) 836 && (l_subtrahendMinY <= l_minuendMinY); 837 createPolygon(x_graphics, x_dataArea, x_plot, x_domainAxis, 838 x_rangeAxis, b_positive, l_polygonXs, l_polygonYs); 839 } 840 841 /** 842 * Draws the visual representation of a single data item, second pass. In 843 * the second pass, the renderer draws the lines and shapes for the 844 * individual points in the two series. 845 * 846 * @param x_graphics the graphics device. 847 * @param x_dataArea the area within which the data is being drawn. 848 * @param x_info collects information about the drawing. 849 * @param x_plot the plot (can be used to obtain standard color 850 * information etc). 851 * @param x_domainAxis the domain (horizontal) axis. 852 * @param x_rangeAxis the range (vertical) axis. 853 * @param x_dataset the dataset. 854 * @param x_series the series index (zero-based). 855 * @param x_item the item index (zero-based). 856 * @param x_crosshairState crosshair information for the plot 857 * (<code>null</code> permitted). 858 */ 859 protected void drawItemPass1(Graphics2D x_graphics, 860 Rectangle2D x_dataArea, 861 PlotRenderingInfo x_info, 862 XYPlot x_plot, 863 ValueAxis x_domainAxis, 864 ValueAxis x_rangeAxis, 865 XYDataset x_dataset, 866 int x_series, 867 int x_item, 868 CrosshairState x_crosshairState) { 869 870 Shape l_entityArea = null; 871 EntityCollection l_entities = null; 872 if (null != x_info) { 873 l_entities = x_info.getOwner().getEntityCollection(); 874 } 875 876 Paint l_seriesPaint = getItemPaint(x_series, x_item); 877 Stroke l_seriesStroke = getItemStroke(x_series, x_item); 878 x_graphics.setPaint(l_seriesPaint); 879 x_graphics.setStroke(l_seriesStroke); 880 881 PlotOrientation l_orientation = x_plot.getOrientation(); 882 RectangleEdge l_domainAxisLocation = x_plot.getDomainAxisEdge(); 883 RectangleEdge l_rangeAxisLocation = x_plot.getRangeAxisEdge(); 884 885 double l_x0 = x_dataset.getXValue(x_series, x_item); 886 double l_y0 = x_dataset.getYValue(x_series, x_item); 887 double l_x1 = x_domainAxis.valueToJava2D(l_x0, x_dataArea, 888 l_domainAxisLocation); 889 double l_y1 = x_rangeAxis.valueToJava2D(l_y0, x_dataArea, 890 l_rangeAxisLocation); 891 892 if (getShapesVisible()) { 893 Shape l_shape = getItemShape(x_series, x_item); 894 if (l_orientation == PlotOrientation.HORIZONTAL) { 895 l_shape = ShapeUtilities.createTranslatedShape(l_shape, 896 l_y1, l_x1); 897 } 898 else { 899 l_shape = ShapeUtilities.createTranslatedShape(l_shape, 900 l_x1, l_y1); 901 } 902 if (l_shape.intersects(x_dataArea)) { 903 x_graphics.setPaint(getItemPaint(x_series, x_item)); 904 x_graphics.fill(l_shape); 905 } 906 l_entityArea = l_shape; 907 } 908 909 // add an entity for the item... 910 if (null != l_entities) { 911 if (null == l_entityArea) { 912 l_entityArea = new Rectangle2D.Double((l_x1 - 2), (l_y1 - 2), 913 4, 4); 914 } 915 String l_tip = null; 916 XYToolTipGenerator l_tipGenerator = getToolTipGenerator(x_series, 917 x_item); 918 if (null != l_tipGenerator) { 919 l_tip = l_tipGenerator.generateToolTip(x_dataset, x_series, 920 x_item); 921 } 922 String l_url = null; 923 XYURLGenerator l_urlGenerator = getURLGenerator(); 924 if (null != l_urlGenerator) { 925 l_url = l_urlGenerator.generateURL(x_dataset, x_series, 926 x_item); 927 } 928 XYItemEntity l_entity = new XYItemEntity(l_entityArea, x_dataset, 929 x_series, x_item, l_tip, l_url); 930 l_entities.add(l_entity); 931 } 932 933 // draw the item label if there is one... 934 if (isItemLabelVisible(x_series, x_item)) { 935 drawItemLabel(x_graphics, l_orientation, x_dataset, x_series, 936 x_item, l_x1, l_y1, (l_y1 < 0.0)); 937 } 938 939 int l_domainAxisIndex = x_plot.getDomainAxisIndex(x_domainAxis); 940 int l_rangeAxisIndex = x_plot.getRangeAxisIndex(x_rangeAxis); 941 updateCrosshairValues(x_crosshairState, l_x0, l_y0, l_domainAxisIndex, 942 l_rangeAxisIndex, l_x1, l_y1, l_orientation); 943 944 if (0 == x_item) { 945 return; 946 } 947 948 double l_x2 = x_domainAxis.valueToJava2D(x_dataset.getXValue(x_series, 949 (x_item - 1)), x_dataArea, l_domainAxisLocation); 950 double l_y2 = x_rangeAxis.valueToJava2D(x_dataset.getYValue(x_series, 951 (x_item - 1)), x_dataArea, l_rangeAxisLocation); 952 953 Line2D l_line = null; 954 if (PlotOrientation.HORIZONTAL == l_orientation) { 955 l_line = new Line2D.Double(l_y1, l_x1, l_y2, l_x2); 956 } 957 else if (PlotOrientation.VERTICAL == l_orientation) { 958 l_line = new Line2D.Double(l_x1, l_y1, l_x2, l_y2); 959 } 960 961 if ((null != l_line) && l_line.intersects(x_dataArea)) { 962 x_graphics.setPaint(getItemPaint(x_series, x_item)); 963 x_graphics.setStroke(getItemStroke(x_series, x_item)); 964 x_graphics.draw(l_line); 965 } 966 } 967 968 /** 969 * Determines if a dataset is degenerate. A degenerate dataset is a 970 * dataset where either series has less than two (2) points. 971 * 972 * @param x_dataset the dataset. 973 * @param x_impliedZeroSubtrahend if false, do not check the subtrahend 974 * 975 * @return true if the dataset is degenerate. 976 */ 977 private boolean isEitherSeriesDegenerate(XYDataset x_dataset, 978 boolean x_impliedZeroSubtrahend) { 979 980 if (x_impliedZeroSubtrahend) { 981 return (x_dataset.getItemCount(0) < 2); 982 } 983 984 return ((x_dataset.getItemCount(0) < 2) 985 || (x_dataset.getItemCount(1) < 2)); 986 } 987 988 /** 989 * Determines if the two (2) series are disjoint. 990 * Disjoint series do not overlap in the domain space. 991 * 992 * @param x_dataset the dataset. 993 * 994 * @return true if the dataset is degenerate. 995 */ 996 private boolean areSeriesDisjoint(XYDataset x_dataset) { 997 998 int l_minuendItemCount = x_dataset.getItemCount(0); 999 double l_minuendFirst = x_dataset.getXValue(0, 0); 1000 double l_minuendLast = x_dataset.getXValue(0, l_minuendItemCount - 1); 1001 1002 int l_subtrahendItemCount = x_dataset.getItemCount(1); 1003 double l_subtrahendFirst = x_dataset.getXValue(1, 0); 1004 double l_subtrahendLast = x_dataset.getXValue(1, 1005 l_subtrahendItemCount - 1); 1006 1007 return ((l_minuendLast < l_subtrahendFirst) 1008 || (l_subtrahendLast < l_minuendFirst)); 1009 } 1010 1011 /** 1012 * Draws the visual representation of a polygon 1013 * 1014 * @param x_graphics the graphics device. 1015 * @param x_dataArea the area within which the data is being drawn. 1016 * @param x_plot the plot (can be used to obtain standard color 1017 * information etc). 1018 * @param x_domainAxis the domain (horizontal) axis. 1019 * @param x_rangeAxis the range (vertical) axis. 1020 * @param x_positive indicates if the polygon is positive (true) or 1021 * negative (false). 1022 * @param x_xValues a linked list of the x values (expects values to be 1023 * of type Double). 1024 * @param x_yValues a linked list of the y values (expects values to be 1025 * of type Double). 1026 */ 1027 private void createPolygon (Graphics2D x_graphics, 1028 Rectangle2D x_dataArea, 1029 XYPlot x_plot, 1030 ValueAxis x_domainAxis, 1031 ValueAxis x_rangeAxis, 1032 boolean x_positive, 1033 LinkedList x_xValues, 1034 LinkedList x_yValues) { 1035 1036 PlotOrientation l_orientation = x_plot.getOrientation(); 1037 RectangleEdge l_domainAxisLocation = x_plot.getDomainAxisEdge(); 1038 RectangleEdge l_rangeAxisLocation = x_plot.getRangeAxisEdge(); 1039 1040 Object[] l_xValues = x_xValues.toArray(); 1041 Object[] l_yValues = x_yValues.toArray(); 1042 1043 GeneralPath l_path = new GeneralPath(); 1044 1045 if (PlotOrientation.VERTICAL == l_orientation) { 1046 double l_x = x_domainAxis.valueToJava2D(( 1047 (Double) l_xValues[0]).doubleValue(), x_dataArea, 1048 l_domainAxisLocation); 1049 if (this.roundXCoordinates) { 1050 l_x = Math.rint(l_x); 1051 } 1052 1053 double l_y = x_rangeAxis.valueToJava2D(( 1054 (Double) l_yValues[0]).doubleValue(), x_dataArea, 1055 l_rangeAxisLocation); 1056 1057 l_path.moveTo((float) l_x, (float) l_y); 1058 for (int i = 1; i < l_xValues.length; i++) { 1059 l_x = x_domainAxis.valueToJava2D(( 1060 (Double) l_xValues[i]).doubleValue(), x_dataArea, 1061 l_domainAxisLocation); 1062 if (this.roundXCoordinates) { 1063 l_x = Math.rint(l_x); 1064 } 1065 1066 l_y = x_rangeAxis.valueToJava2D(( 1067 (Double) l_yValues[i]).doubleValue(), x_dataArea, 1068 l_rangeAxisLocation); 1069 l_path.lineTo((float) l_x, (float) l_y); 1070 } 1071 l_path.closePath(); 1072 } 1073 else { 1074 double l_x = x_domainAxis.valueToJava2D(( 1075 (Double) l_xValues[0]).doubleValue(), x_dataArea, 1076 l_domainAxisLocation); 1077 if (this.roundXCoordinates) { 1078 l_x = Math.rint(l_x); 1079 } 1080 1081 double l_y = x_rangeAxis.valueToJava2D(( 1082 (Double) l_yValues[0]).doubleValue(), x_dataArea, 1083 l_rangeAxisLocation); 1084 1085 l_path.moveTo((float) l_y, (float) l_x); 1086 for (int i = 1; i < l_xValues.length; i++) { 1087 l_x = x_domainAxis.valueToJava2D(( 1088 (Double) l_xValues[i]).doubleValue(), x_dataArea, 1089 l_domainAxisLocation); 1090 if (this.roundXCoordinates) { 1091 l_x = Math.rint(l_x); 1092 } 1093 1094 l_y = x_rangeAxis.valueToJava2D(( 1095 (Double) l_yValues[i]).doubleValue(), x_dataArea, 1096 l_rangeAxisLocation); 1097 l_path.lineTo((float) l_y, (float) l_x); 1098 } 1099 l_path.closePath(); 1100 } 1101 1102 if (l_path.intersects(x_dataArea)) { 1103 x_graphics.setPaint(x_positive ? getPositivePaint() 1104 : getNegativePaint()); 1105 x_graphics.fill(l_path); 1106 } 1107 } 1108 1109 /** 1110 * Returns a default legend item for the specified series. Subclasses 1111 * should override this method to generate customised items. 1112 * 1113 * @param datasetIndex the dataset index (zero-based). 1114 * @param series the series index (zero-based). 1115 * 1116 * @return A legend item for the series. 1117 */ 1118 public LegendItem getLegendItem(int datasetIndex, int series) { 1119 LegendItem result = null; 1120 XYPlot p = getPlot(); 1121 if (p != null) { 1122 XYDataset dataset = p.getDataset(datasetIndex); 1123 if (dataset != null) { 1124 if (getItemVisible(series, 0)) { 1125 String label = getLegendItemLabelGenerator().generateLabel( 1126 dataset, series); 1127 String description = label; 1128 String toolTipText = null; 1129 if (getLegendItemToolTipGenerator() != null) { 1130 toolTipText 1131 = getLegendItemToolTipGenerator().generateLabel( 1132 dataset, series); 1133 } 1134 String urlText = null; 1135 if (getLegendItemURLGenerator() != null) { 1136 urlText = getLegendItemURLGenerator().generateLabel( 1137 dataset, series); 1138 } 1139 Paint paint = lookupSeriesPaint(series); 1140 Stroke stroke = lookupSeriesStroke(series); 1141 Shape line = getLegendLine(); 1142 result = new LegendItem(label, description, 1143 toolTipText, urlText, line, stroke, paint); 1144 result.setLabelFont(lookupLegendTextFont(series)); 1145 Paint labelPaint = lookupLegendTextPaint(series); 1146 if (labelPaint != null) { 1147 result.setLabelPaint(labelPaint); 1148 } 1149 result.setDataset(dataset); 1150 result.setDatasetIndex(datasetIndex); 1151 result.setSeriesKey(dataset.getSeriesKey(series)); 1152 result.setSeriesIndex(series); 1153 } 1154 } 1155 1156 } 1157 1158 return result; 1159 1160 } 1161 1162 /** 1163 * Tests this renderer for equality with an arbitrary object. 1164 * 1165 * @param obj the object (<code>null</code> permitted). 1166 * 1167 * @return A boolean. 1168 */ 1169 public boolean equals(Object obj) { 1170 if (obj == this) { 1171 return true; 1172 } 1173 if (!(obj instanceof XYDifferenceRenderer)) { 1174 return false; 1175 } 1176 if (!super.equals(obj)) { 1177 return false; 1178 } 1179 XYDifferenceRenderer that = (XYDifferenceRenderer) obj; 1180 if (!PaintUtilities.equal(this.positivePaint, that.positivePaint)) { 1181 return false; 1182 } 1183 if (!PaintUtilities.equal(this.negativePaint, that.negativePaint)) { 1184 return false; 1185 } 1186 if (this.shapesVisible != that.shapesVisible) { 1187 return false; 1188 } 1189 if (!ShapeUtilities.equal(this.legendLine, that.legendLine)) { 1190 return false; 1191 } 1192 if (this.roundXCoordinates != that.roundXCoordinates) { 1193 return false; 1194 } 1195 return true; 1196 } 1197 1198 /** 1199 * Returns a clone of the renderer. 1200 * 1201 * @return A clone. 1202 * 1203 * @throws CloneNotSupportedException if the renderer cannot be cloned. 1204 */ 1205 public Object clone() throws CloneNotSupportedException { 1206 XYDifferenceRenderer clone = (XYDifferenceRenderer) super.clone(); 1207 clone.legendLine = ShapeUtilities.clone(this.legendLine); 1208 return clone; 1209 } 1210 1211 /** 1212 * Provides serialization support. 1213 * 1214 * @param stream the output stream. 1215 * 1216 * @throws IOException if there is an I/O error. 1217 */ 1218 private void writeObject(ObjectOutputStream stream) throws IOException { 1219 stream.defaultWriteObject(); 1220 SerialUtilities.writePaint(this.positivePaint, stream); 1221 SerialUtilities.writePaint(this.negativePaint, stream); 1222 SerialUtilities.writeShape(this.legendLine, stream); 1223 } 1224 1225 /** 1226 * Provides serialization support. 1227 * 1228 * @param stream the input stream. 1229 * 1230 * @throws IOException if there is an I/O error. 1231 * @throws ClassNotFoundException if there is a classpath problem. 1232 */ 1233 private void readObject(ObjectInputStream stream) 1234 throws IOException, ClassNotFoundException { 1235 stream.defaultReadObject(); 1236 this.positivePaint = SerialUtilities.readPaint(stream); 1237 this.negativePaint = SerialUtilities.readPaint(stream); 1238 this.legendLine = SerialUtilities.readShape(stream); 1239 } 1240 1241 }