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