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