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