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