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