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