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 * SpiderWebPlot.java 029 * ------------------ 030 * (C) Copyright 2005-present, by Heaps of Flavour Pty Ltd and Contributors. 031 * 032 * Company Info: http://www.i4-talent.com 033 * 034 * Original Author: Don Elliott; 035 * Contributor(s): David Gilbert; 036 * Nina Jeliazkova; 037 * 038 */ 039 040package org.jfree.chart.plot; 041 042import java.awt.AlphaComposite; 043import java.awt.BasicStroke; 044import java.awt.Color; 045import java.awt.Composite; 046import java.awt.Font; 047import java.awt.Graphics2D; 048import java.awt.Paint; 049import java.awt.Polygon; 050import java.awt.Rectangle; 051import java.awt.Shape; 052import java.awt.Stroke; 053import java.awt.font.FontRenderContext; 054import java.awt.font.LineMetrics; 055import java.awt.geom.Arc2D; 056import java.awt.geom.Ellipse2D; 057import java.awt.geom.Line2D; 058import java.awt.geom.Point2D; 059import java.awt.geom.Rectangle2D; 060import java.io.IOException; 061import java.io.ObjectInputStream; 062import java.io.ObjectOutputStream; 063import java.io.Serializable; 064import java.util.Iterator; 065import java.util.List; 066import java.util.Objects; 067 068import org.jfree.chart.LegendItem; 069import org.jfree.chart.LegendItemCollection; 070import org.jfree.chart.entity.CategoryItemEntity; 071import org.jfree.chart.entity.EntityCollection; 072import org.jfree.chart.event.PlotChangeEvent; 073import org.jfree.chart.labels.CategoryItemLabelGenerator; 074import org.jfree.chart.labels.CategoryToolTipGenerator; 075import org.jfree.chart.labels.StandardCategoryItemLabelGenerator; 076import org.jfree.chart.ui.RectangleInsets; 077import org.jfree.chart.urls.CategoryURLGenerator; 078import org.jfree.chart.util.PaintList; 079import org.jfree.chart.util.PaintUtils; 080import org.jfree.chart.util.Args; 081import org.jfree.chart.util.Rotation; 082import org.jfree.chart.util.SerialUtils; 083import org.jfree.chart.util.ShapeUtils; 084import org.jfree.chart.util.StrokeList; 085import org.jfree.chart.util.TableOrder; 086import org.jfree.data.category.CategoryDataset; 087import org.jfree.data.general.DatasetChangeEvent; 088import org.jfree.data.general.DatasetUtils; 089 090/** 091 * A plot that displays data from a {@link CategoryDataset} in the form of a 092 * "spider web". Multiple series can be plotted on the same axis to allow 093 * easy comparison. This plot doesn't support negative values at present. 094 */ 095public class SpiderWebPlot extends Plot implements Cloneable, Serializable { 096 097 /** For serialization. */ 098 private static final long serialVersionUID = -5376340422031599463L; 099 100 /** The default head radius percent (currently 1%). */ 101 public static final double DEFAULT_HEAD = 0.01; 102 103 /** The default axis label gap (currently 10%). */ 104 public static final double DEFAULT_AXIS_LABEL_GAP = 0.10; 105 106 /** The default interior gap. */ 107 public static final double DEFAULT_INTERIOR_GAP = 0.25; 108 109 /** The maximum interior gap (currently 40%). */ 110 public static final double MAX_INTERIOR_GAP = 0.40; 111 112 /** The default starting angle for the radar chart axes. */ 113 public static final double DEFAULT_START_ANGLE = 90.0; 114 115 /** The default series label font. */ 116 public static final Font DEFAULT_LABEL_FONT = new Font("SansSerif", 117 Font.PLAIN, 10); 118 119 /** The default series label paint. */ 120 public static final Paint DEFAULT_LABEL_PAINT = Color.BLACK; 121 122 /** The default series label background paint. */ 123 public static final Paint DEFAULT_LABEL_BACKGROUND_PAINT 124 = new Color(255, 255, 192); 125 126 /** The default series label outline paint. */ 127 public static final Paint DEFAULT_LABEL_OUTLINE_PAINT = Color.BLACK; 128 129 /** The default series label outline stroke. */ 130 public static final Stroke DEFAULT_LABEL_OUTLINE_STROKE 131 = new BasicStroke(0.5f); 132 133 /** The default series label shadow paint. */ 134 public static final Paint DEFAULT_LABEL_SHADOW_PAINT = Color.LIGHT_GRAY; 135 136 /** 137 * The default maximum value plotted - forces the plot to evaluate 138 * the maximum from the data passed in 139 */ 140 public static final double DEFAULT_MAX_VALUE = -1.0; 141 142 /** The head radius as a percentage of the available drawing area. */ 143 protected double headPercent; 144 145 /** The space left around the outside of the plot as a percentage. */ 146 private double interiorGap; 147 148 /** The gap between the labels and the axes as a %age of the radius. */ 149 private double axisLabelGap; 150 151 /** 152 * The paint used to draw the axis lines. 153 */ 154 private transient Paint axisLinePaint; 155 156 /** 157 * The stroke used to draw the axis lines. 158 */ 159 private transient Stroke axisLineStroke; 160 161 /** The dataset. */ 162 private CategoryDataset dataset; 163 164 /** The maximum value we are plotting against on each category axis */ 165 private double maxValue; 166 167 /** 168 * The data extract order (BY_ROW or BY_COLUMN). This denotes whether 169 * the data series are stored in rows (in which case the category names are 170 * derived from the column keys) or in columns (in which case the category 171 * names are derived from the row keys). 172 */ 173 private TableOrder dataExtractOrder; 174 175 /** The starting angle. */ 176 private double startAngle; 177 178 /** The direction for drawing the radar axis and plots. */ 179 private Rotation direction; 180 181 /** The legend item shape. */ 182 private transient Shape legendItemShape; 183 184 /** The paint for ALL series (overrides list). */ 185 private transient Paint seriesPaint; 186 187 /** The series paint list. */ 188 private PaintList seriesPaintList; 189 190 /** The base series paint (fallback). */ 191 private transient Paint baseSeriesPaint; 192 193 /** The outline paint for ALL series (overrides list). */ 194 private transient Paint seriesOutlinePaint; 195 196 /** The series outline paint list. */ 197 private PaintList seriesOutlinePaintList; 198 199 /** The base series outline paint (fallback). */ 200 private transient Paint baseSeriesOutlinePaint; 201 202 /** The outline stroke for ALL series (overrides list). */ 203 private transient Stroke seriesOutlineStroke; 204 205 /** The series outline stroke list. */ 206 private StrokeList seriesOutlineStrokeList; 207 208 /** The base series outline stroke (fallback). */ 209 private transient Stroke baseSeriesOutlineStroke; 210 211 /** The font used to display the category labels. */ 212 private Font labelFont; 213 214 /** The color used to draw the category labels. */ 215 private transient Paint labelPaint; 216 217 /** The label generator. */ 218 private CategoryItemLabelGenerator labelGenerator; 219 220 /** controls if the web polygons are filled or not */ 221 private boolean webFilled = true; 222 223 /** The alpha value of the fill portion of a polygon. */ 224 private float webFillAlpha = 0.1F; 225 226 /** A tooltip generator for the plot ({@code null} permitted). */ 227 private CategoryToolTipGenerator toolTipGenerator; 228 229 /** A URL generator for the plot ({@code null} permitted). */ 230 private CategoryURLGenerator urlGenerator; 231 232 /** 233 * Creates a default plot with no dataset. 234 */ 235 public SpiderWebPlot() { 236 this(null); 237 } 238 239 /** 240 * Creates a new spider web plot with the given dataset, with each row 241 * representing a series. 242 * 243 * @param dataset the dataset ({@code null} permitted). 244 */ 245 public SpiderWebPlot(CategoryDataset dataset) { 246 this(dataset, TableOrder.BY_ROW); 247 } 248 249 /** 250 * Creates a new spider web plot with the given dataset. 251 * 252 * @param dataset the dataset. 253 * @param extract controls how data is extracted ({@link TableOrder#BY_ROW} 254 * or {@link TableOrder#BY_COLUMN}). 255 */ 256 public SpiderWebPlot(CategoryDataset dataset, TableOrder extract) { 257 super(); 258 Args.nullNotPermitted(extract, "extract"); 259 this.dataset = dataset; 260 if (dataset != null) { 261 dataset.addChangeListener(this); 262 } 263 264 this.dataExtractOrder = extract; 265 this.headPercent = DEFAULT_HEAD; 266 this.axisLabelGap = DEFAULT_AXIS_LABEL_GAP; 267 this.axisLinePaint = Color.BLACK; 268 this.axisLineStroke = new BasicStroke(1.0f); 269 270 this.interiorGap = DEFAULT_INTERIOR_GAP; 271 this.startAngle = DEFAULT_START_ANGLE; 272 this.direction = Rotation.CLOCKWISE; 273 this.maxValue = DEFAULT_MAX_VALUE; 274 275 this.seriesPaint = null; 276 this.seriesPaintList = new PaintList(); 277 this.baseSeriesPaint = null; 278 279 this.seriesOutlinePaint = null; 280 this.seriesOutlinePaintList = new PaintList(); 281 this.baseSeriesOutlinePaint = DEFAULT_OUTLINE_PAINT; 282 283 this.seriesOutlineStroke = null; 284 this.seriesOutlineStrokeList = new StrokeList(); 285 this.baseSeriesOutlineStroke = DEFAULT_OUTLINE_STROKE; 286 287 this.labelFont = DEFAULT_LABEL_FONT; 288 this.labelPaint = DEFAULT_LABEL_PAINT; 289 this.labelGenerator = new StandardCategoryItemLabelGenerator(); 290 291 this.legendItemShape = DEFAULT_LEGEND_ITEM_CIRCLE; 292 } 293 294 /** 295 * Returns a short string describing the type of plot. 296 * 297 * @return The plot type. 298 */ 299 @Override 300 public String getPlotType() { 301 // return localizationResources.getString("Radar_Plot"); 302 return ("Spider Web Plot"); 303 } 304 305 /** 306 * Returns the dataset. 307 * 308 * @return The dataset (possibly {@code null}). 309 * 310 * @see #setDataset(CategoryDataset) 311 */ 312 public CategoryDataset getDataset() { 313 return this.dataset; 314 } 315 316 /** 317 * Sets the dataset used by the plot and sends a {@link PlotChangeEvent} 318 * to all registered listeners. 319 * 320 * @param dataset the dataset ({@code null} permitted). 321 * 322 * @see #getDataset() 323 */ 324 public void setDataset(CategoryDataset dataset) { 325 // if there is an existing dataset, remove the plot from the list of 326 // change listeners... 327 if (this.dataset != null) { 328 this.dataset.removeChangeListener(this); 329 } 330 331 // set the new dataset, and register the chart as a change listener... 332 this.dataset = dataset; 333 if (dataset != null) { 334 setDatasetGroup(dataset.getGroup()); 335 dataset.addChangeListener(this); 336 } 337 338 // send a dataset change event to self to trigger plot change event 339 datasetChanged(new DatasetChangeEvent(this, dataset)); 340 } 341 342 /** 343 * Method to determine if the web chart is to be filled. 344 * 345 * @return A boolean. 346 * 347 * @see #setWebFilled(boolean) 348 */ 349 public boolean isWebFilled() { 350 return this.webFilled; 351 } 352 353 /** 354 * Sets the webFilled flag and sends a {@link PlotChangeEvent} to all 355 * registered listeners. 356 * 357 * @param flag the flag. 358 * 359 * @see #isWebFilled() 360 */ 361 public void setWebFilled(boolean flag) { 362 this.webFilled = flag; 363 fireChangeEvent(); 364 } 365 366 /** 367 * Returns the alpha value for filling a graph (in the range 0.0 to 1.0). 368 * 369 * @return The alpha value for filling a spider plot polygon. 370 * 371 * @see #setWebFillAlpha(float) 372 */ 373 public float getWebFillAlpha() { 374 return webFillAlpha; 375 } 376 377 /** 378 * Sets the alpha value for the fill of a plot polygon and sends a {@link PlotChangeEvent} to all 379 * registered listeners. 380 * 381 * @param alpha the new alpha value. If it is outside [0,1] it will be corrected to fit the range. 382 * @see #getWebFillAlpha() 383 */ 384 public void setWebFillAlpha(float alpha) { 385 this.webFillAlpha = alpha; 386 if (webFillAlpha < 0f) { 387 webFillAlpha = 0f; 388 } else if (webFillAlpha > 1f) { 389 webFillAlpha = 1f; 390 } 391 fireChangeEvent(); 392 } 393 394 /** 395 * Returns the data extract order (by row or by column). 396 * 397 * @return The data extract order (never {@code null}). 398 * 399 * @see #setDataExtractOrder(TableOrder) 400 */ 401 public TableOrder getDataExtractOrder() { 402 return this.dataExtractOrder; 403 } 404 405 /** 406 * Sets the data extract order (by row or by column) and sends a 407 * {@link PlotChangeEvent}to all registered listeners. 408 * 409 * @param order the order ({@code null} not permitted). 410 * 411 * @throws IllegalArgumentException if {@code order} is 412 * {@code null}. 413 * 414 * @see #getDataExtractOrder() 415 */ 416 public void setDataExtractOrder(TableOrder order) { 417 Args.nullNotPermitted(order, "order"); 418 this.dataExtractOrder = order; 419 fireChangeEvent(); 420 } 421 422 /** 423 * Returns the head percent (the default value is 0.01). 424 * 425 * @return The head percent (always > 0). 426 * 427 * @see #setHeadPercent(double) 428 */ 429 public double getHeadPercent() { 430 return this.headPercent; 431 } 432 433 /** 434 * Sets the head percent and sends a {@link PlotChangeEvent} to all 435 * registered listeners. Note that 0.10 is 10 percent. 436 * 437 * @param percent the percent (must be greater than zero). 438 * 439 * @see #getHeadPercent() 440 */ 441 public void setHeadPercent(double percent) { 442 Args.requireNonNegative(percent, "percent"); 443 this.headPercent = percent; 444 fireChangeEvent(); 445 } 446 447 /** 448 * Returns the start angle for the first radar axis. 449 * <BR> 450 * This is measured in degrees starting from 3 o'clock (Java Arc2D default) 451 * and measuring anti-clockwise. 452 * 453 * @return The start angle. 454 * 455 * @see #setStartAngle(double) 456 */ 457 public double getStartAngle() { 458 return this.startAngle; 459 } 460 461 /** 462 * Sets the starting angle and sends a {@link PlotChangeEvent} to all 463 * registered listeners. 464 * <P> 465 * The initial default value is 90 degrees, which corresponds to 12 o'clock. 466 * A value of zero corresponds to 3 o'clock... this is the encoding used by 467 * Java's Arc2D class. 468 * 469 * @param angle the angle (in degrees). 470 * 471 * @see #getStartAngle() 472 */ 473 public void setStartAngle(double angle) { 474 this.startAngle = angle; 475 fireChangeEvent(); 476 } 477 478 /** 479 * Returns the maximum value any category axis can take. 480 * 481 * @return The maximum value. 482 * 483 * @see #setMaxValue(double) 484 */ 485 public double getMaxValue() { 486 return this.maxValue; 487 } 488 489 /** 490 * Sets the maximum value any category axis can take and sends 491 * a {@link PlotChangeEvent} to all registered listeners. 492 * 493 * @param value the maximum value. 494 * 495 * @see #getMaxValue() 496 */ 497 public void setMaxValue(double value) { 498 this.maxValue = value; 499 fireChangeEvent(); 500 } 501 502 /** 503 * Returns the direction in which the radar axes are drawn 504 * (clockwise or anti-clockwise). 505 * 506 * @return The direction (never {@code null}). 507 * 508 * @see #setDirection(Rotation) 509 */ 510 public Rotation getDirection() { 511 return this.direction; 512 } 513 514 /** 515 * Sets the direction in which the radar axes are drawn and sends a 516 * {@link PlotChangeEvent} to all registered listeners. 517 * 518 * @param direction the direction ({@code null} not permitted). 519 * 520 * @see #getDirection() 521 */ 522 public void setDirection(Rotation direction) { 523 Args.nullNotPermitted(direction, "direction"); 524 this.direction = direction; 525 fireChangeEvent(); 526 } 527 528 /** 529 * Returns the interior gap, measured as a percentage of the available 530 * drawing space. 531 * 532 * @return The gap (as a percentage of the available drawing space). 533 * 534 * @see #setInteriorGap(double) 535 */ 536 public double getInteriorGap() { 537 return this.interiorGap; 538 } 539 540 /** 541 * Sets the interior gap and sends a {@link PlotChangeEvent} to all 542 * registered listeners. This controls the space between the edges of the 543 * plot and the plot area itself (the region where the axis labels appear). 544 * 545 * @param percent the gap (as a percentage of the available drawing space). 546 * 547 * @see #getInteriorGap() 548 */ 549 public void setInteriorGap(double percent) { 550 if ((percent < 0.0) || (percent > MAX_INTERIOR_GAP)) { 551 throw new IllegalArgumentException( 552 "Percentage outside valid range."); 553 } 554 if (this.interiorGap != percent) { 555 this.interiorGap = percent; 556 fireChangeEvent(); 557 } 558 } 559 560 /** 561 * Returns the axis label gap. 562 * 563 * @return The axis label gap. 564 * 565 * @see #setAxisLabelGap(double) 566 */ 567 public double getAxisLabelGap() { 568 return this.axisLabelGap; 569 } 570 571 /** 572 * Sets the axis label gap and sends a {@link PlotChangeEvent} to all 573 * registered listeners. 574 * 575 * @param gap the gap. 576 * 577 * @see #getAxisLabelGap() 578 */ 579 public void setAxisLabelGap(double gap) { 580 this.axisLabelGap = gap; 581 fireChangeEvent(); 582 } 583 584 /** 585 * Returns the paint used to draw the axis lines. 586 * 587 * @return The paint used to draw the axis lines (never {@code null}). 588 * 589 * @see #setAxisLinePaint(Paint) 590 * @see #getAxisLineStroke() 591 */ 592 public Paint getAxisLinePaint() { 593 return this.axisLinePaint; 594 } 595 596 /** 597 * Sets the paint used to draw the axis lines and sends a 598 * {@link PlotChangeEvent} to all registered listeners. 599 * 600 * @param paint the paint ({@code null} not permitted). 601 * 602 * @see #getAxisLinePaint() 603 */ 604 public void setAxisLinePaint(Paint paint) { 605 Args.nullNotPermitted(paint, "paint"); 606 this.axisLinePaint = paint; 607 fireChangeEvent(); 608 } 609 610 /** 611 * Returns the stroke used to draw the axis lines. 612 * 613 * @return The stroke used to draw the axis lines (never {@code null}). 614 * 615 * @see #setAxisLineStroke(Stroke) 616 * @see #getAxisLinePaint() 617 */ 618 public Stroke getAxisLineStroke() { 619 return this.axisLineStroke; 620 } 621 622 /** 623 * Sets the stroke used to draw the axis lines and sends a 624 * {@link PlotChangeEvent} to all registered listeners. 625 * 626 * @param stroke the stroke ({@code null} not permitted). 627 * 628 * @see #getAxisLineStroke() 629 */ 630 public void setAxisLineStroke(Stroke stroke) { 631 Args.nullNotPermitted(stroke, "stroke"); 632 this.axisLineStroke = stroke; 633 fireChangeEvent(); 634 } 635 636 //// SERIES PAINT ///////////////////////// 637 638 /** 639 * Returns the paint for ALL series in the plot. 640 * 641 * @return The paint (possibly {@code null}). 642 * 643 * @see #setSeriesPaint(Paint) 644 */ 645 public Paint getSeriesPaint() { 646 return this.seriesPaint; 647 } 648 649 /** 650 * Sets the paint for ALL series in the plot. If this is set to 651 * {@code null}, then a list of paints is used instead (to allow different 652 * colors to be used for each series of the radar group). 653 * 654 * @param paint the paint ({@code null} permitted). 655 * 656 * @see #getSeriesPaint() 657 */ 658 public void setSeriesPaint(Paint paint) { 659 this.seriesPaint = paint; 660 fireChangeEvent(); 661 } 662 663 /** 664 * Returns the paint for the specified series. 665 * 666 * @param series the series index (zero-based). 667 * 668 * @return The paint (never {@code null}). 669 * 670 * @see #setSeriesPaint(int, Paint) 671 */ 672 public Paint getSeriesPaint(int series) { 673 674 // return the override, if there is one... 675 if (this.seriesPaint != null) { 676 return this.seriesPaint; 677 } 678 679 // otherwise look up the paint list 680 Paint result = this.seriesPaintList.getPaint(series); 681 if (result == null) { 682 DrawingSupplier supplier = getDrawingSupplier(); 683 if (supplier != null) { 684 Paint p = supplier.getNextPaint(); 685 this.seriesPaintList.setPaint(series, p); 686 result = p; 687 } 688 else { 689 result = this.baseSeriesPaint; 690 } 691 } 692 return result; 693 694 } 695 696 /** 697 * Sets the paint used to fill a series of the radar and sends a 698 * {@link PlotChangeEvent} to all registered listeners. 699 * 700 * @param series the series index (zero-based). 701 * @param paint the paint ({@code null} permitted). 702 * 703 * @see #getSeriesPaint(int) 704 */ 705 public void setSeriesPaint(int series, Paint paint) { 706 this.seriesPaintList.setPaint(series, paint); 707 fireChangeEvent(); 708 } 709 710 /** 711 * Returns the base series paint. This is used when no other paint is 712 * available. 713 * 714 * @return The paint (never {@code null}). 715 * 716 * @see #setBaseSeriesPaint(Paint) 717 */ 718 public Paint getBaseSeriesPaint() { 719 return this.baseSeriesPaint; 720 } 721 722 /** 723 * Sets the base series paint. 724 * 725 * @param paint the paint ({@code null} not permitted). 726 * 727 * @see #getBaseSeriesPaint() 728 */ 729 public void setBaseSeriesPaint(Paint paint) { 730 Args.nullNotPermitted(paint, "paint"); 731 this.baseSeriesPaint = paint; 732 fireChangeEvent(); 733 } 734 735 //// SERIES OUTLINE PAINT //////////////////////////// 736 737 /** 738 * Returns the outline paint for ALL series in the plot. 739 * 740 * @return The paint (possibly {@code null}). 741 */ 742 public Paint getSeriesOutlinePaint() { 743 return this.seriesOutlinePaint; 744 } 745 746 /** 747 * Sets the outline paint for ALL series in the plot. If this is set to 748 * {@code null}, then a list of paints is used instead (to allow 749 * different colors to be used for each series). 750 * 751 * @param paint the paint ({@code null} permitted). 752 */ 753 public void setSeriesOutlinePaint(Paint paint) { 754 this.seriesOutlinePaint = paint; 755 fireChangeEvent(); 756 } 757 758 /** 759 * Returns the paint for the specified series. 760 * 761 * @param series the series index (zero-based). 762 * 763 * @return The paint (never {@code null}). 764 */ 765 public Paint getSeriesOutlinePaint(int series) { 766 // return the override, if there is one... 767 if (this.seriesOutlinePaint != null) { 768 return this.seriesOutlinePaint; 769 } 770 // otherwise look up the paint list 771 Paint result = this.seriesOutlinePaintList.getPaint(series); 772 if (result == null) { 773 result = this.baseSeriesOutlinePaint; 774 } 775 return result; 776 } 777 778 /** 779 * Sets the paint used to fill a series of the radar and sends a 780 * {@link PlotChangeEvent} to all registered listeners. 781 * 782 * @param series the series index (zero-based). 783 * @param paint the paint ({@code null} permitted). 784 */ 785 public void setSeriesOutlinePaint(int series, Paint paint) { 786 this.seriesOutlinePaintList.setPaint(series, paint); 787 fireChangeEvent(); 788 } 789 790 /** 791 * Returns the base series paint. This is used when no other paint is 792 * available. 793 * 794 * @return The paint (never {@code null}). 795 */ 796 public Paint getBaseSeriesOutlinePaint() { 797 return this.baseSeriesOutlinePaint; 798 } 799 800 /** 801 * Sets the base series paint. 802 * 803 * @param paint the paint ({@code null} not permitted). 804 */ 805 public void setBaseSeriesOutlinePaint(Paint paint) { 806 Args.nullNotPermitted(paint, "paint"); 807 this.baseSeriesOutlinePaint = paint; 808 fireChangeEvent(); 809 } 810 811 //// SERIES OUTLINE STROKE ///////////////////// 812 813 /** 814 * Returns the outline stroke for ALL series in the plot. 815 * 816 * @return The stroke (possibly {@code null}). 817 */ 818 public Stroke getSeriesOutlineStroke() { 819 return this.seriesOutlineStroke; 820 } 821 822 /** 823 * Sets the outline stroke for ALL series in the plot. If this is set to 824 * {@code null}, then a list of paints is used instead (to allow 825 * different colors to be used for each series). 826 * 827 * @param stroke the stroke ({@code null} permitted). 828 */ 829 public void setSeriesOutlineStroke(Stroke stroke) { 830 this.seriesOutlineStroke = stroke; 831 fireChangeEvent(); 832 } 833 834 /** 835 * Returns the stroke for the specified series. 836 * 837 * @param series the series index (zero-based). 838 * 839 * @return The stroke (never {@code null}). 840 */ 841 public Stroke getSeriesOutlineStroke(int series) { 842 843 // return the override, if there is one... 844 if (this.seriesOutlineStroke != null) { 845 return this.seriesOutlineStroke; 846 } 847 848 // otherwise look up the paint list 849 Stroke result = this.seriesOutlineStrokeList.getStroke(series); 850 if (result == null) { 851 result = this.baseSeriesOutlineStroke; 852 } 853 return result; 854 855 } 856 857 /** 858 * Sets the stroke used to fill a series of the radar and sends a 859 * {@link PlotChangeEvent} to all registered listeners. 860 * 861 * @param series the series index (zero-based). 862 * @param stroke the stroke ({@code null} permitted). 863 */ 864 public void setSeriesOutlineStroke(int series, Stroke stroke) { 865 this.seriesOutlineStrokeList.setStroke(series, stroke); 866 fireChangeEvent(); 867 } 868 869 /** 870 * Returns the base series stroke. This is used when no other stroke is 871 * available. 872 * 873 * @return The stroke (never {@code null}). 874 */ 875 public Stroke getBaseSeriesOutlineStroke() { 876 return this.baseSeriesOutlineStroke; 877 } 878 879 /** 880 * Sets the base series stroke. 881 * 882 * @param stroke the stroke ({@code null} not permitted). 883 */ 884 public void setBaseSeriesOutlineStroke(Stroke stroke) { 885 Args.nullNotPermitted(stroke, "stroke"); 886 this.baseSeriesOutlineStroke = stroke; 887 fireChangeEvent(); 888 } 889 890 /** 891 * Returns the shape used for legend items. 892 * 893 * @return The shape (never {@code null}). 894 * 895 * @see #setLegendItemShape(Shape) 896 */ 897 public Shape getLegendItemShape() { 898 return this.legendItemShape; 899 } 900 901 /** 902 * Sets the shape used for legend items and sends a {@link PlotChangeEvent} 903 * to all registered listeners. 904 * 905 * @param shape the shape ({@code null} not permitted). 906 * 907 * @see #getLegendItemShape() 908 */ 909 public void setLegendItemShape(Shape shape) { 910 Args.nullNotPermitted(shape, "shape"); 911 this.legendItemShape = shape; 912 fireChangeEvent(); 913 } 914 915 /** 916 * Returns the series label font. 917 * 918 * @return The font (never {@code null}). 919 * 920 * @see #setLabelFont(Font) 921 */ 922 public Font getLabelFont() { 923 return this.labelFont; 924 } 925 926 /** 927 * Sets the series label font and sends a {@link PlotChangeEvent} to all 928 * registered listeners. 929 * 930 * @param font the font ({@code null} not permitted). 931 * 932 * @see #getLabelFont() 933 */ 934 public void setLabelFont(Font font) { 935 Args.nullNotPermitted(font, "font"); 936 this.labelFont = font; 937 fireChangeEvent(); 938 } 939 940 /** 941 * Returns the series label paint. 942 * 943 * @return The paint (never {@code null}). 944 * 945 * @see #setLabelPaint(Paint) 946 */ 947 public Paint getLabelPaint() { 948 return this.labelPaint; 949 } 950 951 /** 952 * Sets the series label paint and sends a {@link PlotChangeEvent} to all 953 * registered listeners. 954 * 955 * @param paint the paint ({@code null} not permitted). 956 * 957 * @see #getLabelPaint() 958 */ 959 public void setLabelPaint(Paint paint) { 960 Args.nullNotPermitted(paint, "paint"); 961 this.labelPaint = paint; 962 fireChangeEvent(); 963 } 964 965 /** 966 * Returns the label generator. 967 * 968 * @return The label generator (never {@code null}). 969 * 970 * @see #setLabelGenerator(CategoryItemLabelGenerator) 971 */ 972 public CategoryItemLabelGenerator getLabelGenerator() { 973 return this.labelGenerator; 974 } 975 976 /** 977 * Sets the label generator and sends a {@link PlotChangeEvent} to all 978 * registered listeners. 979 * 980 * @param generator the generator ({@code null} not permitted). 981 * 982 * @see #getLabelGenerator() 983 */ 984 public void setLabelGenerator(CategoryItemLabelGenerator generator) { 985 Args.nullNotPermitted(generator, "generator"); 986 this.labelGenerator = generator; 987 } 988 989 /** 990 * Returns the tool tip generator for the plot. 991 * 992 * @return The tool tip generator (possibly {@code null}). 993 * 994 * @see #setToolTipGenerator(CategoryToolTipGenerator) 995 */ 996 public CategoryToolTipGenerator getToolTipGenerator() { 997 return this.toolTipGenerator; 998 } 999 1000 /** 1001 * Sets the tool tip generator for the plot and sends a 1002 * {@link PlotChangeEvent} to all registered listeners. 1003 * 1004 * @param generator the generator ({@code null} permitted). 1005 * 1006 * @see #getToolTipGenerator() 1007 */ 1008 public void setToolTipGenerator(CategoryToolTipGenerator generator) { 1009 this.toolTipGenerator = generator; 1010 fireChangeEvent(); 1011 } 1012 1013 /** 1014 * Returns the URL generator for the plot. 1015 * 1016 * @return The URL generator (possibly {@code null}). 1017 * 1018 * @see #setURLGenerator(CategoryURLGenerator) 1019 */ 1020 public CategoryURLGenerator getURLGenerator() { 1021 return this.urlGenerator; 1022 } 1023 1024 /** 1025 * Sets the URL generator for the plot and sends a 1026 * {@link PlotChangeEvent} to all registered listeners. 1027 * 1028 * @param generator the generator ({@code null} permitted). 1029 * 1030 * @see #getURLGenerator() 1031 */ 1032 public void setURLGenerator(CategoryURLGenerator generator) { 1033 this.urlGenerator = generator; 1034 fireChangeEvent(); 1035 } 1036 1037 /** 1038 * Returns a collection of legend items for the spider web chart. 1039 * 1040 * @return The legend items (never {@code null}). 1041 */ 1042 @Override 1043 public LegendItemCollection getLegendItems() { 1044 LegendItemCollection result = new LegendItemCollection(); 1045 if (getDataset() == null) { 1046 return result; 1047 } 1048 List keys = null; 1049 if (this.dataExtractOrder == TableOrder.BY_ROW) { 1050 keys = this.dataset.getRowKeys(); 1051 } 1052 else if (this.dataExtractOrder == TableOrder.BY_COLUMN) { 1053 keys = this.dataset.getColumnKeys(); 1054 } 1055 if (keys == null) { 1056 return result; 1057 } 1058 1059 int series = 0; 1060 Iterator iterator = keys.iterator(); 1061 Shape shape = getLegendItemShape(); 1062 while (iterator.hasNext()) { 1063 Comparable key = (Comparable) iterator.next(); 1064 String label = key.toString(); 1065 String description = label; 1066 Paint paint = getSeriesPaint(series); 1067 Paint outlinePaint = getSeriesOutlinePaint(series); 1068 Stroke stroke = getSeriesOutlineStroke(series); 1069 LegendItem item = new LegendItem(label, description, 1070 null, null, shape, paint, stroke, outlinePaint); 1071 item.setDataset(getDataset()); 1072 item.setSeriesKey(key); 1073 item.setSeriesIndex(series); 1074 result.add(item); 1075 series++; 1076 } 1077 return result; 1078 } 1079 1080 /** 1081 * Returns a cartesian point from a polar angle, length and bounding box 1082 * 1083 * @param bounds the area inside which the point needs to be. 1084 * @param angle the polar angle, in degrees. 1085 * @param length the relative length. Given in percent of maximum extend. 1086 * 1087 * @return The cartesian point. 1088 */ 1089 protected Point2D getWebPoint(Rectangle2D bounds, 1090 double angle, double length) { 1091 1092 double angrad = Math.toRadians(angle); 1093 double x = Math.cos(angrad) * length * bounds.getWidth() / 2; 1094 double y = -Math.sin(angrad) * length * bounds.getHeight() / 2; 1095 1096 return new Point2D.Double(bounds.getX() + x + bounds.getWidth() / 2, 1097 bounds.getY() + y + bounds.getHeight() / 2); 1098 } 1099 1100 /** 1101 * Draws the plot on a Java 2D graphics device (such as the screen or a 1102 * printer). 1103 * 1104 * @param g2 the graphics device. 1105 * @param area the area within which the plot should be drawn. 1106 * @param anchor the anchor point ({@code null} permitted). 1107 * @param parentState the state from the parent plot, if there is one. 1108 * @param info collects info about the drawing. 1109 */ 1110 @Override 1111 public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, 1112 PlotState parentState, PlotRenderingInfo info) { 1113 1114 // adjust for insets... 1115 RectangleInsets insets = getInsets(); 1116 insets.trim(area); 1117 1118 if (info != null) { 1119 info.setPlotArea(area); 1120 info.setDataArea(area); 1121 } 1122 1123 drawBackground(g2, area); 1124 drawOutline(g2, area); 1125 1126 Shape savedClip = g2.getClip(); 1127 1128 g2.clip(area); 1129 Composite originalComposite = g2.getComposite(); 1130 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1131 getForegroundAlpha())); 1132 1133 if (!DatasetUtils.isEmptyOrNull(this.dataset)) { 1134 int seriesCount, catCount; 1135 1136 if (this.dataExtractOrder == TableOrder.BY_ROW) { 1137 seriesCount = this.dataset.getRowCount(); 1138 catCount = this.dataset.getColumnCount(); 1139 } 1140 else { 1141 seriesCount = this.dataset.getColumnCount(); 1142 catCount = this.dataset.getRowCount(); 1143 } 1144 1145 // ensure we have a maximum value to use on the axes 1146 if (this.maxValue == DEFAULT_MAX_VALUE) { 1147 calculateMaxValue(seriesCount, catCount); 1148 } 1149 1150 // Next, setup the plot area 1151 1152 // adjust the plot area by the interior spacing value 1153 1154 double gapHorizontal = area.getWidth() * getInteriorGap(); 1155 double gapVertical = area.getHeight() * getInteriorGap(); 1156 1157 double X = area.getX() + gapHorizontal / 2; 1158 double Y = area.getY() + gapVertical / 2; 1159 double W = area.getWidth() - gapHorizontal; 1160 double H = area.getHeight() - gapVertical; 1161 1162 double headW = area.getWidth() * this.headPercent; 1163 double headH = area.getHeight() * this.headPercent; 1164 1165 // make the chart area a square 1166 double min = Math.min(W, H) / 2; 1167 X = (X + X + W) / 2 - min; 1168 Y = (Y + Y + H) / 2 - min; 1169 W = 2 * min; 1170 H = 2 * min; 1171 1172 Point2D centre = new Point2D.Double(X + W / 2, Y + H / 2); 1173 Rectangle2D radarArea = new Rectangle2D.Double(X, Y, W, H); 1174 1175 // draw the axis and category label 1176 for (int cat = 0; cat < catCount; cat++) { 1177 double angle = getStartAngle() 1178 + (getDirection().getFactor() * cat * 360 / catCount); 1179 1180 Point2D endPoint = getWebPoint(radarArea, angle, 1); 1181 // 1 = end of axis 1182 Line2D line = new Line2D.Double(centre, endPoint); 1183 g2.setPaint(this.axisLinePaint); 1184 g2.setStroke(this.axisLineStroke); 1185 g2.draw(line); 1186 drawLabel(g2, radarArea, 0.0, cat, angle, 360.0 / catCount); 1187 } 1188 1189 // Now actually plot each of the series polygons.. 1190 for (int series = 0; series < seriesCount; series++) { 1191 drawRadarPoly(g2, radarArea, centre, info, series, catCount, 1192 headH, headW); 1193 } 1194 } 1195 else { 1196 drawNoDataMessage(g2, area); 1197 } 1198 g2.setClip(savedClip); 1199 g2.setComposite(originalComposite); 1200 drawOutline(g2, area); 1201 } 1202 1203 /** 1204 * loop through each of the series to get the maximum value 1205 * on each category axis 1206 * 1207 * @param seriesCount the number of series 1208 * @param catCount the number of categories 1209 */ 1210 private void calculateMaxValue(int seriesCount, int catCount) { 1211 double v; 1212 Number nV; 1213 1214 for (int seriesIndex = 0; seriesIndex < seriesCount; seriesIndex++) { 1215 for (int catIndex = 0; catIndex < catCount; catIndex++) { 1216 nV = getPlotValue(seriesIndex, catIndex); 1217 if (nV != null) { 1218 v = nV.doubleValue(); 1219 if (v > this.maxValue) { 1220 this.maxValue = v; 1221 } 1222 } 1223 } 1224 } 1225 } 1226 1227 /** 1228 * Draws a radar plot polygon. 1229 * 1230 * @param g2 the graphics device. 1231 * @param plotArea the area we are plotting in (already adjusted). 1232 * @param centre the centre point of the radar axes 1233 * @param info chart rendering info. 1234 * @param series the series within the dataset we are plotting 1235 * @param catCount the number of categories per radar plot 1236 * @param headH the data point height 1237 * @param headW the data point width 1238 */ 1239 protected void drawRadarPoly(Graphics2D g2, 1240 Rectangle2D plotArea, 1241 Point2D centre, 1242 PlotRenderingInfo info, 1243 int series, int catCount, 1244 double headH, double headW) { 1245 1246 Polygon polygon = new Polygon(); 1247 1248 EntityCollection entities = null; 1249 if (info != null) { 1250 entities = info.getOwner().getEntityCollection(); 1251 } 1252 1253 // plot the data... 1254 for (int cat = 0; cat < catCount; cat++) { 1255 1256 Number dataValue = getPlotValue(series, cat); 1257 1258 if (dataValue != null) { 1259 double value = dataValue.doubleValue(); 1260 1261 if (value >= 0) { // draw the polygon series... 1262 1263 // Finds our starting angle from the centre for this axis 1264 1265 double angle = getStartAngle() 1266 + (getDirection().getFactor() * cat * 360 / catCount); 1267 1268 // The following angle calc will ensure there isn't a top 1269 // vertical axis - this may be useful if you don't want any 1270 // given criteria to 'appear' move important than the 1271 // others.. 1272 // + (getDirection().getFactor() 1273 // * (cat + 0.5) * 360 / catCount); 1274 1275 // find the point at the appropriate distance end point 1276 // along the axis/angle identified above and add it to the 1277 // polygon 1278 1279 Point2D point = getWebPoint(plotArea, angle, 1280 value / this.maxValue); 1281 polygon.addPoint((int) point.getX(), (int) point.getY()); 1282 1283 // put an elipse at the point being plotted.. 1284 1285 Paint paint = getSeriesPaint(series); 1286 Paint outlinePaint = getSeriesOutlinePaint(series); 1287 Stroke outlineStroke = getSeriesOutlineStroke(series); 1288 1289 Ellipse2D head = new Ellipse2D.Double(point.getX() 1290 - headW / 2, point.getY() - headH / 2, headW, 1291 headH); 1292 g2.setPaint(paint); 1293 g2.fill(head); 1294 g2.setStroke(outlineStroke); 1295 g2.setPaint(outlinePaint); 1296 g2.draw(head); 1297 1298 if (entities != null) { 1299 int row, col; 1300 if (this.dataExtractOrder == TableOrder.BY_ROW) { 1301 row = series; 1302 col = cat; 1303 } 1304 else { 1305 row = cat; 1306 col = series; 1307 } 1308 String tip = null; 1309 if (this.toolTipGenerator != null) { 1310 tip = this.toolTipGenerator.generateToolTip( 1311 this.dataset, row, col); 1312 } 1313 1314 String url = null; 1315 if (this.urlGenerator != null) { 1316 url = this.urlGenerator.generateURL(this.dataset, 1317 row, col); 1318 } 1319 1320 Shape area = new Rectangle( 1321 (int) (point.getX() - headW), 1322 (int) (point.getY() - headH), 1323 (int) (headW * 2), (int) (headH * 2)); 1324 CategoryItemEntity entity = new CategoryItemEntity( 1325 area, tip, url, this.dataset, 1326 this.dataset.getRowKey(row), 1327 this.dataset.getColumnKey(col)); 1328 entities.add(entity); 1329 } 1330 1331 } 1332 } 1333 } 1334 // Plot the polygon 1335 1336 Paint paint = getSeriesPaint(series); 1337 g2.setPaint(paint); 1338 g2.setStroke(getSeriesOutlineStroke(series)); 1339 g2.draw(polygon); 1340 1341 // Lastly, fill the web polygon if this is required 1342 1343 if (this.webFilled) { 1344 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1345 webFillAlpha)); 1346 g2.fill(polygon); 1347 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1348 getForegroundAlpha())); 1349 } 1350 } 1351 1352 /** 1353 * Returns the value to be plotted at the intersection of the 1354 * series and the category. This allows us to plot 1355 * {@code BY_ROW} or {@code BY_COLUMN} which basically is just 1356 * reversing the definition of the categories and data series being 1357 * plotted. 1358 * 1359 * @param series the series to be plotted. 1360 * @param cat the category within the series to be plotted. 1361 * 1362 * @return The value to be plotted (possibly {@code null}). 1363 * 1364 * @see #getDataExtractOrder() 1365 */ 1366 protected Number getPlotValue(int series, int cat) { 1367 Number value = null; 1368 if (this.dataExtractOrder == TableOrder.BY_ROW) { 1369 value = this.dataset.getValue(series, cat); 1370 } 1371 else if (this.dataExtractOrder == TableOrder.BY_COLUMN) { 1372 value = this.dataset.getValue(cat, series); 1373 } 1374 return value; 1375 } 1376 1377 /** 1378 * Draws the label for one axis. 1379 * 1380 * @param g2 the graphics device. 1381 * @param plotArea the plot area 1382 * @param value the value of the label (ignored). 1383 * @param cat the category (zero-based index). 1384 * @param startAngle the starting angle. 1385 * @param extent the extent of the arc. 1386 */ 1387 protected void drawLabel(Graphics2D g2, Rectangle2D plotArea, double value, 1388 int cat, double startAngle, double extent) { 1389 FontRenderContext frc = g2.getFontRenderContext(); 1390 1391 String label; 1392 if (this.dataExtractOrder == TableOrder.BY_ROW) { 1393 // if series are in rows, then the categories are the column keys 1394 label = this.labelGenerator.generateColumnLabel(this.dataset, cat); 1395 } 1396 else { 1397 // if series are in columns, then the categories are the row keys 1398 label = this.labelGenerator.generateRowLabel(this.dataset, cat); 1399 } 1400 1401 Rectangle2D labelBounds = getLabelFont().getStringBounds(label, frc); 1402 LineMetrics lm = getLabelFont().getLineMetrics(label, frc); 1403 double ascent = lm.getAscent(); 1404 1405 Point2D labelLocation = calculateLabelLocation(labelBounds, ascent, 1406 plotArea, startAngle); 1407 1408 Composite saveComposite = g2.getComposite(); 1409 1410 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1411 1.0f)); 1412 g2.setPaint(getLabelPaint()); 1413 g2.setFont(getLabelFont()); 1414 g2.drawString(label, (float) labelLocation.getX(), 1415 (float) labelLocation.getY()); 1416 g2.setComposite(saveComposite); 1417 } 1418 1419 /** 1420 * Returns the location for a label 1421 * 1422 * @param labelBounds the label bounds. 1423 * @param ascent the ascent (height of font). 1424 * @param plotArea the plot area 1425 * @param startAngle the start angle for the pie series. 1426 * 1427 * @return The location for a label. 1428 */ 1429 protected Point2D calculateLabelLocation(Rectangle2D labelBounds, 1430 double ascent, 1431 Rectangle2D plotArea, 1432 double startAngle) 1433 { 1434 Arc2D arc1 = new Arc2D.Double(plotArea, startAngle, 0, Arc2D.OPEN); 1435 Point2D point1 = arc1.getEndPoint(); 1436 1437 double deltaX = -(point1.getX() - plotArea.getCenterX()) 1438 * this.axisLabelGap; 1439 double deltaY = -(point1.getY() - plotArea.getCenterY()) 1440 * this.axisLabelGap; 1441 1442 double labelX = point1.getX() - deltaX; 1443 double labelY = point1.getY() - deltaY; 1444 1445 if (labelX < plotArea.getCenterX()) { 1446 labelX -= labelBounds.getWidth(); 1447 } 1448 1449 if (labelX == plotArea.getCenterX()) { 1450 labelX -= labelBounds.getWidth() / 2; 1451 } 1452 1453 if (labelY > plotArea.getCenterY()) { 1454 labelY += ascent; 1455 } 1456 1457 return new Point2D.Double(labelX, labelY); 1458 } 1459 1460 /** 1461 * Tests this plot for equality with an arbitrary object. 1462 * 1463 * @param obj the object ({@code null} permitted). 1464 * 1465 * @return A boolean. 1466 */ 1467 @Override 1468 public boolean equals(Object obj) { 1469 if (obj == this) { 1470 return true; 1471 } 1472 if (!(obj instanceof SpiderWebPlot)) { 1473 return false; 1474 } 1475 if (!super.equals(obj)) { 1476 return false; 1477 } 1478 SpiderWebPlot that = (SpiderWebPlot) obj; 1479 if (!this.dataExtractOrder.equals(that.dataExtractOrder)) { 1480 return false; 1481 } 1482 if (this.headPercent != that.headPercent) { 1483 return false; 1484 } 1485 if (this.interiorGap != that.interiorGap) { 1486 return false; 1487 } 1488 if (this.startAngle != that.startAngle) { 1489 return false; 1490 } 1491 if (!this.direction.equals(that.direction)) { 1492 return false; 1493 } 1494 if (this.maxValue != that.maxValue) { 1495 return false; 1496 } 1497 if (this.webFilled != that.webFilled) { 1498 return false; 1499 } 1500 if (this.webFillAlpha != that.webFillAlpha) { 1501 return false; 1502 } 1503 if (this.axisLabelGap != that.axisLabelGap) { 1504 return false; 1505 } 1506 if (!PaintUtils.equal(this.axisLinePaint, that.axisLinePaint)) { 1507 return false; 1508 } 1509 if (!this.axisLineStroke.equals(that.axisLineStroke)) { 1510 return false; 1511 } 1512 if (!ShapeUtils.equal(this.legendItemShape, that.legendItemShape)) { 1513 return false; 1514 } 1515 if (!PaintUtils.equal(this.seriesPaint, that.seriesPaint)) { 1516 return false; 1517 } 1518 if (!this.seriesPaintList.equals(that.seriesPaintList)) { 1519 return false; 1520 } 1521 if (!PaintUtils.equal(this.baseSeriesPaint, that.baseSeriesPaint)) { 1522 return false; 1523 } 1524 if (!PaintUtils.equal(this.seriesOutlinePaint, 1525 that.seriesOutlinePaint)) { 1526 return false; 1527 } 1528 if (!this.seriesOutlinePaintList.equals(that.seriesOutlinePaintList)) { 1529 return false; 1530 } 1531 if (!PaintUtils.equal(this.baseSeriesOutlinePaint, 1532 that.baseSeriesOutlinePaint)) { 1533 return false; 1534 } 1535 if (!Objects.equals(this.seriesOutlineStroke, 1536 that.seriesOutlineStroke)) { 1537 return false; 1538 } 1539 if (!this.seriesOutlineStrokeList.equals( 1540 that.seriesOutlineStrokeList)) { 1541 return false; 1542 } 1543 if (!this.baseSeriesOutlineStroke.equals( 1544 that.baseSeriesOutlineStroke)) { 1545 return false; 1546 } 1547 if (!this.labelFont.equals(that.labelFont)) { 1548 return false; 1549 } 1550 if (!PaintUtils.equal(this.labelPaint, that.labelPaint)) { 1551 return false; 1552 } 1553 if (!this.labelGenerator.equals(that.labelGenerator)) { 1554 return false; 1555 } 1556 if (!Objects.equals(this.toolTipGenerator, 1557 that.toolTipGenerator)) { 1558 return false; 1559 } 1560 if (!Objects.equals(this.urlGenerator, 1561 that.urlGenerator)) { 1562 return false; 1563 } 1564 return true; 1565 } 1566 1567 /** 1568 * Returns a clone of this plot. 1569 * 1570 * @return A clone of this plot. 1571 * 1572 * @throws CloneNotSupportedException if the plot cannot be cloned for 1573 * any reason. 1574 */ 1575 @Override 1576 public Object clone() throws CloneNotSupportedException { 1577 SpiderWebPlot clone = (SpiderWebPlot) super.clone(); 1578 clone.legendItemShape = ShapeUtils.clone(this.legendItemShape); 1579 clone.seriesPaintList = (PaintList) this.seriesPaintList.clone(); 1580 clone.seriesOutlinePaintList 1581 = (PaintList) this.seriesOutlinePaintList.clone(); 1582 clone.seriesOutlineStrokeList 1583 = (StrokeList) this.seriesOutlineStrokeList.clone(); 1584 return clone; 1585 } 1586 1587 /** 1588 * Provides serialization support. 1589 * 1590 * @param stream the output stream. 1591 * 1592 * @throws IOException if there is an I/O error. 1593 */ 1594 private void writeObject(ObjectOutputStream stream) throws IOException { 1595 stream.defaultWriteObject(); 1596 1597 SerialUtils.writeShape(this.legendItemShape, stream); 1598 SerialUtils.writePaint(this.seriesPaint, stream); 1599 SerialUtils.writePaint(this.baseSeriesPaint, stream); 1600 SerialUtils.writePaint(this.seriesOutlinePaint, stream); 1601 SerialUtils.writePaint(this.baseSeriesOutlinePaint, stream); 1602 SerialUtils.writeStroke(this.seriesOutlineStroke, stream); 1603 SerialUtils.writeStroke(this.baseSeriesOutlineStroke, stream); 1604 SerialUtils.writePaint(this.labelPaint, stream); 1605 SerialUtils.writePaint(this.axisLinePaint, stream); 1606 SerialUtils.writeStroke(this.axisLineStroke, stream); 1607 } 1608 1609 /** 1610 * Provides serialization support. 1611 * 1612 * @param stream the input stream. 1613 * 1614 * @throws IOException if there is an I/O error. 1615 * @throws ClassNotFoundException if there is a classpath problem. 1616 */ 1617 private void readObject(ObjectInputStream stream) throws IOException, 1618 ClassNotFoundException { 1619 stream.defaultReadObject(); 1620 1621 this.legendItemShape = SerialUtils.readShape(stream); 1622 this.seriesPaint = SerialUtils.readPaint(stream); 1623 this.baseSeriesPaint = SerialUtils.readPaint(stream); 1624 this.seriesOutlinePaint = SerialUtils.readPaint(stream); 1625 this.baseSeriesOutlinePaint = SerialUtils.readPaint(stream); 1626 this.seriesOutlineStroke = SerialUtils.readStroke(stream); 1627 this.baseSeriesOutlineStroke = SerialUtils.readStroke(stream); 1628 this.labelPaint = SerialUtils.readPaint(stream); 1629 this.axisLinePaint = SerialUtils.readPaint(stream); 1630 this.axisLineStroke = SerialUtils.readStroke(stream); 1631 if (this.dataset != null) { 1632 this.dataset.addChangeListener(this); 1633 } 1634 } 1635 1636}