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