001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors. 006 * 007 * Project Info: http://www.jfree.org/jfreechart/index.html 008 * 009 * This library is free software; you can redistribute it and/or modify it 010 * under the terms of the GNU Lesser General Public License as published by 011 * the Free Software Foundation; either version 2.1 of the License, or 012 * (at your option) any later version. 013 * 014 * This library is distributed in the hope that it will be useful, but 015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 017 * License for more details. 018 * 019 * You should have received a copy of the GNU Lesser General Public 020 * License along with this library; if not, write to the Free Software 021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 022 * USA. 023 * 024 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 025 * in the United States and other countries.] 026 * 027 * ------------------------- 028 * XYDifferenceRenderer.java 029 * ------------------------- 030 * (C) Copyright 2003-2007, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): Christian W. Zuckschwerdt; 034 * 035 * $Id: XYDifferenceRenderer.java,v 1.12.2.10 2007/03/08 17:22:53 mungady Exp $ 036 * 037 * Changes: 038 * -------- 039 * 30-Apr-2003 : Version 1 (DG); 040 * 30-Jul-2003 : Modified entity constructor (CZ); 041 * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG); 042 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG); 043 * 09-Feb-2004 : Updated to support horizontal plot orientation (DG); 044 * 10-Feb-2004 : Added default constructor, setter methods and updated 045 * Javadocs (DG); 046 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState (DG); 047 * 30-Mar-2004 : Fixed bug in getNegativePaint() method (DG); 048 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 049 * getYValue() (DG); 050 * 25-Aug-2004 : Fixed a bug preventing the use of crosshairs (DG); 051 * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG); 052 * 19-Jan-2005 : Now accesses only primitive values from dataset (DG); 053 * 22-Feb-2005 : Override getLegendItem(int, int) to return "line" items (DG); 054 * 13-Apr-2005 : Fixed shape positioning bug (id = 1182062) (DG); 055 * 20-Apr-2005 : Use generators for legend tooltips and URLs (DG); 056 * 04-May-2005 : Override equals() method, renamed get/setPlotShapes() --> 057 * get/setShapesVisible (DG); 058 * 09-Jun-2005 : Updated equals() to handle GradientPaint (DG); 059 * 16-Jun-2005 : Fix bug (1221021) affecting stroke used for each series (DG); 060 * ------------- JFREECHART 1.0.x --------------------------------------------- 061 * 24-Jan-2007 : Added flag to allow rounding of x-coordinates, and fixed 062 * bug in clone() (DG); 063 * 05-Feb-2007 : Added an extra call to updateCrosshairValues() in 064 * drawItemPass1(), to fix bug 1564967 (DG); 065 * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG); 066 * 08-Mar-2007 : Fixed entity generation (DG); 067 * 068 */ 069 070 package org.jfree.chart.renderer.xy; 071 072 import java.awt.Color; 073 import java.awt.Graphics2D; 074 import java.awt.Paint; 075 import java.awt.Shape; 076 import java.awt.Stroke; 077 import java.awt.geom.GeneralPath; 078 import java.awt.geom.Line2D; 079 import java.awt.geom.Rectangle2D; 080 import java.io.IOException; 081 import java.io.ObjectInputStream; 082 import java.io.ObjectOutputStream; 083 import java.io.Serializable; 084 085 import org.jfree.chart.LegendItem; 086 import org.jfree.chart.axis.ValueAxis; 087 import org.jfree.chart.entity.EntityCollection; 088 import org.jfree.chart.entity.XYItemEntity; 089 import org.jfree.chart.event.RendererChangeEvent; 090 import org.jfree.chart.labels.XYToolTipGenerator; 091 import org.jfree.chart.plot.CrosshairState; 092 import org.jfree.chart.plot.PlotOrientation; 093 import org.jfree.chart.plot.PlotRenderingInfo; 094 import org.jfree.chart.plot.XYPlot; 095 import org.jfree.data.xy.XYDataset; 096 import org.jfree.io.SerialUtilities; 097 import org.jfree.ui.RectangleEdge; 098 import org.jfree.util.PaintUtilities; 099 import org.jfree.util.PublicCloneable; 100 import org.jfree.util.ShapeUtilities; 101 102 /** 103 * A renderer for an {@link XYPlot} that highlights the differences between two 104 * series. The renderer expects a dataset that: 105 * <ul> 106 * <li>has exactly two series;</li> 107 * <li>each series has the same x-values;</li> 108 * <li>no <code>null</code> values; 109 * </ul> 110 */ 111 public class XYDifferenceRenderer extends AbstractXYItemRenderer 112 implements XYItemRenderer, 113 Cloneable, 114 PublicCloneable, 115 Serializable { 116 117 /** For serialization. */ 118 private static final long serialVersionUID = -8447915602375584857L; 119 120 /** The paint used to highlight positive differences (y(0) > y(1)). */ 121 private transient Paint positivePaint; 122 123 /** The paint used to highlight negative differences (y(0) < y(1)). */ 124 private transient Paint negativePaint; 125 126 /** Display shapes at each point? */ 127 private boolean shapesVisible; 128 129 /** The shape to display in the legend item. */ 130 private transient Shape legendLine; 131 132 /** 133 * This flag controls whether or not the x-coordinates (in Java2D space) 134 * are rounded to integers. When set to true, this can avoid the vertical 135 * striping that anti-aliasing can generate. However, the rounding may not 136 * be appropriate for output in high resolution formats (for example, 137 * vector graphics formats such as SVG and PDF). 138 * 139 * @since 1.0.4 140 */ 141 private boolean roundXCoordinates; 142 143 /** 144 * Creates a new renderer with default attributes. 145 */ 146 public XYDifferenceRenderer() { 147 this(Color.green, Color.red, false); 148 } 149 150 /** 151 * Creates a new renderer. 152 * 153 * @param positivePaint the highlight color for positive differences 154 * (<code>null</code> not permitted). 155 * @param negativePaint the highlight color for negative differences 156 * (<code>null</code> not permitted). 157 * @param shapes draw shapes? 158 */ 159 public XYDifferenceRenderer(Paint positivePaint, Paint negativePaint, 160 boolean shapes) { 161 if (positivePaint == null) { 162 throw new IllegalArgumentException( 163 "Null 'positivePaint' argument."); 164 } 165 if (negativePaint == null) { 166 throw new IllegalArgumentException( 167 "Null 'negativePaint' argument."); 168 } 169 this.positivePaint = positivePaint; 170 this.negativePaint = negativePaint; 171 this.shapesVisible = shapes; 172 this.legendLine = new Line2D.Double(-7.0, 0.0, 7.0, 0.0); 173 this.roundXCoordinates = false; 174 } 175 176 /** 177 * Returns the paint used to highlight positive differences. 178 * 179 * @return The paint (never <code>null</code>). 180 * 181 * @see #setPositivePaint(Paint) 182 */ 183 public Paint getPositivePaint() { 184 return this.positivePaint; 185 } 186 187 /** 188 * Sets the paint used to highlight positive differences. 189 * 190 * @param paint the paint (<code>null</code> not permitted). 191 * 192 * @see #getPositivePaint() 193 */ 194 public void setPositivePaint(Paint paint) { 195 if (paint == null) { 196 throw new IllegalArgumentException("Null 'paint' argument."); 197 } 198 this.positivePaint = paint; 199 notifyListeners(new RendererChangeEvent(this)); 200 } 201 202 /** 203 * Returns the paint used to highlight negative differences. 204 * 205 * @return The paint (never <code>null</code>). 206 * 207 * @see #setNegativePaint(Paint) 208 */ 209 public Paint getNegativePaint() { 210 return this.negativePaint; 211 } 212 213 /** 214 * Sets the paint used to highlight negative differences. 215 * 216 * @param paint the paint (<code>null</code> not permitted). 217 * 218 * @see #getNegativePaint() 219 */ 220 public void setNegativePaint(Paint paint) { 221 if (paint == null) { 222 throw new IllegalArgumentException("Null 'paint' argument."); 223 } 224 this.negativePaint = paint; 225 notifyListeners(new RendererChangeEvent(this)); 226 } 227 228 /** 229 * Returns a flag that controls whether or not shapes are drawn for each 230 * data value. 231 * 232 * @return A boolean. 233 * 234 * @see #setShapesVisible(boolean) 235 */ 236 public boolean getShapesVisible() { 237 return this.shapesVisible; 238 } 239 240 /** 241 * Sets a flag that controls whether or not shapes are drawn for each 242 * data value. 243 * 244 * @param flag the flag. 245 * 246 * @see #getShapesVisible() 247 */ 248 public void setShapesVisible(boolean flag) { 249 this.shapesVisible = flag; 250 notifyListeners(new RendererChangeEvent(this)); 251 } 252 253 /** 254 * Returns the shape used to represent a line in the legend. 255 * 256 * @return The legend line (never <code>null</code>). 257 * 258 * @see #setLegendLine(Shape) 259 */ 260 public Shape getLegendLine() { 261 return this.legendLine; 262 } 263 264 /** 265 * Sets the shape used as a line in each legend item and sends a 266 * {@link RendererChangeEvent} to all registered listeners. 267 * 268 * @param line the line (<code>null</code> not permitted). 269 * 270 * @see #getLegendLine() 271 */ 272 public void setLegendLine(Shape line) { 273 if (line == null) { 274 throw new IllegalArgumentException("Null 'line' argument."); 275 } 276 this.legendLine = line; 277 notifyListeners(new RendererChangeEvent(this)); 278 } 279 280 /** 281 * Returns the flag that controls whether or not the x-coordinates (in 282 * Java2D space) are rounded to integer values. 283 * 284 * @return The flag. 285 * 286 * @since 1.0.4 287 * 288 * @see #setRoundXCoordinates(boolean) 289 */ 290 public boolean getRoundXCoordinates() { 291 return this.roundXCoordinates; 292 } 293 294 /** 295 * Sets the flag that controls whether or not the x-coordinates (in 296 * Java2D space) are rounded to integer values, and sends a 297 * {@link RendererChangeEvent} to all registered listeners. 298 * 299 * @param round the new flag value. 300 * 301 * @since 1.0.4 302 * 303 * @see #getRoundXCoordinates() 304 */ 305 public void setRoundXCoordinates(boolean round) { 306 this.roundXCoordinates = round; 307 notifyListeners(new RendererChangeEvent(this)); 308 } 309 310 /** 311 * Initialises the renderer and returns a state object that should be 312 * passed to subsequent calls to the drawItem() method. This method will 313 * be called before the first item is rendered, giving the renderer an 314 * opportunity to initialise any state information it wants to maintain. 315 * The renderer can do nothing if it chooses. 316 * 317 * @param g2 the graphics device. 318 * @param dataArea the area inside the axes. 319 * @param plot the plot. 320 * @param data the data. 321 * @param info an optional info collection object to return data back to 322 * the caller. 323 * 324 * @return A state object. 325 */ 326 public XYItemRendererState initialise(Graphics2D g2, 327 Rectangle2D dataArea, 328 XYPlot plot, 329 XYDataset data, 330 PlotRenderingInfo info) { 331 332 return super.initialise(g2, dataArea, plot, data, info); 333 334 } 335 336 /** 337 * Returns <code>2</code>, the number of passes required by the renderer. 338 * The {@link XYPlot} will run through the dataset this number of times. 339 * 340 * @return The number of passes required by the renderer. 341 */ 342 public int getPassCount() { 343 return 2; 344 } 345 346 /** 347 * Draws the visual representation of a single data item. 348 * 349 * @param g2 the graphics device. 350 * @param state the renderer state. 351 * @param dataArea the area within which the data is being drawn. 352 * @param info collects information about the drawing. 353 * @param plot the plot (can be used to obtain standard color 354 * information etc). 355 * @param domainAxis the domain (horizontal) axis. 356 * @param rangeAxis the range (vertical) axis. 357 * @param dataset the dataset. 358 * @param series the series index (zero-based). 359 * @param item the item index (zero-based). 360 * @param crosshairState crosshair information for the plot 361 * (<code>null</code> permitted). 362 * @param pass the pass index. 363 */ 364 public void drawItem(Graphics2D g2, 365 XYItemRendererState state, 366 Rectangle2D dataArea, 367 PlotRenderingInfo info, 368 XYPlot plot, 369 ValueAxis domainAxis, 370 ValueAxis rangeAxis, 371 XYDataset dataset, 372 int series, 373 int item, 374 CrosshairState crosshairState, 375 int pass) { 376 377 if (pass == 0) { 378 drawItemPass0(g2, dataArea, info, plot, domainAxis, rangeAxis, 379 dataset, series, item, crosshairState); 380 } 381 else if (pass == 1) { 382 drawItemPass1(g2, dataArea, info, plot, domainAxis, rangeAxis, 383 dataset, series, item, crosshairState); 384 } 385 386 } 387 388 /** 389 * Draws the visual representation of a single data item, first pass. 390 * 391 * @param g2 the graphics device. 392 * @param dataArea the area within which the data is being drawn. 393 * @param info collects information about the drawing. 394 * @param plot the plot (can be used to obtain standard color 395 * information etc). 396 * @param domainAxis the domain (horizontal) axis. 397 * @param rangeAxis the range (vertical) axis. 398 * @param dataset the dataset. 399 * @param series the series index (zero-based). 400 * @param item the item index (zero-based). 401 * @param crosshairState crosshair information for the plot 402 * (<code>null</code> permitted). 403 */ 404 protected void drawItemPass0(Graphics2D g2, 405 Rectangle2D dataArea, 406 PlotRenderingInfo info, 407 XYPlot plot, 408 ValueAxis domainAxis, 409 ValueAxis rangeAxis, 410 XYDataset dataset, 411 int series, 412 int item, 413 CrosshairState crosshairState) { 414 415 if (series == 0) { 416 417 PlotOrientation orientation = plot.getOrientation(); 418 RectangleEdge domainAxisLocation = plot.getDomainAxisEdge(); 419 RectangleEdge rangeAxisLocation = plot.getRangeAxisEdge(); 420 421 double y0 = dataset.getYValue(0, item); 422 double x1 = dataset.getXValue(1, item); 423 double y1 = dataset.getYValue(1, item); 424 425 double transY0 = rangeAxis.valueToJava2D(y0, dataArea, 426 rangeAxisLocation); 427 double transX1 = domainAxis.valueToJava2D(x1, dataArea, 428 domainAxisLocation); 429 if (this.roundXCoordinates) { 430 transX1 = Math.rint(transX1); 431 } 432 double transY1 = rangeAxis.valueToJava2D(y1, dataArea, 433 rangeAxisLocation); 434 435 if (item > 0) { 436 double prevx0 = dataset.getXValue(0, item - 1); 437 double prevy0 = dataset.getYValue(0, item - 1); 438 double prevy1 = dataset.getYValue(1, item - 1); 439 440 double prevtransX0 = domainAxis.valueToJava2D(prevx0, dataArea, 441 domainAxisLocation); 442 if (this.roundXCoordinates) { 443 prevtransX0 = Math.rint(prevtransX0); 444 } 445 double prevtransY0 = rangeAxis.valueToJava2D(prevy0, dataArea, 446 rangeAxisLocation); 447 double prevtransY1 = rangeAxis.valueToJava2D(prevy1, dataArea, 448 rangeAxisLocation); 449 450 Shape positive = getPositiveArea((float) prevtransX0, 451 (float) prevtransY0, (float) prevtransY1, 452 (float) transX1, (float) transY0, (float) transY1, 453 orientation); 454 if (positive != null) { 455 g2.setPaint(getPositivePaint()); 456 g2.fill(positive); 457 } 458 459 Shape negative = getNegativeArea((float) prevtransX0, 460 (float) prevtransY0, (float) prevtransY1, 461 (float) transX1, (float) transY0, (float) transY1, 462 orientation); 463 464 if (negative != null) { 465 g2.setPaint(getNegativePaint()); 466 g2.fill(negative); 467 } 468 } 469 } 470 471 } 472 473 /** 474 * Draws the visual representation of a single data item, second pass. In 475 * the second pass, the renderer draws the lines and shapes for the 476 * individual points in the two series. 477 * 478 * @param g2 the graphics device. 479 * @param dataArea the area within which the data is being drawn. 480 * @param info collects information about the drawing. 481 * @param plot the plot (can be used to obtain standard color information 482 * etc). 483 * @param domainAxis the domain (horizontal) axis. 484 * @param rangeAxis the range (vertical) axis. 485 * @param dataset the dataset. 486 * @param series the series index (zero-based). 487 * @param item the item index (zero-based). 488 * @param crosshairState crosshair information for the plot 489 * (<code>null</code> permitted). 490 */ 491 protected void drawItemPass1(Graphics2D g2, 492 Rectangle2D dataArea, 493 PlotRenderingInfo info, 494 XYPlot plot, 495 ValueAxis domainAxis, 496 ValueAxis rangeAxis, 497 XYDataset dataset, 498 int series, 499 int item, 500 CrosshairState crosshairState) { 501 502 Shape entityArea0 = null; 503 Shape entityArea1 = null; 504 EntityCollection entities = null; 505 if (info != null) { 506 entities = info.getOwner().getEntityCollection(); 507 } 508 509 Paint seriesPaint = getItemPaint(series, item); 510 Stroke seriesStroke = getItemStroke(series, item); 511 g2.setPaint(seriesPaint); 512 g2.setStroke(seriesStroke); 513 514 if (series == 0) { 515 516 PlotOrientation orientation = plot.getOrientation(); 517 RectangleEdge domainAxisLocation = plot.getDomainAxisEdge(); 518 RectangleEdge rangeAxisLocation = plot.getRangeAxisEdge(); 519 520 double x0 = dataset.getXValue(0, item); 521 double y0 = dataset.getYValue(0, item); 522 double x1 = dataset.getXValue(1, item); 523 double y1 = dataset.getYValue(1, item); 524 525 double transX0 = domainAxis.valueToJava2D(x0, dataArea, 526 domainAxisLocation); 527 double transY0 = rangeAxis.valueToJava2D(y0, dataArea, 528 rangeAxisLocation); 529 double transX1 = domainAxis.valueToJava2D(x1, dataArea, 530 domainAxisLocation); 531 double transY1 = rangeAxis.valueToJava2D(y1, dataArea, 532 rangeAxisLocation); 533 534 if (item > 0) { 535 // get the previous data points... 536 double prevx0 = dataset.getXValue(0, item - 1); 537 double prevy0 = dataset.getYValue(0, item - 1); 538 double prevx1 = dataset.getXValue(1, item - 1); 539 double prevy1 = dataset.getYValue(1, item - 1); 540 541 double prevtransX0 = domainAxis.valueToJava2D(prevx0, dataArea, 542 domainAxisLocation); 543 double prevtransY0 = rangeAxis.valueToJava2D(prevy0, dataArea, 544 rangeAxisLocation); 545 double prevtransX1 = domainAxis.valueToJava2D(prevx1, dataArea, 546 domainAxisLocation); 547 double prevtransY1 = rangeAxis.valueToJava2D(prevy1, dataArea, 548 rangeAxisLocation); 549 550 Line2D line0 = null; 551 Line2D line1 = null; 552 if (orientation == PlotOrientation.HORIZONTAL) { 553 line0 = new Line2D.Double(transY0, transX0, prevtransY0, 554 prevtransX0); 555 line1 = new Line2D.Double(transY1, transX1, prevtransY1, 556 prevtransX1); 557 } 558 else if (orientation == PlotOrientation.VERTICAL) { 559 line0 = new Line2D.Double(transX0, transY0, prevtransX0, 560 prevtransY0); 561 line1 = new Line2D.Double(transX1, transY1, prevtransX1, 562 prevtransY1); 563 } 564 if (line0 != null && line0.intersects(dataArea)) { 565 g2.setPaint(getItemPaint(series, item)); 566 g2.setStroke(getItemStroke(series, item)); 567 g2.draw(line0); 568 } 569 if (line1 != null && line1.intersects(dataArea)) { 570 g2.setPaint(getItemPaint(1, item)); 571 g2.setStroke(getItemStroke(1, item)); 572 g2.draw(line1); 573 } 574 } 575 576 if (getShapesVisible()) { 577 Shape shape0 = getItemShape(series, item); 578 if (orientation == PlotOrientation.HORIZONTAL) { 579 shape0 = ShapeUtilities.createTranslatedShape(shape0, 580 transY0, transX0); 581 } 582 else { // vertical 583 shape0 = ShapeUtilities.createTranslatedShape(shape0, 584 transX0, transY0); 585 } 586 if (shape0.intersects(dataArea)) { 587 g2.setPaint(getItemPaint(series, item)); 588 g2.fill(shape0); 589 } 590 entityArea0 = shape0; 591 592 Shape shape1 = getItemShape(series + 1, item); 593 if (orientation == PlotOrientation.HORIZONTAL) { 594 shape1 = ShapeUtilities.createTranslatedShape(shape1, 595 transY1, transX1); 596 } 597 else { // vertical 598 shape1 = ShapeUtilities.createTranslatedShape(shape1, 599 transX1, transY1); 600 } 601 if (shape1.intersects(dataArea)) { 602 g2.setPaint(getItemPaint(series + 1, item)); 603 g2.fill(shape1); 604 } 605 entityArea1 = shape1; 606 607 } 608 609 // add an entity for the item... 610 if (entities != null) { 611 if (entityArea0 == null) { 612 entityArea0 = new Rectangle2D.Double(transX0 - 2, 613 transY0 - 2, 4, 4); 614 } 615 String tip = null; 616 XYToolTipGenerator generator = getToolTipGenerator(series, 617 item); 618 if (generator != null) { 619 tip = generator.generateToolTip(dataset, series, item); 620 } 621 String url = null; 622 if (getURLGenerator() != null) { 623 url = getURLGenerator().generateURL(dataset, series, 624 item); 625 } 626 XYItemEntity entity = new XYItemEntity(entityArea0, dataset, 627 series, item, tip, url); 628 entities.add(entity); 629 630 // add an entity for the second item... 631 if (entityArea1 == null) { 632 entityArea1 = new Rectangle2D.Double(transX1 - 2, 633 transY1 - 2, 4, 4); 634 } 635 tip = null; 636 generator = getToolTipGenerator(series, item); 637 if (generator != null) { 638 tip = generator.generateToolTip(dataset, series + 1, item); 639 } 640 url = null; 641 if (getURLGenerator() != null) { 642 url = getURLGenerator().generateURL(dataset, 643 series + 1, item); 644 } 645 entity = new XYItemEntity(entityArea1, dataset, 646 series + 1, item, tip, url); 647 entities.add(entity); 648 } 649 650 int domainAxisIndex = plot.getDomainAxisIndex(domainAxis); 651 int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis); 652 updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex, 653 rangeAxisIndex, transX1, transY1, orientation); 654 updateCrosshairValues(crosshairState, x0, y0, domainAxisIndex, 655 rangeAxisIndex, transX0, transY0, orientation); 656 } 657 658 } 659 660 /** 661 * Returns the positive area for a crossover point. 662 * 663 * @param x0 x coordinate. 664 * @param y0A y coordinate A. 665 * @param y0B y coordinate B. 666 * @param x1 x coordinate. 667 * @param y1A y coordinate A. 668 * @param y1B y coordinate B. 669 * @param orientation the plot orientation. 670 * 671 * @return The positive area. 672 */ 673 protected Shape getPositiveArea(float x0, float y0A, float y0B, 674 float x1, float y1A, float y1B, 675 PlotOrientation orientation) { 676 677 Shape result = null; 678 679 boolean startsNegative = (y0A >= y0B); 680 boolean endsNegative = (y1A >= y1B); 681 if (orientation == PlotOrientation.HORIZONTAL) { 682 startsNegative = (y0B >= y0A); 683 endsNegative = (y1B >= y1A); 684 } 685 686 if (startsNegative) { // starts negative 687 if (endsNegative) { 688 // all negative - return null 689 result = null; 690 } 691 else { 692 // changed from negative to positive 693 float[] p = getIntersection(x0, y0A, x1, y1A, x0, y0B, x1, y1B); 694 GeneralPath area = new GeneralPath(); 695 if (orientation == PlotOrientation.HORIZONTAL) { 696 area.moveTo(y1A, x1); 697 area.lineTo(p[1], p[0]); 698 area.lineTo(y1B, x1); 699 area.closePath(); 700 } 701 else if (orientation == PlotOrientation.VERTICAL) { 702 area.moveTo(x1, y1A); 703 area.lineTo(p[0], p[1]); 704 area.lineTo(x1, y1B); 705 area.closePath(); 706 } 707 result = area; 708 } 709 } 710 else { // starts positive 711 if (endsNegative) { 712 // changed from positive to negative 713 float[] p = getIntersection(x0, y0A, x1, y1A, x0, y0B, x1, y1B); 714 GeneralPath area = new GeneralPath(); 715 if (orientation == PlotOrientation.HORIZONTAL) { 716 area.moveTo(y0A, x0); 717 area.lineTo(p[1], p[0]); 718 area.lineTo(y0B, x0); 719 area.closePath(); 720 } 721 else if (orientation == PlotOrientation.VERTICAL) { 722 area.moveTo(x0, y0A); 723 area.lineTo(p[0], p[1]); 724 area.lineTo(x0, y0B); 725 area.closePath(); 726 } 727 result = area; 728 729 } 730 else { 731 GeneralPath area = new GeneralPath(); 732 if (orientation == PlotOrientation.HORIZONTAL) { 733 area.moveTo(y0A, x0); 734 area.lineTo(y1A, x1); 735 area.lineTo(y1B, x1); 736 area.lineTo(y0B, x0); 737 area.closePath(); 738 } 739 else if (orientation == PlotOrientation.VERTICAL) { 740 area.moveTo(x0, y0A); 741 area.lineTo(x1, y1A); 742 area.lineTo(x1, y1B); 743 area.lineTo(x0, y0B); 744 area.closePath(); 745 } 746 result = area; 747 } 748 749 } 750 751 return result; 752 753 } 754 755 /** 756 * Returns the negative area for a cross-over section. 757 * 758 * @param x0 x coordinate. 759 * @param y0A y coordinate A. 760 * @param y0B y coordinate B. 761 * @param x1 x coordinate. 762 * @param y1A y coordinate A. 763 * @param y1B y coordinate B. 764 * @param orientation the plot orientation. 765 * 766 * @return The negative area. 767 */ 768 protected Shape getNegativeArea(float x0, float y0A, float y0B, 769 float x1, float y1A, float y1B, 770 PlotOrientation orientation) { 771 772 Shape result = null; 773 774 boolean startsNegative = (y0A >= y0B); 775 boolean endsNegative = (y1A >= y1B); 776 if (orientation == PlotOrientation.HORIZONTAL) { 777 startsNegative = (y0B >= y0A); 778 endsNegative = (y1B >= y1A); 779 } 780 if (startsNegative) { // starts negative 781 if (endsNegative) { // all negative 782 GeneralPath area = new GeneralPath(); 783 if (orientation == PlotOrientation.HORIZONTAL) { 784 area.moveTo(y0A, x0); 785 area.lineTo(y1A, x1); 786 area.lineTo(y1B, x1); 787 area.lineTo(y0B, x0); 788 area.closePath(); 789 } 790 else if (orientation == PlotOrientation.VERTICAL) { 791 area.moveTo(x0, y0A); 792 area.lineTo(x1, y1A); 793 area.lineTo(x1, y1B); 794 area.lineTo(x0, y0B); 795 area.closePath(); 796 } 797 result = area; 798 } 799 else { // changed from negative to positive 800 float[] p = getIntersection(x0, y0A, x1, y1A, x0, y0B, x1, y1B); 801 GeneralPath area = new GeneralPath(); 802 if (orientation == PlotOrientation.HORIZONTAL) { 803 area.moveTo(y0A, x0); 804 area.lineTo(p[1], p[0]); 805 area.lineTo(y0B, x0); 806 area.closePath(); 807 } 808 else if (orientation == PlotOrientation.VERTICAL) { 809 area.moveTo(x0, y0A); 810 area.lineTo(p[0], p[1]); 811 area.lineTo(x0, y0B); 812 area.closePath(); 813 } 814 result = area; 815 } 816 } 817 else { 818 if (endsNegative) { 819 // changed from positive to negative 820 float[] p = getIntersection(x0, y0A, x1, y1A, x0, y0B, x1, y1B); 821 GeneralPath area = new GeneralPath(); 822 if (orientation == PlotOrientation.HORIZONTAL) { 823 area.moveTo(y1A, x1); 824 area.lineTo(p[1], p[0]); 825 area.lineTo(y1B, x1); 826 area.closePath(); 827 } 828 else if (orientation == PlotOrientation.VERTICAL) { 829 area.moveTo(x1, y1A); 830 area.lineTo(p[0], p[1]); 831 area.lineTo(x1, y1B); 832 area.closePath(); 833 } 834 result = area; 835 } 836 else { 837 // all negative - return null 838 } 839 840 } 841 842 return result; 843 844 } 845 846 /** 847 * Returns the intersection point of two lines. 848 * 849 * @param x1 x1 850 * @param y1 y1 851 * @param x2 x2 852 * @param y2 y2 853 * @param x3 x3 854 * @param y3 y3 855 * @param x4 x4 856 * @param y4 y4 857 * 858 * @return The intersection point. 859 */ 860 private float[] getIntersection(float x1, float y1, float x2, float y2, 861 float x3, float y3, float x4, float y4) { 862 863 float n = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3); 864 float d = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1); 865 float u = n / d; 866 867 float[] result = new float[2]; 868 result[0] = x1 + u * (x2 - x1); 869 result[1] = y1 + u * (y2 - y1); 870 return result; 871 872 } 873 874 /** 875 * Returns a default legend item for the specified series. Subclasses 876 * should override this method to generate customised items. 877 * 878 * @param datasetIndex the dataset index (zero-based). 879 * @param series the series index (zero-based). 880 * 881 * @return A legend item for the series. 882 */ 883 public LegendItem getLegendItem(int datasetIndex, int series) { 884 LegendItem result = null; 885 XYPlot p = getPlot(); 886 if (p != null) { 887 XYDataset dataset = p.getDataset(datasetIndex); 888 if (dataset != null) { 889 if (getItemVisible(series, 0)) { 890 String label = getLegendItemLabelGenerator().generateLabel( 891 dataset, series); 892 String description = label; 893 String toolTipText = null; 894 if (getLegendItemToolTipGenerator() != null) { 895 toolTipText 896 = getLegendItemToolTipGenerator().generateLabel( 897 dataset, series); 898 } 899 String urlText = null; 900 if (getLegendItemURLGenerator() != null) { 901 urlText = getLegendItemURLGenerator().generateLabel( 902 dataset, series); 903 } 904 Paint paint = getSeriesPaint(series); 905 Stroke stroke = getSeriesStroke(series); 906 // TODO: the following hard-coded line needs generalising 907 Line2D line = new Line2D.Double(-7.0, 0.0, 7.0, 0.0); 908 result = new LegendItem(label, description, 909 toolTipText, urlText, line, stroke, paint); 910 } 911 } 912 913 } 914 915 return result; 916 917 } 918 919 /** 920 * Tests this renderer for equality with an arbitrary object. 921 * 922 * @param obj the object (<code>null</code> permitted). 923 * 924 * @return A boolean. 925 */ 926 public boolean equals(Object obj) { 927 if (obj == this) { 928 return true; 929 } 930 if (!(obj instanceof XYDifferenceRenderer)) { 931 return false; 932 } 933 if (!super.equals(obj)) { 934 return false; 935 } 936 XYDifferenceRenderer that = (XYDifferenceRenderer) obj; 937 if (!PaintUtilities.equal(this.positivePaint, that.positivePaint)) { 938 return false; 939 } 940 if (!PaintUtilities.equal(this.negativePaint, that.negativePaint)) { 941 return false; 942 } 943 if (this.shapesVisible != that.shapesVisible) { 944 return false; 945 } 946 if (!ShapeUtilities.equal(this.legendLine, that.legendLine)) { 947 return false; 948 } 949 if (this.roundXCoordinates != that.roundXCoordinates) { 950 return false; 951 } 952 return true; 953 } 954 955 /** 956 * Returns a clone of the renderer. 957 * 958 * @return A clone. 959 * 960 * @throws CloneNotSupportedException if the renderer cannot be cloned. 961 */ 962 public Object clone() throws CloneNotSupportedException { 963 XYDifferenceRenderer clone = (XYDifferenceRenderer) super.clone(); 964 clone.legendLine = ShapeUtilities.clone(this.legendLine); 965 return clone; 966 } 967 968 /** 969 * Provides serialization support. 970 * 971 * @param stream the output stream. 972 * 973 * @throws IOException if there is an I/O error. 974 */ 975 private void writeObject(ObjectOutputStream stream) throws IOException { 976 stream.defaultWriteObject(); 977 SerialUtilities.writePaint(this.positivePaint, stream); 978 SerialUtilities.writePaint(this.negativePaint, stream); 979 SerialUtilities.writeShape(this.legendLine, stream); 980 } 981 982 /** 983 * Provides serialization support. 984 * 985 * @param stream the input stream. 986 * 987 * @throws IOException if there is an I/O error. 988 * @throws ClassNotFoundException if there is a classpath problem. 989 */ 990 private void readObject(ObjectInputStream stream) 991 throws IOException, ClassNotFoundException { 992 stream.defaultReadObject(); 993 this.positivePaint = SerialUtilities.readPaint(stream); 994 this.negativePaint = SerialUtilities.readPaint(stream); 995 this.legendLine = SerialUtilities.readShape(stream); 996 } 997 998 }