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 * StandardXYItemRenderer.java 029 * --------------------------- 030 * (C) Copyright 2001-2007, by Object Refinery Limited and Contributors. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): Mark Watson (www.markwatson.com); 034 * Jonathan Nash; 035 * Andreas Schneider; 036 * Norbert Kiesel (for TBD Networks); 037 * Christian W. Zuckschwerdt; 038 * Bill Kelemen; 039 * Nicolas Brodu (for Astrium and EADS Corporate Research 040 * Center); 041 * 042 * Changes: 043 * -------- 044 * 19-Oct-2001 : Version 1, based on code by Mark Watson (DG); 045 * 22-Oct-2001 : Renamed DataSource.java --> Dataset.java etc. (DG); 046 * 21-Dec-2001 : Added working line instance to improve performance (DG); 047 * 22-Jan-2002 : Added code to lock crosshairs to data points. Based on code 048 * by Jonathan Nash (DG); 049 * 23-Jan-2002 : Added DrawInfo parameter to drawItem() method (DG); 050 * 28-Mar-2002 : Added a property change listener mechanism so that the 051 * renderer no longer needs to be immutable (DG); 052 * 02-Apr-2002 : Modified to handle null values (DG); 053 * 09-Apr-2002 : Modified draw method to return void. Removed the translated 054 * zero from the drawItem method. Override the initialise() 055 * method to calculate it (DG); 056 * 13-May-2002 : Added code from Andreas Schneider to allow changing 057 * shapes/colors per item (DG); 058 * 24-May-2002 : Incorporated tooltips into chart entities (DG); 059 * 25-Jun-2002 : Removed redundant code (DG); 060 * 05-Aug-2002 : Incorporated URLs for HTML image maps into chart entities (RA); 061 * 08-Aug-2002 : Added discontinuous lines option contributed by 062 * Norbert Kiesel (DG); 063 * 20-Aug-2002 : Added user definable default values to be returned by 064 * protected methods unless overridden by a subclass (DG); 065 * 23-Sep-2002 : Updated for changes in the XYItemRenderer interface (DG); 066 * 02-Oct-2002 : Fixed errors reported by Checkstyle (DG); 067 * 25-Mar-2003 : Implemented Serializable (DG); 068 * 01-May-2003 : Modified drawItem() method signature (DG); 069 * 15-May-2003 : Modified to take into account the plot orientation (DG); 070 * 29-Jul-2003 : Amended code that doesn't compile with JDK 1.2.2 (DG); 071 * 30-Jul-2003 : Modified entity constructor (CZ); 072 * 20-Aug-2003 : Implemented Cloneable and PublicCloneable (DG); 073 * 24-Aug-2003 : Added null/NaN checks in drawItem (BK); 074 * 08-Sep-2003 : Fixed serialization (NB); 075 * 16-Sep-2003 : Changed ChartRenderingInfo --> PlotRenderingInfo (DG); 076 * 21-Jan-2004 : Override for getLegendItem() method (DG); 077 * 27-Jan-2004 : Moved working line into state object (DG); 078 * 10-Feb-2004 : Changed drawItem() method to make cut-and-paste overriding 079 * easier (DG); 080 * 25-Feb-2004 : Replaced CrosshairInfo with CrosshairState. Renamed 081 * XYToolTipGenerator --> XYItemLabelGenerator (DG); 082 * 08-Jun-2004 : Modified to use getX() and getY() methods (DG); 083 * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 084 * getYValue() (DG); 085 * 25-Aug-2004 : Created addEntity() method in superclass (DG); 086 * 08-Oct-2004 : Added 'gapThresholdType' as suggested by Mike Watts (DG); 087 * 11-Nov-2004 : Now uses ShapeUtilities to translate shapes (DG); 088 * 23-Feb-2005 : Fixed getLegendItem() method to show lines. Fixed bug 089 * 1077108 (shape not visible for first item in series) (DG); 090 * 10-Apr-2005 : Fixed item label positioning with horizontal orientation (DG); 091 * 20-Apr-2005 : Use generators for legend tooltips and URLs (DG); 092 * 27-Apr-2005 : Use generator for series label in legend (DG); 093 * ------------- JFREECHART 1.0.x --------------------------------------------- 094 * 15-Jun-2006 : Fixed bug (1380480) for rendering series as path (DG); 095 * 06-Feb-2007 : Fixed bug 1086307, crosshairs with multiple axes (DG); 096 * 14-Mar-2007 : Fixed problems with the equals() and clone() methods (DG); 097 * 23-Mar-2007 : Clean-up of shapesFilled attributes (DG); 098 * 20-Apr-2007 : Updated getLegendItem() and drawItem() for renderer 099 * change (DG); 100 * 17-May-2007 : Set datasetIndex and seriesIndex in getLegendItem() 101 * method (DG); 102 * 18-May-2007 : Set dataset and seriesKey for LegendItem (DG); 103 * 08-Jun-2007 : Fixed bug in entity creation (DG); 104 * 21-Nov-2007 : Deprecated override flag methods (DG); 105 * 106 */ 107 108 package org.jfree.chart.renderer.xy; 109 110 import java.awt.Graphics2D; 111 import java.awt.Image; 112 import java.awt.Paint; 113 import java.awt.Point; 114 import java.awt.Shape; 115 import java.awt.Stroke; 116 import java.awt.geom.GeneralPath; 117 import java.awt.geom.Line2D; 118 import java.awt.geom.Rectangle2D; 119 import java.io.IOException; 120 import java.io.ObjectInputStream; 121 import java.io.ObjectOutputStream; 122 import java.io.Serializable; 123 124 import org.jfree.chart.LegendItem; 125 import org.jfree.chart.axis.ValueAxis; 126 import org.jfree.chart.entity.EntityCollection; 127 import org.jfree.chart.event.RendererChangeEvent; 128 import org.jfree.chart.labels.XYToolTipGenerator; 129 import org.jfree.chart.plot.CrosshairState; 130 import org.jfree.chart.plot.Plot; 131 import org.jfree.chart.plot.PlotOrientation; 132 import org.jfree.chart.plot.PlotRenderingInfo; 133 import org.jfree.chart.plot.XYPlot; 134 import org.jfree.chart.urls.XYURLGenerator; 135 import org.jfree.data.xy.XYDataset; 136 import org.jfree.io.SerialUtilities; 137 import org.jfree.ui.RectangleEdge; 138 import org.jfree.util.BooleanList; 139 import org.jfree.util.BooleanUtilities; 140 import org.jfree.util.ObjectUtilities; 141 import org.jfree.util.PublicCloneable; 142 import org.jfree.util.ShapeUtilities; 143 import org.jfree.util.UnitType; 144 145 /** 146 * Standard item renderer for an {@link XYPlot}. This class can draw (a) 147 * shapes at each point, or (b) lines between points, or (c) both shapes and 148 * lines. 149 * <P> 150 * This renderer has been retained for historical reasons and, in general, you 151 * should use the {@link XYLineAndShapeRenderer} class instead. 152 */ 153 public class StandardXYItemRenderer extends AbstractXYItemRenderer 154 implements XYItemRenderer, Cloneable, PublicCloneable, Serializable { 155 156 /** For serialization. */ 157 private static final long serialVersionUID = -3271351259436865995L; 158 159 /** Constant for the type of rendering (shapes only). */ 160 public static final int SHAPES = 1; 161 162 /** Constant for the type of rendering (lines only). */ 163 public static final int LINES = 2; 164 165 /** Constant for the type of rendering (shapes and lines). */ 166 public static final int SHAPES_AND_LINES = SHAPES | LINES; 167 168 /** Constant for the type of rendering (images only). */ 169 public static final int IMAGES = 4; 170 171 /** Constant for the type of rendering (discontinuous lines). */ 172 public static final int DISCONTINUOUS = 8; 173 174 /** Constant for the type of rendering (discontinuous lines). */ 175 public static final int DISCONTINUOUS_LINES = LINES | DISCONTINUOUS; 176 177 /** A flag indicating whether or not shapes are drawn at each XY point. */ 178 private boolean baseShapesVisible; 179 180 /** A flag indicating whether or not lines are drawn between XY points. */ 181 private boolean plotLines; 182 183 /** A flag indicating whether or not images are drawn between XY points. */ 184 private boolean plotImages; 185 186 /** A flag controlling whether or not discontinuous lines are used. */ 187 private boolean plotDiscontinuous; 188 189 /** Specifies how the gap threshold value is interpreted. */ 190 private UnitType gapThresholdType = UnitType.RELATIVE; 191 192 /** Threshold for deciding when to discontinue a line. */ 193 private double gapThreshold = 1.0; 194 195 /** 196 * A flag that controls whether or not shapes are filled for ALL series. 197 * 198 * @deprecated As of 1.0.8, this override should not be used. 199 */ 200 private Boolean shapesFilled; 201 202 /** 203 * A table of flags that control (per series) whether or not shapes are 204 * filled. 205 */ 206 private BooleanList seriesShapesFilled; 207 208 /** The default value returned by the getShapeFilled() method. */ 209 private boolean baseShapesFilled; 210 211 /** 212 * A flag that controls whether or not each series is drawn as a single 213 * path. 214 */ 215 private boolean drawSeriesLineAsPath; 216 217 /** 218 * The shape that is used to represent a line in the legend. 219 * This should never be set to <code>null</code>. 220 */ 221 private transient Shape legendLine; 222 223 /** 224 * Constructs a new renderer. 225 */ 226 public StandardXYItemRenderer() { 227 this(LINES, null); 228 } 229 230 /** 231 * Constructs a new renderer. To specify the type of renderer, use one of 232 * the constants: {@link #SHAPES}, {@link #LINES} or 233 * {@link #SHAPES_AND_LINES}. 234 * 235 * @param type the type. 236 */ 237 public StandardXYItemRenderer(int type) { 238 this(type, null); 239 } 240 241 /** 242 * Constructs a new renderer. To specify the type of renderer, use one of 243 * the constants: {@link #SHAPES}, {@link #LINES} or 244 * {@link #SHAPES_AND_LINES}. 245 * 246 * @param type the type of renderer. 247 * @param toolTipGenerator the item label generator (<code>null</code> 248 * permitted). 249 */ 250 public StandardXYItemRenderer(int type, 251 XYToolTipGenerator toolTipGenerator) { 252 this(type, toolTipGenerator, null); 253 } 254 255 /** 256 * Constructs a new renderer. To specify the type of renderer, use one of 257 * the constants: {@link #SHAPES}, {@link #LINES} or 258 * {@link #SHAPES_AND_LINES}. 259 * 260 * @param type the type of renderer. 261 * @param toolTipGenerator the item label generator (<code>null</code> 262 * permitted). 263 * @param urlGenerator the URL generator. 264 */ 265 public StandardXYItemRenderer(int type, 266 XYToolTipGenerator toolTipGenerator, 267 XYURLGenerator urlGenerator) { 268 269 super(); 270 setBaseToolTipGenerator(toolTipGenerator); 271 setURLGenerator(urlGenerator); 272 if ((type & SHAPES) != 0) { 273 this.baseShapesVisible = true; 274 } 275 if ((type & LINES) != 0) { 276 this.plotLines = true; 277 } 278 if ((type & IMAGES) != 0) { 279 this.plotImages = true; 280 } 281 if ((type & DISCONTINUOUS) != 0) { 282 this.plotDiscontinuous = true; 283 } 284 285 this.shapesFilled = null; 286 this.seriesShapesFilled = new BooleanList(); 287 this.baseShapesFilled = true; 288 this.legendLine = new Line2D.Double(-7.0, 0.0, 7.0, 0.0); 289 this.drawSeriesLineAsPath = false; 290 } 291 292 /** 293 * Returns true if shapes are being plotted by the renderer. 294 * 295 * @return <code>true</code> if shapes are being plotted by the renderer. 296 * 297 * @see #setBaseShapesVisible 298 */ 299 public boolean getBaseShapesVisible() { 300 return this.baseShapesVisible; 301 } 302 303 /** 304 * Sets the flag that controls whether or not a shape is plotted at each 305 * data point. 306 * 307 * @param flag the flag. 308 * 309 * @see #getBaseShapesVisible 310 */ 311 public void setBaseShapesVisible(boolean flag) { 312 if (this.baseShapesVisible != flag) { 313 this.baseShapesVisible = flag; 314 fireChangeEvent(); 315 } 316 } 317 318 // SHAPES FILLED 319 320 /** 321 * Returns the flag used to control whether or not the shape for an item is 322 * filled. 323 * <p> 324 * The default implementation passes control to the 325 * <code>getSeriesShapesFilled</code> method. You can override this method 326 * if you require different behaviour. 327 * 328 * @param series the series index (zero-based). 329 * @param item the item index (zero-based). 330 * 331 * @return A boolean. 332 * 333 * @see #getSeriesShapesFilled(int) 334 */ 335 public boolean getItemShapeFilled(int series, int item) { 336 // return the overall setting, if there is one... 337 if (this.shapesFilled != null) { 338 return this.shapesFilled.booleanValue(); 339 } 340 341 // otherwise look up the paint table 342 Boolean flag = this.seriesShapesFilled.getBoolean(series); 343 if (flag != null) { 344 return flag.booleanValue(); 345 } 346 else { 347 return this.baseShapesFilled; 348 } 349 } 350 351 /** 352 * Returns the override flag that controls whether or not shapes are filled 353 * for ALL series. 354 * 355 * @return The flag (possibly <code>null</code>). 356 * 357 * @since 1.0.5 358 * 359 * @deprecated As of 1.0.8, you should avoid using this method and rely 360 * on just the per-series ({@link #getSeriesShapesFilled(int)}) 361 * and base-level ({@link #getBaseShapesFilled()}) settings. 362 */ 363 public Boolean getShapesFilled() { 364 return this.shapesFilled; 365 } 366 367 /** 368 * Sets the override flag that controls whether or not shapes are filled 369 * for ALL series and sends a {@link RendererChangeEvent} to all registered 370 * listeners. 371 * 372 * @param filled the flag. 373 * 374 * @see #setShapesFilled(Boolean) 375 * 376 * @deprecated As of 1.0.8, you should avoid using this method and rely 377 * on just the per-series ({@link #setSeriesShapesFilled(int, 378 * Boolean)}) and base-level ({@link #setBaseShapesVisible( 379 * boolean)}) settings. 380 */ 381 public void setShapesFilled(boolean filled) { 382 // here we use BooleanUtilities to remain compatible with JDKs < 1.4 383 setShapesFilled(BooleanUtilities.valueOf(filled)); 384 } 385 386 /** 387 * Sets the override flag that controls whether or not shapes are filled 388 * for ALL series and sends a {@link RendererChangeEvent} to all registered 389 * listeners. 390 * 391 * @param filled the flag (<code>null</code> permitted). 392 * 393 * @see #setShapesFilled(boolean) 394 * 395 * @deprecated As of 1.0.8, you should avoid using this method and rely 396 * on just the per-series ({@link #setSeriesShapesFilled(int, 397 * Boolean)}) and base-level ({@link #setBaseShapesVisible( 398 * boolean)}) settings. 399 */ 400 public void setShapesFilled(Boolean filled) { 401 this.shapesFilled = filled; 402 fireChangeEvent(); 403 } 404 405 /** 406 * Returns the flag used to control whether or not the shapes for a series 407 * are filled. 408 * 409 * @param series the series index (zero-based). 410 * 411 * @return A boolean. 412 */ 413 public Boolean getSeriesShapesFilled(int series) { 414 return this.seriesShapesFilled.getBoolean(series); 415 } 416 417 /** 418 * Sets the 'shapes filled' flag for a series and sends a 419 * {@link RendererChangeEvent} to all registered listeners. 420 * 421 * @param series the series index (zero-based). 422 * @param flag the flag. 423 * 424 * @see #getSeriesShapesFilled(int) 425 */ 426 public void setSeriesShapesFilled(int series, Boolean flag) { 427 this.seriesShapesFilled.setBoolean(series, flag); 428 fireChangeEvent(); 429 } 430 431 /** 432 * Returns the base 'shape filled' attribute. 433 * 434 * @return The base flag. 435 * 436 * @see #setBaseShapesFilled(boolean) 437 */ 438 public boolean getBaseShapesFilled() { 439 return this.baseShapesFilled; 440 } 441 442 /** 443 * Sets the base 'shapes filled' flag and sends a 444 * {@link RendererChangeEvent} to all registered listeners. 445 * 446 * @param flag the flag. 447 * 448 * @see #getBaseShapesFilled() 449 */ 450 public void setBaseShapesFilled(boolean flag) { 451 this.baseShapesFilled = flag; 452 } 453 454 /** 455 * Returns true if lines are being plotted by the renderer. 456 * 457 * @return <code>true</code> if lines are being plotted by the renderer. 458 * 459 * @see #setPlotLines(boolean) 460 */ 461 public boolean getPlotLines() { 462 return this.plotLines; 463 } 464 465 /** 466 * Sets the flag that controls whether or not a line is plotted between 467 * each data point and sends a {@link RendererChangeEvent} to all 468 * registered listeners. 469 * 470 * @param flag the flag. 471 * 472 * @see #getPlotLines() 473 */ 474 public void setPlotLines(boolean flag) { 475 if (this.plotLines != flag) { 476 this.plotLines = flag; 477 fireChangeEvent(); 478 } 479 } 480 481 /** 482 * Returns the gap threshold type (relative or absolute). 483 * 484 * @return The type. 485 * 486 * @see #setGapThresholdType(UnitType) 487 */ 488 public UnitType getGapThresholdType() { 489 return this.gapThresholdType; 490 } 491 492 /** 493 * Sets the gap threshold type and sends a {@link RendererChangeEvent} to 494 * all registered listeners. 495 * 496 * @param thresholdType the type (<code>null</code> not permitted). 497 * 498 * @see #getGapThresholdType() 499 */ 500 public void setGapThresholdType(UnitType thresholdType) { 501 if (thresholdType == null) { 502 throw new IllegalArgumentException( 503 "Null 'thresholdType' argument."); 504 } 505 this.gapThresholdType = thresholdType; 506 fireChangeEvent(); 507 } 508 509 /** 510 * Returns the gap threshold for discontinuous lines. 511 * 512 * @return The gap threshold. 513 * 514 * @see #setGapThreshold(double) 515 */ 516 public double getGapThreshold() { 517 return this.gapThreshold; 518 } 519 520 /** 521 * Sets the gap threshold for discontinuous lines and sends a 522 * {@link RendererChangeEvent} to all registered listeners. 523 * 524 * @param t the threshold. 525 * 526 * @see #getGapThreshold() 527 */ 528 public void setGapThreshold(double t) { 529 this.gapThreshold = t; 530 fireChangeEvent(); 531 } 532 533 /** 534 * Returns true if images are being plotted by the renderer. 535 * 536 * @return <code>true</code> if images are being plotted by the renderer. 537 * 538 * @see #setPlotImages(boolean) 539 */ 540 public boolean getPlotImages() { 541 return this.plotImages; 542 } 543 544 /** 545 * Sets the flag that controls whether or not an image is drawn at each 546 * data point and sends a {@link RendererChangeEvent} to all registered 547 * listeners. 548 * 549 * @param flag the flag. 550 * 551 * @see #getPlotImages() 552 */ 553 public void setPlotImages(boolean flag) { 554 if (this.plotImages != flag) { 555 this.plotImages = flag; 556 fireChangeEvent(); 557 } 558 } 559 560 /** 561 * Returns a flag that controls whether or not the renderer shows 562 * discontinuous lines. 563 * 564 * @return <code>true</code> if lines should be discontinuous. 565 */ 566 public boolean getPlotDiscontinuous() { 567 return this.plotDiscontinuous; 568 } 569 570 /** 571 * Sets the flag that controls whether or not the renderer shows 572 * discontinuous lines, and sends a {@link RendererChangeEvent} to all 573 * registered listeners. 574 * 575 * @param flag the new flag value. 576 * 577 * @since 1.0.5 578 */ 579 public void setPlotDiscontinuous(boolean flag) { 580 if (this.plotDiscontinuous != flag) { 581 this.plotDiscontinuous = flag; 582 fireChangeEvent(); 583 } 584 } 585 586 /** 587 * Returns a flag that controls whether or not each series is drawn as a 588 * single path. 589 * 590 * @return A boolean. 591 * 592 * @see #setDrawSeriesLineAsPath(boolean) 593 */ 594 public boolean getDrawSeriesLineAsPath() { 595 return this.drawSeriesLineAsPath; 596 } 597 598 /** 599 * Sets the flag that controls whether or not each series is drawn as a 600 * single path. 601 * 602 * @param flag the flag. 603 * 604 * @see #getDrawSeriesLineAsPath() 605 */ 606 public void setDrawSeriesLineAsPath(boolean flag) { 607 this.drawSeriesLineAsPath = flag; 608 } 609 610 /** 611 * Returns the shape used to represent a line in the legend. 612 * 613 * @return The legend line (never <code>null</code>). 614 * 615 * @see #setLegendLine(Shape) 616 */ 617 public Shape getLegendLine() { 618 return this.legendLine; 619 } 620 621 /** 622 * Sets the shape used as a line in each legend item and sends a 623 * {@link RendererChangeEvent} to all registered listeners. 624 * 625 * @param line the line (<code>null</code> not permitted). 626 * 627 * @see #getLegendLine() 628 */ 629 public void setLegendLine(Shape line) { 630 if (line == null) { 631 throw new IllegalArgumentException("Null 'line' argument."); 632 } 633 this.legendLine = line; 634 fireChangeEvent(); 635 } 636 637 /** 638 * Returns a legend item for a series. 639 * 640 * @param datasetIndex the dataset index (zero-based). 641 * @param series the series index (zero-based). 642 * 643 * @return A legend item for the series. 644 */ 645 public LegendItem getLegendItem(int datasetIndex, int series) { 646 XYPlot plot = getPlot(); 647 if (plot == null) { 648 return null; 649 } 650 LegendItem result = null; 651 XYDataset dataset = plot.getDataset(datasetIndex); 652 if (dataset != null) { 653 if (getItemVisible(series, 0)) { 654 String label = getLegendItemLabelGenerator().generateLabel( 655 dataset, series); 656 String description = label; 657 String toolTipText = null; 658 if (getLegendItemToolTipGenerator() != null) { 659 toolTipText = getLegendItemToolTipGenerator().generateLabel( 660 dataset, series); 661 } 662 String urlText = null; 663 if (getLegendItemURLGenerator() != null) { 664 urlText = getLegendItemURLGenerator().generateLabel( 665 dataset, series); 666 } 667 Shape shape = lookupSeriesShape(series); 668 boolean shapeFilled = getItemShapeFilled(series, 0); 669 Paint paint = lookupSeriesPaint(series); 670 Paint linePaint = paint; 671 Stroke lineStroke = lookupSeriesStroke(series); 672 result = new LegendItem(label, description, toolTipText, 673 urlText, this.baseShapesVisible, shape, shapeFilled, 674 paint, !shapeFilled, paint, lineStroke, 675 this.plotLines, this.legendLine, lineStroke, linePaint); 676 result.setDataset(dataset); 677 result.setDatasetIndex(datasetIndex); 678 result.setSeriesKey(dataset.getSeriesKey(series)); 679 result.setSeriesIndex(series); 680 } 681 } 682 return result; 683 } 684 685 /** 686 * Records the state for the renderer. This is used to preserve state 687 * information between calls to the drawItem() method for a single chart 688 * drawing. 689 */ 690 public static class State extends XYItemRendererState { 691 692 /** The path for the current series. */ 693 public GeneralPath seriesPath; 694 695 /** The series index. */ 696 private int seriesIndex; 697 698 /** 699 * A flag that indicates if the last (x, y) point was 'good' 700 * (non-null). 701 */ 702 private boolean lastPointGood; 703 704 /** 705 * Creates a new state instance. 706 * 707 * @param info the plot rendering info. 708 */ 709 public State(PlotRenderingInfo info) { 710 super(info); 711 } 712 713 /** 714 * Returns a flag that indicates if the last point drawn (in the 715 * current series) was 'good' (non-null). 716 * 717 * @return A boolean. 718 */ 719 public boolean isLastPointGood() { 720 return this.lastPointGood; 721 } 722 723 /** 724 * Sets a flag that indicates if the last point drawn (in the current 725 * series) was 'good' (non-null). 726 * 727 * @param good the flag. 728 */ 729 public void setLastPointGood(boolean good) { 730 this.lastPointGood = good; 731 } 732 733 /** 734 * Returns the series index for the current path. 735 * 736 * @return The series index for the current path. 737 */ 738 public int getSeriesIndex() { 739 return this.seriesIndex; 740 } 741 742 /** 743 * Sets the series index for the current path. 744 * 745 * @param index the index. 746 */ 747 public void setSeriesIndex(int index) { 748 this.seriesIndex = index; 749 } 750 } 751 752 /** 753 * Initialises the renderer. 754 * <P> 755 * This method will be called before the first item is rendered, giving the 756 * renderer an opportunity to initialise any state information it wants to 757 * maintain. The renderer can do nothing if it chooses. 758 * 759 * @param g2 the graphics device. 760 * @param dataArea the area inside the axes. 761 * @param plot the plot. 762 * @param data the data. 763 * @param info an optional info collection object to return data back to 764 * the caller. 765 * 766 * @return The renderer state. 767 */ 768 public XYItemRendererState initialise(Graphics2D g2, 769 Rectangle2D dataArea, 770 XYPlot plot, 771 XYDataset data, 772 PlotRenderingInfo info) { 773 774 State state = new State(info); 775 state.seriesPath = new GeneralPath(); 776 state.seriesIndex = -1; 777 return state; 778 779 } 780 781 /** 782 * Draws the visual representation of a single data item. 783 * 784 * @param g2 the graphics device. 785 * @param state the renderer state. 786 * @param dataArea the area within which the data is being drawn. 787 * @param info collects information about the drawing. 788 * @param plot the plot (can be used to obtain standard color information 789 * etc). 790 * @param domainAxis the domain axis. 791 * @param rangeAxis the range axis. 792 * @param dataset the dataset. 793 * @param series the series index (zero-based). 794 * @param item the item index (zero-based). 795 * @param crosshairState crosshair information for the plot 796 * (<code>null</code> permitted). 797 * @param pass the pass index. 798 */ 799 public void drawItem(Graphics2D g2, 800 XYItemRendererState state, 801 Rectangle2D dataArea, 802 PlotRenderingInfo info, 803 XYPlot plot, 804 ValueAxis domainAxis, 805 ValueAxis rangeAxis, 806 XYDataset dataset, 807 int series, 808 int item, 809 CrosshairState crosshairState, 810 int pass) { 811 812 boolean itemVisible = getItemVisible(series, item); 813 814 // setup for collecting optional entity info... 815 Shape entityArea = null; 816 EntityCollection entities = null; 817 if (info != null) { 818 entities = info.getOwner().getEntityCollection(); 819 } 820 821 PlotOrientation orientation = plot.getOrientation(); 822 Paint paint = getItemPaint(series, item); 823 Stroke seriesStroke = getItemStroke(series, item); 824 g2.setPaint(paint); 825 g2.setStroke(seriesStroke); 826 827 // get the data point... 828 double x1 = dataset.getXValue(series, item); 829 double y1 = dataset.getYValue(series, item); 830 if (Double.isNaN(x1) || Double.isNaN(y1)) { 831 itemVisible = false; 832 } 833 834 RectangleEdge xAxisLocation = plot.getDomainAxisEdge(); 835 RectangleEdge yAxisLocation = plot.getRangeAxisEdge(); 836 double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation); 837 double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation); 838 839 if (getPlotLines()) { 840 if (this.drawSeriesLineAsPath) { 841 State s = (State) state; 842 if (s.getSeriesIndex() != series) { 843 // we are starting a new series path 844 s.seriesPath.reset(); 845 s.lastPointGood = false; 846 s.setSeriesIndex(series); 847 } 848 849 // update path to reflect latest point 850 if (itemVisible && !Double.isNaN(transX1) 851 && !Double.isNaN(transY1)) { 852 float x = (float) transX1; 853 float y = (float) transY1; 854 if (orientation == PlotOrientation.HORIZONTAL) { 855 x = (float) transY1; 856 y = (float) transX1; 857 } 858 if (s.isLastPointGood()) { 859 // TODO: check threshold 860 s.seriesPath.lineTo(x, y); 861 } 862 else { 863 s.seriesPath.moveTo(x, y); 864 } 865 s.setLastPointGood(true); 866 } 867 else { 868 s.setLastPointGood(false); 869 } 870 if (item == dataset.getItemCount(series) - 1) { 871 if (s.seriesIndex == series) { 872 // draw path 873 g2.setStroke(lookupSeriesStroke(series)); 874 g2.setPaint(lookupSeriesPaint(series)); 875 g2.draw(s.seriesPath); 876 } 877 } 878 } 879 880 else if (item != 0 && itemVisible) { 881 // get the previous data point... 882 double x0 = dataset.getXValue(series, item - 1); 883 double y0 = dataset.getYValue(series, item - 1); 884 if (!Double.isNaN(x0) && !Double.isNaN(y0)) { 885 boolean drawLine = true; 886 if (getPlotDiscontinuous()) { 887 // only draw a line if the gap between the current and 888 // previous data point is within the threshold 889 int numX = dataset.getItemCount(series); 890 double minX = dataset.getXValue(series, 0); 891 double maxX = dataset.getXValue(series, numX - 1); 892 if (this.gapThresholdType == UnitType.ABSOLUTE) { 893 drawLine = Math.abs(x1 - x0) <= this.gapThreshold; 894 } 895 else { 896 drawLine = Math.abs(x1 - x0) <= ((maxX - minX) 897 / numX * getGapThreshold()); 898 } 899 } 900 if (drawLine) { 901 double transX0 = domainAxis.valueToJava2D(x0, dataArea, 902 xAxisLocation); 903 double transY0 = rangeAxis.valueToJava2D(y0, dataArea, 904 yAxisLocation); 905 906 // only draw if we have good values 907 if (Double.isNaN(transX0) || Double.isNaN(transY0) 908 || Double.isNaN(transX1) || Double.isNaN(transY1)) { 909 return; 910 } 911 912 if (orientation == PlotOrientation.HORIZONTAL) { 913 state.workingLine.setLine(transY0, transX0, 914 transY1, transX1); 915 } 916 else if (orientation == PlotOrientation.VERTICAL) { 917 state.workingLine.setLine(transX0, transY0, 918 transX1, transY1); 919 } 920 921 if (state.workingLine.intersects(dataArea)) { 922 g2.draw(state.workingLine); 923 } 924 } 925 } 926 } 927 } 928 929 // we needed to get this far even for invisible items, to ensure that 930 // seriesPath updates happened, but now there is nothing more we need 931 // to do for non-visible items... 932 if (!itemVisible) { 933 return; 934 } 935 936 if (getBaseShapesVisible()) { 937 938 Shape shape = getItemShape(series, item); 939 if (orientation == PlotOrientation.HORIZONTAL) { 940 shape = ShapeUtilities.createTranslatedShape(shape, transY1, 941 transX1); 942 } 943 else if (orientation == PlotOrientation.VERTICAL) { 944 shape = ShapeUtilities.createTranslatedShape(shape, transX1, 945 transY1); 946 } 947 if (shape.intersects(dataArea)) { 948 if (getItemShapeFilled(series, item)) { 949 g2.fill(shape); 950 } 951 else { 952 g2.draw(shape); 953 } 954 } 955 entityArea = shape; 956 957 } 958 959 if (getPlotImages()) { 960 Image image = getImage(plot, series, item, transX1, transY1); 961 if (image != null) { 962 Point hotspot = getImageHotspot(plot, series, item, transX1, 963 transY1, image); 964 g2.drawImage(image, (int) (transX1 - hotspot.getX()), 965 (int) (transY1 - hotspot.getY()), null); 966 entityArea = new Rectangle2D.Double(transX1 - hotspot.getX(), 967 transY1 - hotspot.getY(), image.getWidth(null), 968 image.getHeight(null)); 969 } 970 971 } 972 973 double xx = transX1; 974 double yy = transY1; 975 if (orientation == PlotOrientation.HORIZONTAL) { 976 xx = transY1; 977 yy = transX1; 978 } 979 980 // draw the item label if there is one... 981 if (isItemLabelVisible(series, item)) { 982 drawItemLabel(g2, orientation, dataset, series, item, xx, yy, 983 (y1 < 0.0)); 984 } 985 986 int domainAxisIndex = plot.getDomainAxisIndex(domainAxis); 987 int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis); 988 updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex, 989 rangeAxisIndex, transX1, transY1, orientation); 990 991 // add an entity for the item... 992 if (entities != null && dataArea.contains(xx, yy)) { 993 addEntity(entities, entityArea, dataset, series, item, xx, yy); 994 } 995 996 } 997 998 /** 999 * Tests this renderer for equality with another object. 1000 * 1001 * @param obj the object (<code>null</code> permitted). 1002 * 1003 * @return A boolean. 1004 */ 1005 public boolean equals(Object obj) { 1006 1007 if (obj == this) { 1008 return true; 1009 } 1010 if (!(obj instanceof StandardXYItemRenderer)) { 1011 return false; 1012 } 1013 StandardXYItemRenderer that = (StandardXYItemRenderer) obj; 1014 if (this.baseShapesVisible != that.baseShapesVisible) { 1015 return false; 1016 } 1017 if (this.plotLines != that.plotLines) { 1018 return false; 1019 } 1020 if (this.plotImages != that.plotImages) { 1021 return false; 1022 } 1023 if (this.plotDiscontinuous != that.plotDiscontinuous) { 1024 return false; 1025 } 1026 if (this.gapThresholdType != that.gapThresholdType) { 1027 return false; 1028 } 1029 if (this.gapThreshold != that.gapThreshold) { 1030 return false; 1031 } 1032 if (!ObjectUtilities.equal(this.shapesFilled, that.shapesFilled)) { 1033 return false; 1034 } 1035 if (!this.seriesShapesFilled.equals(that.seriesShapesFilled)) { 1036 return false; 1037 } 1038 if (this.baseShapesFilled != that.baseShapesFilled) { 1039 return false; 1040 } 1041 if (this.drawSeriesLineAsPath != that.drawSeriesLineAsPath) { 1042 return false; 1043 } 1044 if (!ShapeUtilities.equal(this.legendLine, that.legendLine)) { 1045 return false; 1046 } 1047 return super.equals(obj); 1048 1049 } 1050 1051 /** 1052 * Returns a clone of the renderer. 1053 * 1054 * @return A clone. 1055 * 1056 * @throws CloneNotSupportedException if the renderer cannot be cloned. 1057 */ 1058 public Object clone() throws CloneNotSupportedException { 1059 StandardXYItemRenderer clone = (StandardXYItemRenderer) super.clone(); 1060 clone.seriesShapesFilled 1061 = (BooleanList) this.seriesShapesFilled.clone(); 1062 clone.legendLine = ShapeUtilities.clone(this.legendLine); 1063 return clone; 1064 } 1065 1066 //////////////////////////////////////////////////////////////////////////// 1067 // PROTECTED METHODS 1068 // These provide the opportunity to subclass the standard renderer and 1069 // create custom effects. 1070 //////////////////////////////////////////////////////////////////////////// 1071 1072 /** 1073 * Returns the image used to draw a single data item. 1074 * 1075 * @param plot the plot (can be used to obtain standard color information 1076 * etc). 1077 * @param series the series index. 1078 * @param item the item index. 1079 * @param x the x value of the item. 1080 * @param y the y value of the item. 1081 * 1082 * @return The image. 1083 * 1084 * @see #getPlotImages() 1085 */ 1086 protected Image getImage(Plot plot, int series, int item, 1087 double x, double y) { 1088 // this method must be overridden if you want to display images 1089 return null; 1090 } 1091 1092 /** 1093 * Returns the hotspot of the image used to draw a single data item. 1094 * The hotspot is the point relative to the top left of the image 1095 * that should indicate the data item. The default is the center of the 1096 * image. 1097 * 1098 * @param plot the plot (can be used to obtain standard color information 1099 * etc). 1100 * @param image the image (can be used to get size information about the 1101 * image) 1102 * @param series the series index 1103 * @param item the item index 1104 * @param x the x value of the item 1105 * @param y the y value of the item 1106 * 1107 * @return The hotspot used to draw the data item. 1108 */ 1109 protected Point getImageHotspot(Plot plot, int series, int item, 1110 double x, double y, Image image) { 1111 1112 int height = image.getHeight(null); 1113 int width = image.getWidth(null); 1114 return new Point(width / 2, height / 2); 1115 1116 } 1117 1118 /** 1119 * Provides serialization support. 1120 * 1121 * @param stream the input stream. 1122 * 1123 * @throws IOException if there is an I/O error. 1124 * @throws ClassNotFoundException if there is a classpath problem. 1125 */ 1126 private void readObject(ObjectInputStream stream) 1127 throws IOException, ClassNotFoundException { 1128 stream.defaultReadObject(); 1129 this.legendLine = SerialUtilities.readShape(stream); 1130 } 1131 1132 /** 1133 * Provides serialization support. 1134 * 1135 * @param stream the output stream. 1136 * 1137 * @throws IOException if there is an I/O error. 1138 */ 1139 private void writeObject(ObjectOutputStream stream) throws IOException { 1140 stream.defaultWriteObject(); 1141 SerialUtilities.writeShape(this.legendLine, stream); 1142 } 1143 1144 }