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