001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2021, 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 * XYLineAndShapeRenderer.java 029 * --------------------------- 030 * (C) Copyright 2004-2021, by Object Refinery Limited. 031 * 032 * Original Author: David Gilbert (for Object Refinery Limited); 033 * Contributor(s): -; 034 * 035 */ 036 037package org.jfree.chart.renderer.xy; 038 039import java.awt.Graphics2D; 040import java.awt.Paint; 041import java.awt.Shape; 042import java.awt.Stroke; 043import java.awt.geom.GeneralPath; 044import java.awt.geom.Line2D; 045import java.awt.geom.Rectangle2D; 046import java.io.IOException; 047import java.io.ObjectInputStream; 048import java.io.ObjectOutputStream; 049import java.io.Serializable; 050import java.util.Objects; 051 052import org.jfree.chart.LegendItem; 053import org.jfree.chart.axis.ValueAxis; 054import org.jfree.chart.entity.EntityCollection; 055import org.jfree.chart.event.RendererChangeEvent; 056import org.jfree.chart.plot.CrosshairState; 057import org.jfree.chart.plot.PlotOrientation; 058import org.jfree.chart.plot.PlotRenderingInfo; 059import org.jfree.chart.plot.XYPlot; 060import org.jfree.chart.ui.RectangleEdge; 061import org.jfree.chart.util.BooleanList; 062import org.jfree.chart.util.LineUtils; 063import org.jfree.chart.util.Args; 064import org.jfree.chart.util.PublicCloneable; 065import org.jfree.chart.util.SerialUtils; 066import org.jfree.chart.util.ShapeUtils; 067import org.jfree.data.xy.XYDataset; 068 069/** 070 * A renderer that connects data points with lines and/or draws shapes at each 071 * data point. This renderer is designed for use with the {@link XYPlot} 072 * class. The example shown here is generated by 073 * the {@code XYLineAndShapeRendererDemo2.java} program included in the 074 * JFreeChart demo collection: 075 * <br><br> 076 * <img src="doc-files/XYLineAndShapeRendererSample.png" 077 * alt="XYLineAndShapeRendererSample.png"> 078 * 079 */ 080public class XYLineAndShapeRenderer extends AbstractXYItemRenderer 081 implements XYItemRenderer, Cloneable, PublicCloneable, Serializable { 082 083 /** For serialization. */ 084 private static final long serialVersionUID = -7435246895986425885L; 085 086 /** 087 * A table of flags that control (per series) whether or not lines are 088 * visible. 089 */ 090 private BooleanList seriesLinesVisible; 091 092 /** The default value returned by the getLinesVisible() method. */ 093 private boolean defaultLinesVisible; 094 095 /** The shape that is used to represent a line in the legend. */ 096 private transient Shape legendLine; 097 098 /** 099 * A table of flags that control (per series) whether or not shapes are 100 * visible. 101 */ 102 private BooleanList seriesShapesVisible; 103 104 /** The default value returned by the getShapeVisible() method. */ 105 private boolean defaultShapesVisible; 106 107 /** 108 * A table of flags that control (per series) whether or not shapes are 109 * filled. 110 */ 111 private BooleanList seriesShapesFilled; 112 113 /** The default value returned by the getShapeFilled() method. */ 114 private boolean defaultShapesFilled; 115 116 /** A flag that controls whether outlines are drawn for shapes. */ 117 private boolean drawOutlines; 118 119 /** 120 * A flag that controls whether the fill paint is used for filling 121 * shapes. 122 */ 123 private boolean useFillPaint; 124 125 /** 126 * A flag that controls whether the outline paint is used for drawing shape 127 * outlines. 128 */ 129 private boolean useOutlinePaint; 130 131 /** 132 * A flag that controls whether or not each series is drawn as a single 133 * path. 134 */ 135 private boolean drawSeriesLineAsPath; 136 137 /** 138 * Creates a new renderer with both lines and shapes visible. 139 */ 140 public XYLineAndShapeRenderer() { 141 this(true, true); 142 } 143 144 /** 145 * Creates a new renderer. 146 * 147 * @param lines lines visible? 148 * @param shapes shapes visible? 149 */ 150 public XYLineAndShapeRenderer(boolean lines, boolean shapes) { 151 this.seriesLinesVisible = new BooleanList(); 152 this.defaultLinesVisible = lines; 153 this.legendLine = new Line2D.Double(-7.0, 0.0, 7.0, 0.0); 154 155 this.seriesShapesVisible = new BooleanList(); 156 this.defaultShapesVisible = shapes; 157 158 this.useFillPaint = false; // use item paint for fills by default 159 this.seriesShapesFilled = new BooleanList(); 160 this.defaultShapesFilled = true; 161 162 this.drawOutlines = true; 163 this.useOutlinePaint = false; // use item paint for outlines by 164 // default, not outline paint 165 166 this.drawSeriesLineAsPath = false; 167 } 168 169 /** 170 * Returns a flag that controls whether or not each series is drawn as a 171 * single path. The default value is {@code false}. 172 * 173 * @return A boolean. 174 * 175 * @see #setDrawSeriesLineAsPath(boolean) 176 */ 177 public boolean getDrawSeriesLineAsPath() { 178 return this.drawSeriesLineAsPath; 179 } 180 181 /** 182 * Sets the flag that controls whether or not each series is drawn as a 183 * single path and sends a {@link RendererChangeEvent} to all registered 184 * listeners. 185 * 186 * @param flag the flag. 187 * 188 * @see #getDrawSeriesLineAsPath() 189 */ 190 public void setDrawSeriesLineAsPath(boolean flag) { 191 if (this.drawSeriesLineAsPath != flag) { 192 this.drawSeriesLineAsPath = flag; 193 fireChangeEvent(); 194 } 195 } 196 197 /** 198 * Returns the number of passes through the data that the renderer requires 199 * in order to draw the chart. Most charts will require a single pass, but 200 * some require two passes. 201 * 202 * @return The pass count. 203 */ 204 @Override 205 public int getPassCount() { 206 return 2; 207 } 208 209 // LINES VISIBLE 210 211 /** 212 * Returns the flag used to control whether or not the shape for an item is 213 * visible. 214 * 215 * @param series the series index (zero-based). 216 * @param item the item index (zero-based). 217 * 218 * @return A boolean. 219 */ 220 public boolean getItemLineVisible(int series, int item) { 221 Boolean flag = getSeriesLinesVisible(series); 222 if (flag != null) { 223 return flag; 224 } 225 return this.defaultLinesVisible; 226 } 227 228 /** 229 * Returns the flag used to control whether or not the lines for a series 230 * are visible. 231 * 232 * @param series the series index (zero-based). 233 * 234 * @return The flag (possibly {@code null}). 235 * 236 * @see #setSeriesLinesVisible(int, Boolean) 237 */ 238 public Boolean getSeriesLinesVisible(int series) { 239 return this.seriesLinesVisible.getBoolean(series); 240 } 241 242 /** 243 * Sets the 'lines visible' flag for a series and sends a 244 * {@link RendererChangeEvent} to all registered listeners. 245 * 246 * @param series the series index (zero-based). 247 * @param flag the flag ({@code null} permitted). 248 * 249 * @see #getSeriesLinesVisible(int) 250 */ 251 public void setSeriesLinesVisible(int series, Boolean flag) { 252 this.seriesLinesVisible.setBoolean(series, flag); 253 fireChangeEvent(); 254 } 255 256 /** 257 * Sets the 'lines visible' flag for a series and sends a 258 * {@link RendererChangeEvent} to all registered listeners. 259 * 260 * @param series the series index (zero-based). 261 * @param visible the flag. 262 * 263 * @see #getSeriesLinesVisible(int) 264 */ 265 public void setSeriesLinesVisible(int series, boolean visible) { 266 setSeriesLinesVisible(series, Boolean.valueOf(visible)); 267 } 268 269 /** 270 * Returns the default 'lines visible' attribute. 271 * 272 * @return The default flag. 273 * 274 * @see #setDefaultLinesVisible(boolean) 275 */ 276 public boolean getDefaultLinesVisible() { 277 return this.defaultLinesVisible; 278 } 279 280 /** 281 * Sets the default 'lines visible' flag and sends a 282 * {@link RendererChangeEvent} to all registered listeners. 283 * 284 * @param flag the flag. 285 * 286 * @see #getDefaultLinesVisible() 287 */ 288 public void setDefaultLinesVisible(boolean flag) { 289 this.defaultLinesVisible = flag; 290 fireChangeEvent(); 291 } 292 293 /** 294 * Returns the shape used to represent a line in the legend. 295 * 296 * @return The legend line (never {@code null}). 297 * 298 * @see #setLegendLine(Shape) 299 */ 300 public Shape getLegendLine() { 301 return this.legendLine; 302 } 303 304 /** 305 * Sets the shape used as a line in each legend item and sends a 306 * {@link RendererChangeEvent} to all registered listeners. 307 * 308 * @param line the line ({@code null} not permitted). 309 * 310 * @see #getLegendLine() 311 */ 312 public void setLegendLine(Shape line) { 313 Args.nullNotPermitted(line, "line"); 314 this.legendLine = line; 315 fireChangeEvent(); 316 } 317 318 // SHAPES VISIBLE 319 320 /** 321 * Returns the flag used to control whether or not the shape for an item is 322 * visible. 323 * <p> 324 * The default implementation passes control to the 325 * {@code getSeriesShapesVisible()} 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 public boolean getItemShapeVisible(int series, int item) { 334 Boolean flag = getSeriesShapesVisible(series); 335 if (flag != null) { 336 return flag; 337 } 338 return this.defaultShapesVisible; 339 } 340 341 /** 342 * Returns the flag used to control whether or not the shapes for a series 343 * are visible. 344 * 345 * @param series the series index (zero-based). 346 * 347 * @return A boolean. 348 * 349 * @see #setSeriesShapesVisible(int, Boolean) 350 */ 351 public Boolean getSeriesShapesVisible(int series) { 352 return this.seriesShapesVisible.getBoolean(series); 353 } 354 355 /** 356 * Sets the 'shapes visible' flag for a series and sends a 357 * {@link RendererChangeEvent} to all registered listeners. 358 * 359 * @param series the series index (zero-based). 360 * @param visible the flag. 361 * 362 * @see #getSeriesShapesVisible(int) 363 */ 364 public void setSeriesShapesVisible(int series, boolean visible) { 365 setSeriesShapesVisible(series, Boolean.valueOf(visible)); 366 } 367 368 /** 369 * Sets the 'shapes visible' flag for a series and sends a 370 * {@link RendererChangeEvent} to all registered listeners. 371 * 372 * @param series the series index (zero-based). 373 * @param flag the flag. 374 * 375 * @see #getSeriesShapesVisible(int) 376 */ 377 public void setSeriesShapesVisible(int series, Boolean flag) { 378 this.seriesShapesVisible.setBoolean(series, flag); 379 fireChangeEvent(); 380 } 381 382 /** 383 * Returns the default 'shape visible' attribute. 384 * 385 * @return The default flag. 386 * 387 * @see #setDefaultShapesVisible(boolean) 388 */ 389 public boolean getDefaultShapesVisible() { 390 return this.defaultShapesVisible; 391 } 392 393 /** 394 * Sets the default 'shapes visible' flag and sends a 395 * {@link RendererChangeEvent} to all registered listeners. 396 * 397 * @param flag the flag. 398 * 399 * @see #getDefaultShapesVisible() 400 */ 401 public void setDefaultShapesVisible(boolean flag) { 402 this.defaultShapesVisible = flag; 403 fireChangeEvent(); 404 } 405 406 // SHAPES FILLED 407 408 /** 409 * Returns the flag used to control whether or not the shape for an item 410 * is filled. 411 * <p> 412 * The default implementation passes control to the 413 * {@code getSeriesShapesFilled} method. You can override this method 414 * if you require different behaviour. 415 * 416 * @param series the series index (zero-based). 417 * @param item the item index (zero-based). 418 * 419 * @return A boolean. 420 */ 421 public boolean getItemShapeFilled(int series, int item) { 422 Boolean flag = getSeriesShapesFilled(series); 423 if (flag != null) { 424 return flag; 425 } 426 return this.defaultShapesFilled; 427 428 } 429 430 /** 431 * Returns the flag used to control whether or not the shapes for a series 432 * are filled. 433 * 434 * @param series the series index (zero-based). 435 * 436 * @return A boolean. 437 * 438 * @see #setSeriesShapesFilled(int, Boolean) 439 */ 440 public Boolean getSeriesShapesFilled(int series) { 441 return this.seriesShapesFilled.getBoolean(series); 442 } 443 444 /** 445 * Sets the 'shapes filled' flag for a series and sends a 446 * {@link RendererChangeEvent} to all registered listeners. 447 * 448 * @param series the series index (zero-based). 449 * @param flag the flag. 450 * 451 * @see #getSeriesShapesFilled(int) 452 */ 453 public void setSeriesShapesFilled(int series, boolean flag) { 454 setSeriesShapesFilled(series, Boolean.valueOf(flag)); 455 } 456 457 /** 458 * Sets the 'shapes filled' flag for a series and sends a 459 * {@link RendererChangeEvent} to all registered listeners. 460 * 461 * @param series the series index (zero-based). 462 * @param flag the flag. 463 * 464 * @see #getSeriesShapesFilled(int) 465 */ 466 public void setSeriesShapesFilled(int series, Boolean flag) { 467 this.seriesShapesFilled.setBoolean(series, flag); 468 fireChangeEvent(); 469 } 470 471 /** 472 * Returns the default 'shape filled' attribute. 473 * 474 * @return The default flag. 475 * 476 * @see #setDefaultShapesFilled(boolean) 477 */ 478 public boolean getDefaultShapesFilled() { 479 return this.defaultShapesFilled; 480 } 481 482 /** 483 * Sets the default 'shapes filled' flag and sends a 484 * {@link RendererChangeEvent} to all registered listeners. 485 * 486 * @param flag the flag. 487 * 488 * @see #getDefaultShapesFilled() 489 */ 490 public void setDefaultShapesFilled(boolean flag) { 491 this.defaultShapesFilled = flag; 492 fireChangeEvent(); 493 } 494 495 /** 496 * Returns {@code true} if outlines should be drawn for shapes, and 497 * {@code false} otherwise. 498 * 499 * @return A boolean. 500 * 501 * @see #setDrawOutlines(boolean) 502 */ 503 public boolean getDrawOutlines() { 504 return this.drawOutlines; 505 } 506 507 /** 508 * Sets the flag that controls whether outlines are drawn for 509 * shapes, and sends a {@link RendererChangeEvent} to all registered 510 * listeners. 511 * <P> 512 * In some cases, shapes look better if they do NOT have an outline, but 513 * this flag allows you to set your own preference. 514 * 515 * @param flag the flag. 516 * 517 * @see #getDrawOutlines() 518 */ 519 public void setDrawOutlines(boolean flag) { 520 this.drawOutlines = flag; 521 fireChangeEvent(); 522 } 523 524 /** 525 * Returns {@code true} if the renderer should use the fill paint 526 * setting to fill shapes, and {@code false} if it should just 527 * use the regular paint. 528 * <p> 529 * Refer to {@code XYLineAndShapeRendererDemo2.java} to see the 530 * effect of this flag. 531 * 532 * @return A boolean. 533 * 534 * @see #setUseFillPaint(boolean) 535 * @see #getUseOutlinePaint() 536 */ 537 public boolean getUseFillPaint() { 538 return this.useFillPaint; 539 } 540 541 /** 542 * Sets the flag that controls whether the fill paint is used to fill 543 * shapes, and sends a {@link RendererChangeEvent} to all 544 * registered listeners. 545 * 546 * @param flag the flag. 547 * 548 * @see #getUseFillPaint() 549 */ 550 public void setUseFillPaint(boolean flag) { 551 this.useFillPaint = flag; 552 fireChangeEvent(); 553 } 554 555 /** 556 * Returns {@code true} if the renderer should use the outline paint 557 * setting to draw shape outlines, and {@code false} if it should just 558 * use the regular paint. 559 * 560 * @return A boolean. 561 * 562 * @see #setUseOutlinePaint(boolean) 563 * @see #getUseFillPaint() 564 */ 565 public boolean getUseOutlinePaint() { 566 return this.useOutlinePaint; 567 } 568 569 /** 570 * Sets the flag that controls whether the outline paint is used to draw 571 * shape outlines, and sends a {@link RendererChangeEvent} to all 572 * registered listeners. 573 * <p> 574 * Refer to {@code XYLineAndShapeRendererDemo2.java} to see the 575 * effect of this flag. 576 * 577 * @param flag the flag. 578 * 579 * @see #getUseOutlinePaint() 580 */ 581 public void setUseOutlinePaint(boolean flag) { 582 this.useOutlinePaint = flag; 583 fireChangeEvent(); 584 } 585 586 /** 587 * Records the state for the renderer. This is used to preserve state 588 * information between calls to the drawItem() method for a single chart 589 * drawing. 590 */ 591 public static class State extends XYItemRendererState { 592 593 /** The path for the current series. */ 594 public GeneralPath seriesPath; 595 596 /** 597 * A flag that indicates if the last (x, y) point was 'good' 598 * (non-null). 599 */ 600 private boolean lastPointGood; 601 602 /** 603 * Creates a new state instance. 604 * 605 * @param info the plot rendering info. 606 */ 607 public State(PlotRenderingInfo info) { 608 super(info); 609 this.seriesPath = new GeneralPath(); 610 } 611 612 /** 613 * Returns a flag that indicates if the last point drawn (in the 614 * current series) was 'good' (non-null). 615 * 616 * @return A boolean. 617 */ 618 public boolean isLastPointGood() { 619 return this.lastPointGood; 620 } 621 622 /** 623 * Sets a flag that indicates if the last point drawn (in the current 624 * series) was 'good' (non-null). 625 * 626 * @param good the flag. 627 */ 628 public void setLastPointGood(boolean good) { 629 this.lastPointGood = good; 630 } 631 632 /** 633 * This method is called by the {@link XYPlot} at the start of each 634 * series pass. We reset the state for the current series. 635 * 636 * @param dataset the dataset. 637 * @param series the series index. 638 * @param firstItem the first item index for this pass. 639 * @param lastItem the last item index for this pass. 640 * @param pass the current pass index. 641 * @param passCount the number of passes. 642 */ 643 @Override 644 public void startSeriesPass(XYDataset dataset, int series, 645 int firstItem, int lastItem, int pass, int passCount) { 646 this.seriesPath.reset(); 647 this.lastPointGood = false; 648 super.startSeriesPass(dataset, series, firstItem, lastItem, pass, 649 passCount); 650 } 651 652 } 653 654 /** 655 * Initialises the renderer. 656 * <P> 657 * This method will be called before the first item is rendered, giving the 658 * renderer an opportunity to initialise any state information it wants to 659 * maintain. The renderer can do nothing if it chooses. 660 * 661 * @param g2 the graphics device. 662 * @param dataArea the area inside the axes. 663 * @param plot the plot. 664 * @param data the data. 665 * @param info an optional info collection object to return data back to 666 * the caller. 667 * 668 * @return The renderer state. 669 */ 670 @Override 671 public XYItemRendererState initialise(Graphics2D g2, Rectangle2D dataArea, 672 XYPlot plot, XYDataset data, PlotRenderingInfo info) { 673 return new State(info); 674 } 675 676 /** 677 * Draws the visual representation of a single data item. 678 * 679 * @param g2 the graphics device. 680 * @param state the renderer state. 681 * @param dataArea the area within which the data is being drawn. 682 * @param info collects information about the drawing. 683 * @param plot the plot (can be used to obtain standard color 684 * information etc). 685 * @param domainAxis the domain axis. 686 * @param rangeAxis the range axis. 687 * @param dataset the dataset. 688 * @param series the series index (zero-based). 689 * @param item the item index (zero-based). 690 * @param crosshairState crosshair information for the plot 691 * ({@code null} permitted). 692 * @param pass the pass index. 693 */ 694 @Override 695 public void drawItem(Graphics2D g2, XYItemRendererState state, 696 Rectangle2D dataArea, PlotRenderingInfo info, XYPlot plot, 697 ValueAxis domainAxis, ValueAxis rangeAxis, XYDataset dataset, 698 int series, int item, CrosshairState crosshairState, int pass) { 699 700 // do nothing if item is not visible 701 if (!getItemVisible(series, item)) { 702 return; 703 } 704 705 // first pass draws the background (lines, for instance) 706 if (isLinePass(pass)) { 707 if (getItemLineVisible(series, item)) { 708 if (this.drawSeriesLineAsPath) { 709 drawPrimaryLineAsPath(state, g2, plot, dataset, pass, 710 series, item, domainAxis, rangeAxis, dataArea); 711 } 712 else { 713 drawPrimaryLine(state, g2, plot, dataset, pass, series, 714 item, domainAxis, rangeAxis, dataArea); 715 } 716 } 717 } 718 // second pass adds shapes where the items are .. 719 else if (isItemPass(pass)) { 720 721 // setup for collecting optional entity info... 722 EntityCollection entities = null; 723 if (info != null && info.getOwner() != null) { 724 entities = info.getOwner().getEntityCollection(); 725 } 726 727 drawSecondaryPass(g2, plot, dataset, pass, series, item, 728 domainAxis, dataArea, rangeAxis, crosshairState, entities); 729 } 730 } 731 732 /** 733 * Returns {@code true} if the specified pass is the one for drawing 734 * lines. 735 * 736 * @param pass the pass. 737 * 738 * @return A boolean. 739 */ 740 protected boolean isLinePass(int pass) { 741 return pass == 0; 742 } 743 744 /** 745 * Returns {@code true} if the specified pass is the one for drawing 746 * items. 747 * 748 * @param pass the pass. 749 * 750 * @return A boolean. 751 */ 752 protected boolean isItemPass(int pass) { 753 return pass == 1; 754 } 755 756 /** 757 * Draws the item (first pass). This method draws the lines 758 * connecting the items. 759 * 760 * @param g2 the graphics device. 761 * @param state the renderer state. 762 * @param dataArea the area within which the data is being drawn. 763 * @param plot the plot (can be used to obtain standard color 764 * information etc). 765 * @param domainAxis the domain axis. 766 * @param rangeAxis the range axis. 767 * @param dataset the dataset. 768 * @param pass the pass. 769 * @param series the series index (zero-based). 770 * @param item the item index (zero-based). 771 */ 772 protected void drawPrimaryLine(XYItemRendererState state, 773 Graphics2D g2, 774 XYPlot plot, 775 XYDataset dataset, 776 int pass, 777 int series, 778 int item, 779 ValueAxis domainAxis, 780 ValueAxis rangeAxis, 781 Rectangle2D dataArea) { 782 if (item == 0) { 783 return; 784 } 785 786 // get the data point... 787 double x1 = dataset.getXValue(series, item); 788 double y1 = dataset.getYValue(series, item); 789 if (Double.isNaN(y1) || Double.isNaN(x1)) { 790 return; 791 } 792 793 double x0 = dataset.getXValue(series, item - 1); 794 double y0 = dataset.getYValue(series, item - 1); 795 if (Double.isNaN(y0) || Double.isNaN(x0)) { 796 return; 797 } 798 799 RectangleEdge xAxisLocation = plot.getDomainAxisEdge(); 800 RectangleEdge yAxisLocation = plot.getRangeAxisEdge(); 801 802 double transX0 = domainAxis.valueToJava2D(x0, dataArea, xAxisLocation); 803 double transY0 = rangeAxis.valueToJava2D(y0, dataArea, yAxisLocation); 804 805 double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation); 806 double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation); 807 808 // only draw if we have good values 809 if (Double.isNaN(transX0) || Double.isNaN(transY0) 810 || Double.isNaN(transX1) || Double.isNaN(transY1)) { 811 return; 812 } 813 814 PlotOrientation orientation = plot.getOrientation(); 815 boolean visible; 816 if (orientation == PlotOrientation.HORIZONTAL) { 817 state.workingLine.setLine(transY0, transX0, transY1, transX1); 818 } 819 else if (orientation == PlotOrientation.VERTICAL) { 820 state.workingLine.setLine(transX0, transY0, transX1, transY1); 821 } 822 visible = LineUtils.clipLine(state.workingLine, dataArea); 823 if (visible) { 824 drawFirstPassShape(g2, pass, series, item, state.workingLine); 825 } 826 } 827 828 /** 829 * Draws the first pass shape. 830 * 831 * @param g2 the graphics device. 832 * @param pass the pass. 833 * @param series the series index. 834 * @param item the item index. 835 * @param shape the shape. 836 */ 837 protected void drawFirstPassShape(Graphics2D g2, int pass, int series, 838 int item, Shape shape) { 839 g2.setStroke(getItemStroke(series, item)); 840 g2.setPaint(getItemPaint(series, item)); 841 g2.draw(shape); 842 } 843 844 845 /** 846 * Draws the item (first pass). This method draws the lines 847 * connecting the items. Instead of drawing separate lines, 848 * a {@code GeneralPath} is constructed and drawn at the end of 849 * the series painting. 850 * 851 * @param g2 the graphics device. 852 * @param state the renderer state. 853 * @param plot the plot (can be used to obtain standard color information 854 * etc). 855 * @param dataset the dataset. 856 * @param pass the pass. 857 * @param series the series index (zero-based). 858 * @param item the item index (zero-based). 859 * @param domainAxis the domain axis. 860 * @param rangeAxis the range axis. 861 * @param dataArea the area within which the data is being drawn. 862 */ 863 protected void drawPrimaryLineAsPath(XYItemRendererState state, 864 Graphics2D g2, XYPlot plot, XYDataset dataset, int pass, 865 int series, int item, ValueAxis domainAxis, ValueAxis rangeAxis, 866 Rectangle2D dataArea) { 867 868 RectangleEdge xAxisLocation = plot.getDomainAxisEdge(); 869 RectangleEdge yAxisLocation = plot.getRangeAxisEdge(); 870 871 // get the data point... 872 double x1 = dataset.getXValue(series, item); 873 double y1 = dataset.getYValue(series, item); 874 double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation); 875 double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation); 876 877 State s = (State) state; 878 // update path to reflect latest point 879 if (!Double.isNaN(transX1) && !Double.isNaN(transY1)) { 880 float x = (float) transX1; 881 float y = (float) transY1; 882 PlotOrientation orientation = plot.getOrientation(); 883 if (orientation == PlotOrientation.HORIZONTAL) { 884 x = (float) transY1; 885 y = (float) transX1; 886 } 887 if (s.isLastPointGood()) { 888 s.seriesPath.lineTo(x, y); 889 } 890 else { 891 s.seriesPath.moveTo(x, y); 892 } 893 s.setLastPointGood(true); 894 } else { 895 s.setLastPointGood(false); 896 } 897 // if this is the last item, draw the path ... 898 if (item == s.getLastItemIndex()) { 899 // draw path 900 drawFirstPassShape(g2, pass, series, item, s.seriesPath); 901 } 902 } 903 904 /** 905 * Draws the item shapes and adds chart entities (second pass). This method 906 * draws the shapes which mark the item positions. If {@code entities} 907 * is not {@code null} it will be populated with entity information 908 * for points that fall within the data area. 909 * 910 * @param g2 the graphics device. 911 * @param plot the plot (can be used to obtain standard color 912 * information etc). 913 * @param domainAxis the domain axis. 914 * @param dataArea the area within which the data is being drawn. 915 * @param rangeAxis the range axis. 916 * @param dataset the dataset. 917 * @param pass the pass. 918 * @param series the series index (zero-based). 919 * @param item the item index (zero-based). 920 * @param crosshairState the crosshair state. 921 * @param entities the entity collection. 922 */ 923 protected void drawSecondaryPass(Graphics2D g2, XYPlot plot, 924 XYDataset dataset, int pass, int series, int item, 925 ValueAxis domainAxis, Rectangle2D dataArea, ValueAxis rangeAxis, 926 CrosshairState crosshairState, EntityCollection entities) { 927 928 Shape entityArea = null; 929 930 // get the data point... 931 double x1 = dataset.getXValue(series, item); 932 double y1 = dataset.getYValue(series, item); 933 if (Double.isNaN(y1) || Double.isNaN(x1)) { 934 return; 935 } 936 937 PlotOrientation orientation = plot.getOrientation(); 938 RectangleEdge xAxisLocation = plot.getDomainAxisEdge(); 939 RectangleEdge yAxisLocation = plot.getRangeAxisEdge(); 940 double transX1 = domainAxis.valueToJava2D(x1, dataArea, xAxisLocation); 941 double transY1 = rangeAxis.valueToJava2D(y1, dataArea, yAxisLocation); 942 943 if (getItemShapeVisible(series, item)) { 944 Shape shape = getItemShape(series, item); 945 if (orientation == PlotOrientation.HORIZONTAL) { 946 shape = ShapeUtils.createTranslatedShape(shape, transY1, 947 transX1); 948 } 949 else if (orientation == PlotOrientation.VERTICAL) { 950 shape = ShapeUtils.createTranslatedShape(shape, transX1, 951 transY1); 952 } 953 entityArea = shape; 954 if (shape.intersects(dataArea)) { 955 if (getItemShapeFilled(series, item)) { 956 if (this.useFillPaint) { 957 g2.setPaint(getItemFillPaint(series, item)); 958 } 959 else { 960 g2.setPaint(getItemPaint(series, item)); 961 } 962 g2.fill(shape); 963 } 964 if (this.drawOutlines) { 965 if (getUseOutlinePaint()) { 966 g2.setPaint(getItemOutlinePaint(series, item)); 967 } 968 else { 969 g2.setPaint(getItemPaint(series, item)); 970 } 971 g2.setStroke(getItemOutlineStroke(series, item)); 972 g2.draw(shape); 973 } 974 } 975 } 976 977 double xx = transX1; 978 double yy = transY1; 979 if (orientation == PlotOrientation.HORIZONTAL) { 980 xx = transY1; 981 yy = transX1; 982 } 983 984 // draw the item label if there is one... 985 if (isItemLabelVisible(series, item)) { 986 drawItemLabel(g2, orientation, dataset, series, item, xx, yy, 987 (y1 < 0.0)); 988 } 989 990 int datasetIndex = plot.indexOf(dataset); 991 updateCrosshairValues(crosshairState, x1, y1, datasetIndex, 992 transX1, transY1, orientation); 993 994 // add an entity for the item, but only if it falls within the data 995 // area... 996 if (entities != null && ShapeUtils.isPointInRect(dataArea, xx, yy)) { 997 addEntity(entities, entityArea, dataset, series, item, xx, yy); 998 } 999 } 1000 1001 1002 /** 1003 * Returns a legend item for the specified series. 1004 * 1005 * @param datasetIndex the dataset index (zero-based). 1006 * @param series the series index (zero-based). 1007 * 1008 * @return A legend item for the series (possibly {@code null}). 1009 */ 1010 @Override 1011 public LegendItem getLegendItem(int datasetIndex, int series) { 1012 XYPlot plot = getPlot(); 1013 if (plot == null) { 1014 return null; 1015 } 1016 1017 XYDataset dataset = plot.getDataset(datasetIndex); 1018 if (dataset == null) { 1019 return null; 1020 } 1021 1022 if (!getItemVisible(series, 0)) { 1023 return null; 1024 } 1025 String label = getLegendItemLabelGenerator().generateLabel(dataset, 1026 series); 1027 String description = label; 1028 String toolTipText = null; 1029 if (getLegendItemToolTipGenerator() != null) { 1030 toolTipText = getLegendItemToolTipGenerator().generateLabel( 1031 dataset, series); 1032 } 1033 String urlText = null; 1034 if (getLegendItemURLGenerator() != null) { 1035 urlText = getLegendItemURLGenerator().generateLabel(dataset, 1036 series); 1037 } 1038 boolean shapeIsVisible = getItemShapeVisible(series, 0); 1039 Shape shape = lookupLegendShape(series); 1040 boolean shapeIsFilled = getItemShapeFilled(series, 0); 1041 Paint fillPaint = (this.useFillPaint ? lookupSeriesFillPaint(series) 1042 : lookupSeriesPaint(series)); 1043 boolean shapeOutlineVisible = this.drawOutlines; 1044 Paint outlinePaint = (this.useOutlinePaint ? lookupSeriesOutlinePaint( 1045 series) : lookupSeriesPaint(series)); 1046 Stroke outlineStroke = lookupSeriesOutlineStroke(series); 1047 boolean lineVisible = getItemLineVisible(series, 0); 1048 Stroke lineStroke = lookupSeriesStroke(series); 1049 Paint linePaint = lookupSeriesPaint(series); 1050 LegendItem result = new LegendItem(label, description, toolTipText, 1051 urlText, shapeIsVisible, shape, shapeIsFilled, fillPaint, 1052 shapeOutlineVisible, outlinePaint, outlineStroke, lineVisible, 1053 this.legendLine, lineStroke, linePaint); 1054 result.setLabelFont(lookupLegendTextFont(series)); 1055 Paint labelPaint = lookupLegendTextPaint(series); 1056 if (labelPaint != null) { 1057 result.setLabelPaint(labelPaint); 1058 } 1059 result.setSeriesKey(dataset.getSeriesKey(series)); 1060 result.setSeriesIndex(series); 1061 result.setDataset(dataset); 1062 result.setDatasetIndex(datasetIndex); 1063 1064 return result; 1065 } 1066 1067 /** 1068 * Returns a clone of the renderer. 1069 * 1070 * @return A clone. 1071 * 1072 * @throws CloneNotSupportedException if the clone cannot be created. 1073 */ 1074 @Override 1075 public Object clone() throws CloneNotSupportedException { 1076 XYLineAndShapeRenderer clone = (XYLineAndShapeRenderer) super.clone(); 1077 clone.seriesLinesVisible 1078 = (BooleanList) this.seriesLinesVisible.clone(); 1079 if (this.legendLine != null) { 1080 clone.legendLine = ShapeUtils.clone(this.legendLine); 1081 } 1082 clone.seriesShapesVisible 1083 = (BooleanList) this.seriesShapesVisible.clone(); 1084 clone.seriesShapesFilled 1085 = (BooleanList) this.seriesShapesFilled.clone(); 1086 return clone; 1087 } 1088 1089 /** 1090 * Tests this renderer for equality with an arbitrary object. 1091 * 1092 * @param obj the object ({@code null} permitted). 1093 * 1094 * @return {@code true} or {@code false}. 1095 */ 1096 @Override 1097 public boolean equals(Object obj) { 1098 if (obj == this) { 1099 return true; 1100 } 1101 if (!(obj instanceof XYLineAndShapeRenderer)) { 1102 return false; 1103 } 1104 if (!super.equals(obj)) { 1105 return false; 1106 } 1107 XYLineAndShapeRenderer that = (XYLineAndShapeRenderer) obj; 1108 if (!Objects.equals( 1109 this.seriesLinesVisible, that.seriesLinesVisible) 1110 ) { 1111 return false; 1112 } 1113 if (this.defaultLinesVisible != that.defaultLinesVisible) { 1114 return false; 1115 } 1116 if (!ShapeUtils.equal(this.legendLine, that.legendLine)) { 1117 return false; 1118 } 1119 if (!Objects.equals( 1120 this.seriesShapesVisible, that.seriesShapesVisible) 1121 ) { 1122 return false; 1123 } 1124 if (this.defaultShapesVisible != that.defaultShapesVisible) { 1125 return false; 1126 } 1127 if (!Objects.equals( 1128 this.seriesShapesFilled, that.seriesShapesFilled) 1129 ) { 1130 return false; 1131 } 1132 if (this.defaultShapesFilled != that.defaultShapesFilled) { 1133 return false; 1134 } 1135 if (this.drawOutlines != that.drawOutlines) { 1136 return false; 1137 } 1138 if (this.useOutlinePaint != that.useOutlinePaint) { 1139 return false; 1140 } 1141 if (this.useFillPaint != that.useFillPaint) { 1142 return false; 1143 } 1144 if (this.drawSeriesLineAsPath != that.drawSeriesLineAsPath) { 1145 return false; 1146 } 1147 return true; 1148 } 1149 1150 /** 1151 * Provides serialization support. 1152 * 1153 * @param stream the input stream. 1154 * 1155 * @throws IOException if there is an I/O error. 1156 * @throws ClassNotFoundException if there is a classpath problem. 1157 */ 1158 private void readObject(ObjectInputStream stream) 1159 throws IOException, ClassNotFoundException { 1160 stream.defaultReadObject(); 1161 this.legendLine = SerialUtils.readShape(stream); 1162 } 1163 1164 /** 1165 * Provides serialization support. 1166 * 1167 * @param stream the output stream. 1168 * 1169 * @throws IOException if there is an I/O error. 1170 */ 1171 private void writeObject(ObjectOutputStream stream) throws IOException { 1172 stream.defaultWriteObject(); 1173 SerialUtils.writeShape(this.legendLine, stream); 1174 } 1175 1176}