001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2013, by Object Refinery Limited and Contributors. 006 * 007 * Project Info: http://www.jfree.org/jfreechart/index.html 008 * 009 * This library is free software; you can redistribute it and/or modify it 010 * under the terms of the GNU Lesser General Public License as published by 011 * the Free Software Foundation; either version 2.1 of the License, or 012 * (at your option) any later version. 013 * 014 * This library is distributed in the hope that it will be useful, but 015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 017 * License for more details. 018 * 019 * You should have received a copy of the GNU Lesser General Public 020 * License along with this library; if not, write to the Free Software 021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 022 * USA. 023 * 024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * --------------------------- 028 * StandardXYItemRenderer.java 029 * --------------------------- 030 * (C) Copyright 2001-2013, 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 * 02-Jun-2008 : Fixed tooltips for data items at lower edges of data area (DG); 106 * 17-Jun-2008 : Apply legend shape, font and paint attributes (DG); 107 * 03-Jul-2013 : Use ParamChecks (DG); 108 * 109 */ 110 111package org.jfree.chart.renderer.xy; 112 113import java.awt.Graphics2D; 114import java.awt.Image; 115import java.awt.Paint; 116import java.awt.Point; 117import java.awt.Shape; 118import java.awt.Stroke; 119import java.awt.geom.GeneralPath; 120import java.awt.geom.Line2D; 121import java.awt.geom.Rectangle2D; 122import java.io.IOException; 123import java.io.ObjectInputStream; 124import java.io.ObjectOutputStream; 125import java.io.Serializable; 126 127import org.jfree.chart.LegendItem; 128import org.jfree.chart.axis.ValueAxis; 129import org.jfree.chart.entity.EntityCollection; 130import org.jfree.chart.event.RendererChangeEvent; 131import org.jfree.chart.labels.XYToolTipGenerator; 132import org.jfree.chart.plot.CrosshairState; 133import org.jfree.chart.plot.Plot; 134import org.jfree.chart.plot.PlotOrientation; 135import org.jfree.chart.plot.PlotRenderingInfo; 136import org.jfree.chart.plot.XYPlot; 137import org.jfree.chart.urls.XYURLGenerator; 138import org.jfree.chart.util.ParamChecks; 139import org.jfree.data.xy.XYDataset; 140import org.jfree.io.SerialUtilities; 141import org.jfree.ui.RectangleEdge; 142import org.jfree.util.BooleanList; 143import org.jfree.util.BooleanUtilities; 144import org.jfree.util.ObjectUtilities; 145import org.jfree.util.PublicCloneable; 146import org.jfree.util.ShapeUtilities; 147import org.jfree.util.UnitType; 148 149/** 150 * Standard item renderer for an {@link XYPlot}. This class can draw (a) 151 * shapes at each point, or (b) lines between points, or (c) both shapes and 152 * lines. 153 * <P> 154 * This renderer has been retained for historical reasons and, in general, you 155 * should use the {@link XYLineAndShapeRenderer} class instead. 156 */ 157public class StandardXYItemRenderer extends AbstractXYItemRenderer 158 implements XYItemRenderer, Cloneable, PublicCloneable, Serializable { 159 160 /** For serialization. */ 161 private static final long serialVersionUID = -3271351259436865995L; 162 163 /** Constant for the type of rendering (shapes only). */ 164 public static final int SHAPES = 1; 165 166 /** Constant for the type of rendering (lines only). */ 167 public static final int LINES = 2; 168 169 /** Constant for the type of rendering (shapes and lines). */ 170 public static final int SHAPES_AND_LINES = SHAPES | LINES; 171 172 /** Constant for the type of rendering (images only). */ 173 public static final int IMAGES = 4; 174 175 /** Constant for the type of rendering (discontinuous lines). */ 176 public static final int DISCONTINUOUS = 8; 177 178 /** Constant for the type of rendering (discontinuous lines). */ 179 public static final int DISCONTINUOUS_LINES = LINES | DISCONTINUOUS; 180 181 /** A flag indicating whether or not shapes are drawn at each XY point. */ 182 private boolean baseShapesVisible; 183 184 /** A flag indicating whether or not lines are drawn between XY points. */ 185 private boolean plotLines; 186 187 /** A flag indicating whether or not images are drawn between XY points. */ 188 private boolean plotImages; 189 190 /** A flag controlling whether or not discontinuous lines are used. */ 191 private boolean plotDiscontinuous; 192 193 /** Specifies how the gap threshold value is interpreted. */ 194 private UnitType gapThresholdType = UnitType.RELATIVE; 195 196 /** Threshold for deciding when to discontinue a line. */ 197 private double gapThreshold = 1.0; 198 199 /** 200 * A flag that controls whether or not shapes are filled for ALL series. 201 * 202 * @deprecated As of 1.0.8, this override should not be used. 203 */ 204 private Boolean shapesFilled; 205 206 /** 207 * A table of flags that control (per series) whether or not shapes are 208 * filled. 209 */ 210 private BooleanList seriesShapesFilled; 211 212 /** The default value returned by the getShapeFilled() method. */ 213 private boolean baseShapesFilled; 214 215 /** 216 * A flag that controls whether or not each series is drawn as a single 217 * path. 218 */ 219 private boolean drawSeriesLineAsPath; 220 221 /** 222 * The shape that is used to represent a line in the legend. 223 * This should never be set to <code>null</code>. 224 */ 225 private transient Shape legendLine; 226 227 /** 228 * Constructs a new renderer. 229 */ 230 public StandardXYItemRenderer() { 231 this(LINES, null); 232 } 233 234 /** 235 * Constructs a new renderer. To specify the type of renderer, use one of 236 * the constants: {@link #SHAPES}, {@link #LINES} or 237 * {@link #SHAPES_AND_LINES}. 238 * 239 * @param type the type. 240 */ 241 public StandardXYItemRenderer(int type) { 242 this(type, null); 243 } 244 245 /** 246 * Constructs a new renderer. To specify the type of renderer, use one of 247 * the constants: {@link #SHAPES}, {@link #LINES} or 248 * {@link #SHAPES_AND_LINES}. 249 * 250 * @param type the type of renderer. 251 * @param toolTipGenerator the item label generator (<code>null</code> 252 * permitted). 253 */ 254 public StandardXYItemRenderer(int type, 255 XYToolTipGenerator toolTipGenerator) { 256 this(type, toolTipGenerator, null); 257 } 258 259 /** 260 * Constructs a new renderer. To specify the type of renderer, use one of 261 * the constants: {@link #SHAPES}, {@link #LINES} or 262 * {@link #SHAPES_AND_LINES}. 263 * 264 * @param type the type of renderer. 265 * @param toolTipGenerator the item label generator (<code>null</code> 266 * permitted). 267 * @param urlGenerator the URL generator. 268 */ 269 public StandardXYItemRenderer(int type, 270 XYToolTipGenerator toolTipGenerator, 271 XYURLGenerator urlGenerator) { 272 273 super(); 274 setBaseToolTipGenerator(toolTipGenerator); 275 setURLGenerator(urlGenerator); 276 if ((type & SHAPES) != 0) { 277 this.baseShapesVisible = true; 278 } 279 if ((type & LINES) != 0) { 280 this.plotLines = true; 281 } 282 if ((type & IMAGES) != 0) { 283 this.plotImages = true; 284 } 285 if ((type & DISCONTINUOUS) != 0) { 286 this.plotDiscontinuous = true; 287 } 288 289 this.shapesFilled = null; 290 this.seriesShapesFilled = new BooleanList(); 291 this.baseShapesFilled = true; 292 this.legendLine = new Line2D.Double(-7.0, 0.0, 7.0, 0.0); 293 this.drawSeriesLineAsPath = false; 294 } 295 296 /** 297 * Returns true if shapes are being plotted by the renderer. 298 * 299 * @return <code>true</code> if shapes are being plotted by the renderer. 300 * 301 * @see #setBaseShapesVisible 302 */ 303 public boolean getBaseShapesVisible() { 304 return this.baseShapesVisible; 305 } 306 307 /** 308 * Sets the flag that controls whether or not a shape is plotted at each 309 * data point. 310 * 311 * @param flag the flag. 312 * 313 * @see #getBaseShapesVisible 314 */ 315 public void setBaseShapesVisible(boolean flag) { 316 if (this.baseShapesVisible != flag) { 317 this.baseShapesVisible = flag; 318 fireChangeEvent(); 319 } 320 } 321 322 // SHAPES FILLED 323 324 /** 325 * Returns the flag used to control whether or not the shape for an item is 326 * filled. 327 * <p> 328 * The default implementation passes control to the 329 * <code>getSeriesShapesFilled</code> method. You can override this method 330 * if you require different behaviour. 331 * 332 * @param series the series index (zero-based). 333 * @param item the item index (zero-based). 334 * 335 * @return A boolean. 336 * 337 * @see #getSeriesShapesFilled(int) 338 */ 339 public boolean getItemShapeFilled(int series, int item) { 340 // return the overall setting, if there is one... 341 if (this.shapesFilled != null) { 342 return this.shapesFilled.booleanValue(); 343 } 344 345 // otherwise look up the paint table 346 Boolean flag = this.seriesShapesFilled.getBoolean(series); 347 if (flag != null) { 348 return flag.booleanValue(); 349 } 350 else { 351 return this.baseShapesFilled; 352 } 353 } 354 355 /** 356 * Returns the override flag that controls whether or not shapes are filled 357 * for ALL series. 358 * 359 * @return The flag (possibly <code>null</code>). 360 * 361 * @since 1.0.5 362 * 363 * @deprecated As of 1.0.8, you should avoid using this method and rely 364 * on just the per-series ({@link #getSeriesShapesFilled(int)}) 365 * and base-level ({@link #getBaseShapesFilled()}) settings. 366 */ 367 public Boolean getShapesFilled() { 368 return this.shapesFilled; 369 } 370 371 /** 372 * Sets the override flag that controls whether or not shapes are filled 373 * for ALL series and sends a {@link RendererChangeEvent} to all registered 374 * listeners. 375 * 376 * @param filled the flag. 377 * 378 * @see #setShapesFilled(Boolean) 379 * 380 * @deprecated As of 1.0.8, you should avoid using this method and rely 381 * on just the per-series ({@link #setSeriesShapesFilled(int, 382 * Boolean)}) and base-level ({@link #setBaseShapesVisible( 383 * boolean)}) settings. 384 */ 385 public void setShapesFilled(boolean filled) { 386 // here we use BooleanUtilities to remain compatible with JDKs < 1.4 387 setShapesFilled(BooleanUtilities.valueOf(filled)); 388 } 389 390 /** 391 * Sets the override flag that controls whether or not shapes are filled 392 * for ALL series and sends a {@link RendererChangeEvent} to all registered 393 * listeners. 394 * 395 * @param filled the flag (<code>null</code> permitted). 396 * 397 * @see #setShapesFilled(boolean) 398 * 399 * @deprecated As of 1.0.8, you should avoid using this method and rely 400 * on just the per-series ({@link #setSeriesShapesFilled(int, 401 * Boolean)}) and base-level ({@link #setBaseShapesVisible( 402 * boolean)}) settings. 403 */ 404 public void setShapesFilled(Boolean filled) { 405 this.shapesFilled = filled; 406 fireChangeEvent(); 407 } 408 409 /** 410 * Returns the flag used to control whether or not the shapes for a series 411 * are filled. 412 * 413 * @param series the series index (zero-based). 414 * 415 * @return A boolean. 416 */ 417 public Boolean getSeriesShapesFilled(int series) { 418 return this.seriesShapesFilled.getBoolean(series); 419 } 420 421 /** 422 * Sets the 'shapes filled' flag for a series and sends a 423 * {@link RendererChangeEvent} to all registered listeners. 424 * 425 * @param series the series index (zero-based). 426 * @param flag the flag. 427 * 428 * @see #getSeriesShapesFilled(int) 429 */ 430 public void setSeriesShapesFilled(int series, Boolean flag) { 431 this.seriesShapesFilled.setBoolean(series, flag); 432 fireChangeEvent(); 433 } 434 435 /** 436 * Returns the base 'shape filled' attribute. 437 * 438 * @return The base flag. 439 * 440 * @see #setBaseShapesFilled(boolean) 441 */ 442 public boolean getBaseShapesFilled() { 443 return this.baseShapesFilled; 444 } 445 446 /** 447 * Sets the base 'shapes filled' flag and sends a 448 * {@link RendererChangeEvent} to all registered listeners. 449 * 450 * @param flag the flag. 451 * 452 * @see #getBaseShapesFilled() 453 */ 454 public void setBaseShapesFilled(boolean flag) { 455 this.baseShapesFilled = flag; 456 } 457 458 /** 459 * Returns true if lines are being plotted by the renderer. 460 * 461 * @return <code>true</code> if lines are being plotted by the renderer. 462 * 463 * @see #setPlotLines(boolean) 464 */ 465 public boolean getPlotLines() { 466 return this.plotLines; 467 } 468 469 /** 470 * Sets the flag that controls whether or not a line is plotted between 471 * each data point and sends a {@link RendererChangeEvent} to all 472 * registered listeners. 473 * 474 * @param flag the flag. 475 * 476 * @see #getPlotLines() 477 */ 478 public void setPlotLines(boolean flag) { 479 if (this.plotLines != flag) { 480 this.plotLines = flag; 481 fireChangeEvent(); 482 } 483 } 484 485 /** 486 * Returns the gap threshold type (relative or absolute). 487 * 488 * @return The type. 489 * 490 * @see #setGapThresholdType(UnitType) 491 */ 492 public UnitType getGapThresholdType() { 493 return this.gapThresholdType; 494 } 495 496 /** 497 * Sets the gap threshold type and sends a {@link RendererChangeEvent} to 498 * all registered listeners. 499 * 500 * @param thresholdType the type (<code>null</code> not permitted). 501 * 502 * @see #getGapThresholdType() 503 */ 504 public void setGapThresholdType(UnitType thresholdType) { 505 ParamChecks.nullNotPermitted(thresholdType, "thresholdType"); 506 this.gapThresholdType = thresholdType; 507 fireChangeEvent(); 508 } 509 510 /** 511 * Returns the gap threshold for discontinuous lines. 512 * 513 * @return The gap threshold. 514 * 515 * @see #setGapThreshold(double) 516 */ 517 public double getGapThreshold() { 518 return this.gapThreshold; 519 } 520 521 /** 522 * Sets the gap threshold for discontinuous lines and sends a 523 * {@link RendererChangeEvent} to all registered listeners. 524 * 525 * @param t the threshold. 526 * 527 * @see #getGapThreshold() 528 */ 529 public void setGapThreshold(double t) { 530 this.gapThreshold = t; 531 fireChangeEvent(); 532 } 533 534 /** 535 * Returns true if images are being plotted by the renderer. 536 * 537 * @return <code>true</code> if images are being plotted by the renderer. 538 * 539 * @see #setPlotImages(boolean) 540 */ 541 public boolean getPlotImages() { 542 return this.plotImages; 543 } 544 545 /** 546 * Sets the flag that controls whether or not an image is drawn at each 547 * data point and sends a {@link RendererChangeEvent} to all registered 548 * listeners. 549 * 550 * @param flag the flag. 551 * 552 * @see #getPlotImages() 553 */ 554 public void setPlotImages(boolean flag) { 555 if (this.plotImages != flag) { 556 this.plotImages = flag; 557 fireChangeEvent(); 558 } 559 } 560 561 /** 562 * Returns a flag that controls whether or not the renderer shows 563 * discontinuous lines. 564 * 565 * @return <code>true</code> if lines should be discontinuous. 566 */ 567 public boolean getPlotDiscontinuous() { 568 return this.plotDiscontinuous; 569 } 570 571 /** 572 * Sets the flag that controls whether or not the renderer shows 573 * discontinuous lines, and sends a {@link RendererChangeEvent} to all 574 * registered listeners. 575 * 576 * @param flag the new flag value. 577 * 578 * @since 1.0.5 579 */ 580 public void setPlotDiscontinuous(boolean flag) { 581 if (this.plotDiscontinuous != flag) { 582 this.plotDiscontinuous = flag; 583 fireChangeEvent(); 584 } 585 } 586 587 /** 588 * Returns a flag that controls whether or not each series is drawn as a 589 * single path. 590 * 591 * @return A boolean. 592 * 593 * @see #setDrawSeriesLineAsPath(boolean) 594 */ 595 public boolean getDrawSeriesLineAsPath() { 596 return this.drawSeriesLineAsPath; 597 } 598 599 /** 600 * Sets the flag that controls whether or not each series is drawn as a 601 * single path. 602 * 603 * @param flag the flag. 604 * 605 * @see #getDrawSeriesLineAsPath() 606 */ 607 public void setDrawSeriesLineAsPath(boolean flag) { 608 this.drawSeriesLineAsPath = flag; 609 } 610 611 /** 612 * Returns the shape used to represent a line in the legend. 613 * 614 * @return The legend line (never <code>null</code>). 615 * 616 * @see #setLegendLine(Shape) 617 */ 618 public Shape getLegendLine() { 619 return this.legendLine; 620 } 621 622 /** 623 * Sets the shape used as a line in each legend item and sends a 624 * {@link RendererChangeEvent} to all registered listeners. 625 * 626 * @param line the line (<code>null</code> not permitted). 627 * 628 * @see #getLegendLine() 629 */ 630 public void setLegendLine(Shape line) { 631 ParamChecks.nullNotPermitted(line, "line"); 632 this.legendLine = line; 633 fireChangeEvent(); 634 } 635 636 /** 637 * Returns a legend item for a series. 638 * 639 * @param datasetIndex the dataset index (zero-based). 640 * @param series the series index (zero-based). 641 * 642 * @return A legend item for the series. 643 */ 644 @Override 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 = lookupLegendShape(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.setLabelFont(lookupLegendTextFont(series)); 677 Paint labelPaint = lookupLegendTextPaint(series); 678 if (labelPaint != null) { 679 result.setLabelPaint(labelPaint); 680 } 681 result.setDataset(dataset); 682 result.setDatasetIndex(datasetIndex); 683 result.setSeriesKey(dataset.getSeriesKey(series)); 684 result.setSeriesIndex(series); 685 } 686 } 687 return result; 688 } 689 690 /** 691 * Records the state for the renderer. This is used to preserve state 692 * information between calls to the drawItem() method for a single chart 693 * drawing. 694 */ 695 public static class State extends XYItemRendererState { 696 697 /** The path for the current series. */ 698 public GeneralPath seriesPath; 699 700 /** The series index. */ 701 private int seriesIndex; 702 703 /** 704 * A flag that indicates if the last (x, y) point was 'good' 705 * (non-null). 706 */ 707 private boolean lastPointGood; 708 709 /** 710 * Creates a new state instance. 711 * 712 * @param info the plot rendering info. 713 */ 714 public State(PlotRenderingInfo info) { 715 super(info); 716 } 717 718 /** 719 * Returns a flag that indicates if the last point drawn (in the 720 * current series) was 'good' (non-null). 721 * 722 * @return A boolean. 723 */ 724 public boolean isLastPointGood() { 725 return this.lastPointGood; 726 } 727 728 /** 729 * Sets a flag that indicates if the last point drawn (in the current 730 * series) was 'good' (non-null). 731 * 732 * @param good the flag. 733 */ 734 public void setLastPointGood(boolean good) { 735 this.lastPointGood = good; 736 } 737 738 /** 739 * Returns the series index for the current path. 740 * 741 * @return The series index for the current path. 742 */ 743 public int getSeriesIndex() { 744 return this.seriesIndex; 745 } 746 747 /** 748 * Sets the series index for the current path. 749 * 750 * @param index the index. 751 */ 752 public void setSeriesIndex(int index) { 753 this.seriesIndex = index; 754 } 755 } 756 757 /** 758 * Initialises the renderer. 759 * <P> 760 * This method will be called before the first item is rendered, giving the 761 * renderer an opportunity to initialise any state information it wants to 762 * maintain. The renderer can do nothing if it chooses. 763 * 764 * @param g2 the graphics device. 765 * @param dataArea the area inside the axes. 766 * @param plot the plot. 767 * @param data the data. 768 * @param info an optional info collection object to return data back to 769 * the caller. 770 * 771 * @return The renderer state. 772 */ 773 @Override 774 public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea, 775 XYPlot plot, XYDataset data, PlotRenderingInfo info) { 776 777 State state = new State(info); 778 state.seriesPath = new GeneralPath(); 779 state.seriesIndex = -1; 780 return state; 781 782 } 783 784 /** 785 * Draws the visual representation of a single data item. 786 * 787 * @param g2 the graphics device. 788 * @param state the renderer state. 789 * @param dataArea the area within which the data is being drawn. 790 * @param info collects information about the drawing. 791 * @param plot the plot (can be used to obtain standard color information 792 * etc). 793 * @param domainAxis the domain axis. 794 * @param rangeAxis the range axis. 795 * @param dataset the dataset. 796 * @param series the series index (zero-based). 797 * @param item the item index (zero-based). 798 * @param crosshairState crosshair information for the plot 799 * (<code>null</code> permitted). 800 * @param pass the pass index. 801 */ 802 @Override 803 public void drawItem(Graphics2D g2, XYItemRendererState state, 804 Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, 805 ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, 806 int series, int item, CrosshairState crosshairState, int pass) { 807 808 boolean itemVisible = getItemVisible(series, item); 809 810 // setup for collecting optional entity info... 811 Shape entityArea = null; 812 EntityCollection entities = null; 813 if (info != null) { 814 entities = info.getOwner().getEntityCollection(); 815 } 816 817 PlotOrientation orientation = plot.getOrientation(); 818 Paint paint = getItemPaint(series, item); 819 Stroke seriesStroke = getItemStroke(series, item); 820 g2.setPaint(paint); 821 g2.setStroke(seriesStroke); 822 823 // get the data point... 824 double x1 = dataset.getXValue(series, item); 825 double y1 = dataset.getYValue(series, item); 826 if (Double.isNaN(x1) || Double.isNaN(y1)) { 827 itemVisible = false; 828 } 829 830 RectangleEdge xAxisLocation = plot.getDomainAxisEdge(); 831 RectangleEdge yAxisLocation = plot.getRangeAxisEdge(); 832 double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation); 833 double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation); 834 835 if (getPlotLines()) { 836 if (this.drawSeriesLineAsPath) { 837 State s = (State) state; 838 if (s.getSeriesIndex() != series) { 839 // we are starting a new series path 840 s.seriesPath.reset(); 841 s.lastPointGood = false; 842 s.setSeriesIndex(series); 843 } 844 845 // update path to reflect latest point 846 if (itemVisible && !Double.isNaN(transX1) 847 && !Double.isNaN(transY1)) { 848 float x = (float) transX1; 849 float y = (float) transY1; 850 if (orientation == PlotOrientation.HORIZONTAL) { 851 x = (float) transY1; 852 y = (float) transX1; 853 } 854 if (s.isLastPointGood()) { 855 // TODO: check threshold 856 s.seriesPath.lineTo(x, y); 857 } 858 else { 859 s.seriesPath.moveTo(x, y); 860 } 861 s.setLastPointGood(true); 862 } 863 else { 864 s.setLastPointGood(false); 865 } 866 if (item == dataset.getItemCount(series) - 1) { 867 if (s.seriesIndex == series) { 868 // draw path 869 g2.setStroke(lookupSeriesStroke(series)); 870 g2.setPaint(lookupSeriesPaint(series)); 871 g2.draw(s.seriesPath); 872 } 873 } 874 } 875 876 else if (item != 0 && itemVisible) { 877 // get the previous data point... 878 double x0 = dataset.getXValue(series, item - 1); 879 double y0 = dataset.getYValue(series, item - 1); 880 if (!Double.isNaN(x0) && !Double.isNaN(y0)) { 881 boolean drawLine = true; 882 if (getPlotDiscontinuous()) { 883 // only draw a line if the gap between the current and 884 // previous data point is within the threshold 885 int numX = dataset.getItemCount(series); 886 double minX = dataset.getXValue(series, 0); 887 double maxX = dataset.getXValue(series, numX - 1); 888 if (this.gapThresholdType == UnitType.ABSOLUTE) { 889 drawLine = Math.abs(x1 - x0) <= this.gapThreshold; 890 } 891 else { 892 drawLine = Math.abs(x1 - x0) <= ((maxX - minX) 893 / numX * getGapThreshold()); 894 } 895 } 896 if (drawLine) { 897 double transX0 = domainAxis.valueToJava2D(x0, dataArea, 898 xAxisLocation); 899 double transY0 = rangeAxis.valueToJava2D(y0, dataArea, 900 yAxisLocation); 901 902 // only draw if we have good values 903 if (Double.isNaN(transX0) || Double.isNaN(transY0) 904 || Double.isNaN(transX1) || Double.isNaN(transY1)) { 905 return; 906 } 907 908 if (orientation == PlotOrientation.HORIZONTAL) { 909 state.workingLine.setLine(transY0, transX0, 910 transY1, transX1); 911 } 912 else if (orientation == PlotOrientation.VERTICAL) { 913 state.workingLine.setLine(transX0, transY0, 914 transX1, transY1); 915 } 916 917 if (state.workingLine.intersects(dataArea)) { 918 g2.draw(state.workingLine); 919 } 920 } 921 } 922 } 923 } 924 925 // we needed to get this far even for invisible items, to ensure that 926 // seriesPath updates happened, but now there is nothing more we need 927 // to do for non-visible items... 928 if (!itemVisible) { 929 return; 930 } 931 932 if (getBaseShapesVisible()) { 933 934 Shape shape = getItemShape(series, item); 935 if (orientation == PlotOrientation.HORIZONTAL) { 936 shape = ShapeUtilities.createTranslatedShape(shape, transY1, 937 transX1); 938 } 939 else if (orientation == PlotOrientation.VERTICAL) { 940 shape = ShapeUtilities.createTranslatedShape(shape, transX1, 941 transY1); 942 } 943 if (shape.intersects(dataArea)) { 944 if (getItemShapeFilled(series, item)) { 945 g2.fill(shape); 946 } 947 else { 948 g2.draw(shape); 949 } 950 } 951 entityArea = shape; 952 953 } 954 955 if (getPlotImages()) { 956 Image image = getImage(plot, series, item, transX1, transY1); 957 if (image != null) { 958 Point hotspot = getImageHotspot(plot, series, item, transX1, 959 transY1, image); 960 g2.drawImage(image, (int) (transX1 - hotspot.getX()), 961 (int) (transY1 - hotspot.getY()), null); 962 entityArea = new Rectangle2D.Double(transX1 - hotspot.getX(), 963 transY1 - hotspot.getY(), image.getWidth(null), 964 image.getHeight(null)); 965 } 966 967 } 968 969 double xx = transX1; 970 double yy = transY1; 971 if (orientation == PlotOrientation.HORIZONTAL) { 972 xx = transY1; 973 yy = transX1; 974 } 975 976 // draw the item label if there is one... 977 if (isItemLabelVisible(series, item)) { 978 drawItemLabel(g2, orientation, dataset, series, item, xx, yy, 979 (y1 < 0.0)); 980 } 981 982 int domainAxisIndex = plot.getDomainAxisIndex(domainAxis); 983 int rangeAxisIndex = plot.getRangeAxisIndex(rangeAxis); 984 updateCrosshairValues(crosshairState, x1, y1, domainAxisIndex, 985 rangeAxisIndex, transX1, transY1, orientation); 986 987 // add an entity for the item... 988 if (entities != null && isPointInRect(dataArea, xx, yy)) { 989 addEntity(entities, entityArea, dataset, series, item, xx, yy); 990 } 991 992 } 993 994 /** 995 * Tests this renderer for equality with another object. 996 * 997 * @param obj the object (<code>null</code> permitted). 998 * 999 * @return A boolean. 1000 */ 1001 @Override 1002 public boolean equals(Object obj) { 1003 1004 if (obj == this) { 1005 return true; 1006 } 1007 if (!(obj instanceof StandardXYItemRenderer)) { 1008 return false; 1009 } 1010 StandardXYItemRenderer that = (StandardXYItemRenderer) obj; 1011 if (this.baseShapesVisible != that.baseShapesVisible) { 1012 return false; 1013 } 1014 if (this.plotLines != that.plotLines) { 1015 return false; 1016 } 1017 if (this.plotImages != that.plotImages) { 1018 return false; 1019 } 1020 if (this.plotDiscontinuous != that.plotDiscontinuous) { 1021 return false; 1022 } 1023 if (this.gapThresholdType != that.gapThresholdType) { 1024 return false; 1025 } 1026 if (this.gapThreshold != that.gapThreshold) { 1027 return false; 1028 } 1029 if (!ObjectUtilities.equal(this.shapesFilled, that.shapesFilled)) { 1030 return false; 1031 } 1032 if (!this.seriesShapesFilled.equals(that.seriesShapesFilled)) { 1033 return false; 1034 } 1035 if (this.baseShapesFilled != that.baseShapesFilled) { 1036 return false; 1037 } 1038 if (this.drawSeriesLineAsPath != that.drawSeriesLineAsPath) { 1039 return false; 1040 } 1041 if (!ShapeUtilities.equal(this.legendLine, that.legendLine)) { 1042 return false; 1043 } 1044 return super.equals(obj); 1045 1046 } 1047 1048 /** 1049 * Returns a clone of the renderer. 1050 * 1051 * @return A clone. 1052 * 1053 * @throws CloneNotSupportedException if the renderer cannot be cloned. 1054 */ 1055 @Override 1056 public Object clone() throws CloneNotSupportedException { 1057 StandardXYItemRenderer clone = (StandardXYItemRenderer) super.clone(); 1058 clone.seriesShapesFilled 1059 = (BooleanList) this.seriesShapesFilled.clone(); 1060 clone.legendLine = ShapeUtilities.clone(this.legendLine); 1061 return clone; 1062 } 1063 1064 //////////////////////////////////////////////////////////////////////////// 1065 // PROTECTED METHODS 1066 // These provide the opportunity to subclass the standard renderer and 1067 // create custom effects. 1068 //////////////////////////////////////////////////////////////////////////// 1069 1070 /** 1071 * Returns the image used to draw a single data item. 1072 * 1073 * @param plot the plot (can be used to obtain standard color information 1074 * etc). 1075 * @param series the series index. 1076 * @param item the item index. 1077 * @param x the x value of the item. 1078 * @param y the y value of the item. 1079 * 1080 * @return The image. 1081 * 1082 * @see #getPlotImages() 1083 */ 1084 protected Image getImage(Plot plot, int series, int item, 1085 double x, double y) { 1086 // this method must be overridden if you want to display images 1087 return null; 1088 } 1089 1090 /** 1091 * Returns the hotspot of the image used to draw a single data item. 1092 * The hotspot is the point relative to the top left of the image 1093 * that should indicate the data item. The default is the center of the 1094 * image. 1095 * 1096 * @param plot the plot (can be used to obtain standard color information 1097 * etc). 1098 * @param image the image (can be used to get size information about the 1099 * image) 1100 * @param series the series index 1101 * @param item the item index 1102 * @param x the x value of the item 1103 * @param y the y value of the item 1104 * 1105 * @return The hotspot used to draw the data item. 1106 */ 1107 protected Point getImageHotspot(Plot plot, int series, int item, 1108 double x, double y, Image image) { 1109 1110 int height = image.getHeight(null); 1111 int width = image.getWidth(null); 1112 return new Point(width / 2, height / 2); 1113 1114 } 1115 1116 /** 1117 * Provides serialization support. 1118 * 1119 * @param stream the input stream. 1120 * 1121 * @throws IOException if there is an I/O error. 1122 * @throws ClassNotFoundException if there is a classpath problem. 1123 */ 1124 private void readObject(ObjectInputStream stream) 1125 throws IOException, ClassNotFoundException { 1126 stream.defaultReadObject(); 1127 this.legendLine = SerialUtilities.readShape(stream); 1128 } 1129 1130 /** 1131 * Provides serialization support. 1132 * 1133 * @param stream the output stream. 1134 * 1135 * @throws IOException if there is an I/O error. 1136 */ 1137 private void writeObject(ObjectOutputStream stream) throws IOException { 1138 stream.defaultWriteObject(); 1139 SerialUtilities.writeShape(this.legendLine, stream); 1140 } 1141 1142}