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