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