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