001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2020, by Object Refinery Limited and Contributors. 006 * 007 * Project Info: http://www.jfree.org/jfreechart/index.html 008 * 009 * This library is free software; you can redistribute it and/or modify it 010 * under the terms of the GNU Lesser General Public License as published by 011 * the Free Software Foundation; either version 2.1 of the License, or 012 * (at your option) any later version. 013 * 014 * This library is distributed in the hope that it will be useful, but 015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 017 * License for more details. 018 * 019 * You should have received a copy of the GNU Lesser General Public 020 * License along with this library; if not, write to the Free Software 021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, 022 * USA. 023 * 024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 025 * Other names may be trademarks of their respective owners.] 026 * 027 * -------------- 028 * PolarPlot.java 029 * -------------- 030 * (C) Copyright 2004-2020, by Solution Engineering, Inc. and Contributors. 031 * 032 * Original Author: Daniel Bridenbecker, Solution Engineering, Inc.; 033 * Contributor(s): David Gilbert (for Object Refinery Limited); 034 * Martin Hoeller (patches 1871902 and 2850344); 035 * 036 * Changes 037 * ------- 038 * 19-Jan-2004 : Version 1, contributed by DB with minor changes by DG (DG); 039 * 07-Apr-2004 : Changed text bounds calculation (DG); 040 * 05-May-2005 : Updated draw() method parameters (DG); 041 * 09-Jun-2005 : Fixed getDataRange() and equals() methods (DG); 042 * 25-Oct-2005 : Implemented Zoomable (DG); 043 * ------------- JFREECHART 1.0.x --------------------------------------------- 044 * 07-Feb-2007 : Fixed bug 1599761, data value less than axis minimum (DG); 045 * 21-Mar-2007 : Fixed serialization bug (DG); 046 * 24-Sep-2007 : Implemented new zooming methods (DG); 047 * 17-Feb-2007 : Added angle tick unit attribute (see patch 1871902 by 048 * Martin Hoeller) (DG); 049 * 18-Dec-2008 : Use ResourceBundleWrapper - see patch 1607918 by 050 * Jess Thrysoee (DG); 051 * 03-Sep-2009 : Applied patch 2850344 by Martin Hoeller (DG); 052 * 27-Nov-2009 : Added support for multiple datasets, renderers and axes (DG); 053 * 09-Dec-2009 : Extended getLegendItems() to handle multiple datasets (DG); 054 * 25-Jun-2010 : Better support for multiple axes (MH); 055 * 03-Oct-2011 : Added support for angleOffset and direction (MH); 056 * 12-Nov-2011 : Fixed bug 3432721, log-axis doesn't work (MH); 057 * 12-Dec-2011 : Added support for radiusMinorGridlinesVisible (MH); 058 * 02-Jul-2013 : Use ParamChecks (DG); 059 * 05-Jul-2013 : Fire change event from setRadiusMinorGridlinesVisible (DG); 060 * 061 */ 062 063package org.jfree.chart.plot; 064 065import java.awt.AlphaComposite; 066import java.awt.BasicStroke; 067import java.awt.Color; 068import java.awt.Composite; 069import java.awt.Font; 070import java.awt.FontMetrics; 071import java.awt.Graphics2D; 072import java.awt.Paint; 073import java.awt.Point; 074import java.awt.Shape; 075import java.awt.Stroke; 076import java.awt.geom.Point2D; 077import java.awt.geom.Rectangle2D; 078import java.io.IOException; 079import java.io.ObjectInputStream; 080import java.io.ObjectOutputStream; 081import java.io.Serializable; 082import java.util.ArrayList; 083import java.util.HashSet; 084import java.util.Iterator; 085import java.util.List; 086import java.util.Map; 087import java.util.ResourceBundle; 088import java.util.TreeMap; 089 090import org.jfree.chart.LegendItem; 091import org.jfree.chart.LegendItemCollection; 092import org.jfree.chart.axis.Axis; 093import org.jfree.chart.axis.AxisState; 094import org.jfree.chart.axis.NumberTick; 095import org.jfree.chart.axis.NumberTickUnit; 096import org.jfree.chart.axis.TickType; 097import org.jfree.chart.axis.TickUnit; 098import org.jfree.chart.axis.ValueAxis; 099import org.jfree.chart.axis.ValueTick; 100import org.jfree.chart.event.PlotChangeEvent; 101import org.jfree.chart.event.RendererChangeEvent; 102import org.jfree.chart.event.RendererChangeListener; 103import org.jfree.chart.renderer.PolarItemRenderer; 104import org.jfree.chart.text.TextUtils; 105import org.jfree.chart.ui.RectangleEdge; 106import org.jfree.chart.ui.RectangleInsets; 107import org.jfree.chart.ui.TextAnchor; 108import org.jfree.chart.util.ObjectList; 109import org.jfree.chart.util.ObjectUtils; 110import org.jfree.chart.util.PaintUtils; 111import org.jfree.chart.util.Args; 112import org.jfree.chart.util.PublicCloneable; 113import org.jfree.chart.util.ResourceBundleWrapper; 114import org.jfree.chart.util.SerialUtils; 115import org.jfree.data.Range; 116import org.jfree.data.general.Dataset; 117import org.jfree.data.general.DatasetChangeEvent; 118import org.jfree.data.general.DatasetUtils; 119import org.jfree.data.xy.XYDataset; 120 121/** 122 * Plots data that is in (theta, radius) pairs where 123 * theta equal to zero is due north and increases clockwise. 124 */ 125public class PolarPlot extends Plot implements ValueAxisPlot, Zoomable, 126 RendererChangeListener, Cloneable, Serializable { 127 128 /** For serialization. */ 129 private static final long serialVersionUID = 3794383185924179525L; 130 131 /** The default margin. */ 132 private static final int DEFAULT_MARGIN = 20; 133 134 /** The annotation margin. */ 135 private static final double ANNOTATION_MARGIN = 7.0; 136 137 /** 138 * The default angle tick unit size. 139 * 140 * @since 1.0.10 141 */ 142 public static final double DEFAULT_ANGLE_TICK_UNIT_SIZE = 45.0; 143 144 /** 145 * The default angle offset. 146 * 147 * @since 1.0.14 148 */ 149 public static final double DEFAULT_ANGLE_OFFSET = -90.0; 150 151 /** The default grid line stroke. */ 152 public static final Stroke DEFAULT_GRIDLINE_STROKE = new BasicStroke( 153 0.5f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 154 0.0f, new float[]{2.0f, 2.0f}, 0.0f); 155 156 /** The default grid line paint. */ 157 public static final Paint DEFAULT_GRIDLINE_PAINT = Color.GRAY; 158 159 /** The resourceBundle for the localization. */ 160 protected static ResourceBundle localizationResources 161 = ResourceBundleWrapper.getBundle( 162 "org.jfree.chart.plot.LocalizationBundle"); 163 164 /** The angles that are marked with gridlines. */ 165 private List angleTicks; 166 167 /** The range axis (used for the y-values). */ 168 private ObjectList axes; 169 170 /** The axis locations. */ 171 private ObjectList axisLocations; 172 173 /** Storage for the datasets. */ 174 private ObjectList datasets; 175 176 /** Storage for the renderers. */ 177 private ObjectList renderers; 178 179 /** 180 * The tick unit that controls the spacing between the angular grid lines. 181 * 182 * @since 1.0.10 183 */ 184 private TickUnit angleTickUnit; 185 186 /** 187 * An offset for the angles, to start with 0 degrees at north, east, south 188 * or west. 189 * 190 * @since 1.0.14 191 */ 192 private double angleOffset; 193 194 /** 195 * A flag indicating if the angles increase counterclockwise or clockwise. 196 * 197 * @since 1.0.14 198 */ 199 private boolean counterClockwise; 200 201 /** A flag that controls whether or not the angle labels are visible. */ 202 private boolean angleLabelsVisible = true; 203 204 /** The font used to display the angle labels - never null. */ 205 private Font angleLabelFont = new Font("SansSerif", Font.PLAIN, 12); 206 207 /** The paint used to display the angle labels. */ 208 private transient Paint angleLabelPaint = Color.BLACK; 209 210 /** A flag that controls whether the angular grid-lines are visible. */ 211 private boolean angleGridlinesVisible; 212 213 /** The stroke used to draw the angular grid-lines. */ 214 private transient Stroke angleGridlineStroke; 215 216 /** The paint used to draw the angular grid-lines. */ 217 private transient Paint angleGridlinePaint; 218 219 /** A flag that controls whether the radius grid-lines are visible. */ 220 private boolean radiusGridlinesVisible; 221 222 /** The stroke used to draw the radius grid-lines. */ 223 private transient Stroke radiusGridlineStroke; 224 225 /** The paint used to draw the radius grid-lines. */ 226 private transient Paint radiusGridlinePaint; 227 228 /** 229 * A flag that controls whether the radial minor grid-lines are visible. 230 * @since 1.0.15 231 */ 232 private boolean radiusMinorGridlinesVisible; 233 234 /** The annotations for the plot. */ 235 private List cornerTextItems = new ArrayList(); 236 237 /** 238 * The actual margin in pixels. 239 * 240 * @since 1.0.14 241 */ 242 private int margin; 243 244 /** 245 * An optional collection of legend items that can be returned by the 246 * getLegendItems() method. 247 */ 248 private LegendItemCollection fixedLegendItems; 249 250 /** 251 * Storage for the mapping between datasets/renderers and range axes. The 252 * keys in the map are Integer objects, corresponding to the dataset 253 * index. The values in the map are List objects containing Integer 254 * objects (corresponding to the axis indices). If the map contains no 255 * entry for a dataset, it is assumed to map to the primary domain axis 256 * (index = 0). 257 */ 258 private Map datasetToAxesMap; 259 260 /** 261 * Default constructor. 262 */ 263 public PolarPlot() { 264 this(null, null, null); 265 } 266 267 /** 268 * Creates a new plot. 269 * 270 * @param dataset the dataset ({@code null} permitted). 271 * @param radiusAxis the radius axis ({@code null} permitted). 272 * @param renderer the renderer ({@code null} permitted). 273 */ 274 public PolarPlot(XYDataset dataset, ValueAxis radiusAxis, 275 PolarItemRenderer renderer) { 276 277 super(); 278 279 this.datasets = new ObjectList(); 280 this.datasets.set(0, dataset); 281 if (dataset != null) { 282 dataset.addChangeListener(this); 283 } 284 this.angleTickUnit = new NumberTickUnit(DEFAULT_ANGLE_TICK_UNIT_SIZE); 285 286 this.axes = new ObjectList(); 287 this.datasetToAxesMap = new TreeMap(); 288 this.axes.set(0, radiusAxis); 289 if (radiusAxis != null) { 290 radiusAxis.setPlot(this); 291 radiusAxis.addChangeListener(this); 292 } 293 294 // define the default locations for up to 8 axes... 295 this.axisLocations = new ObjectList(); 296 this.axisLocations.set(0, PolarAxisLocation.EAST_ABOVE); 297 this.axisLocations.set(1, PolarAxisLocation.NORTH_LEFT); 298 this.axisLocations.set(2, PolarAxisLocation.WEST_BELOW); 299 this.axisLocations.set(3, PolarAxisLocation.SOUTH_RIGHT); 300 this.axisLocations.set(4, PolarAxisLocation.EAST_BELOW); 301 this.axisLocations.set(5, PolarAxisLocation.NORTH_RIGHT); 302 this.axisLocations.set(6, PolarAxisLocation.WEST_ABOVE); 303 this.axisLocations.set(7, PolarAxisLocation.SOUTH_LEFT); 304 305 this.renderers = new ObjectList(); 306 this.renderers.set(0, renderer); 307 if (renderer != null) { 308 renderer.setPlot(this); 309 renderer.addChangeListener(this); 310 } 311 312 this.angleOffset = DEFAULT_ANGLE_OFFSET; 313 this.counterClockwise = false; 314 this.angleGridlinesVisible = true; 315 this.angleGridlineStroke = DEFAULT_GRIDLINE_STROKE; 316 this.angleGridlinePaint = DEFAULT_GRIDLINE_PAINT; 317 318 this.radiusGridlinesVisible = true; 319 this.radiusMinorGridlinesVisible = true; 320 this.radiusGridlineStroke = DEFAULT_GRIDLINE_STROKE; 321 this.radiusGridlinePaint = DEFAULT_GRIDLINE_PAINT; 322 this.margin = DEFAULT_MARGIN; 323 } 324 325 /** 326 * Returns the plot type as a string. 327 * 328 * @return A short string describing the type of plot. 329 */ 330 @Override 331 public String getPlotType() { 332 return PolarPlot.localizationResources.getString("Polar_Plot"); 333 } 334 335 /** 336 * Returns the primary axis for the plot. 337 * 338 * @return The primary axis (possibly {@code null}). 339 * 340 * @see #setAxis(ValueAxis) 341 */ 342 public ValueAxis getAxis() { 343 return getAxis(0); 344 } 345 346 /** 347 * Returns an axis for the plot. 348 * 349 * @param index the axis index. 350 * 351 * @return The axis ({@code null} possible). 352 * 353 * @see #setAxis(int, ValueAxis) 354 * 355 * @since 1.0.14 356 */ 357 public ValueAxis getAxis(int index) { 358 ValueAxis result = null; 359 if (index < this.axes.size()) { 360 result = (ValueAxis) this.axes.get(index); 361 } 362 return result; 363 } 364 365 /** 366 * Sets the primary axis for the plot and sends a {@link PlotChangeEvent} 367 * to all registered listeners. 368 * 369 * @param axis the new primary axis ({@code null} permitted). 370 */ 371 public void setAxis(ValueAxis axis) { 372 setAxis(0, axis); 373 } 374 375 /** 376 * Sets an axis for the plot and sends a {@link PlotChangeEvent} to all 377 * registered listeners. 378 * 379 * @param index the axis index. 380 * @param axis the axis ({@code null} permitted). 381 * 382 * @see #getAxis(int) 383 * 384 * @since 1.0.14 385 */ 386 public void setAxis(int index, ValueAxis axis) { 387 setAxis(index, axis, true); 388 } 389 390 /** 391 * Sets an axis for the plot and, if requested, sends a 392 * {@link PlotChangeEvent} to all registered listeners. 393 * 394 * @param index the axis index. 395 * @param axis the axis ({@code null} permitted). 396 * @param notify notify listeners? 397 * 398 * @see #getAxis(int) 399 * 400 * @since 1.0.14 401 */ 402 public void setAxis(int index, ValueAxis axis, boolean notify) { 403 ValueAxis existing = getAxis(index); 404 if (existing != null) { 405 existing.removeChangeListener(this); 406 } 407 if (axis != null) { 408 axis.setPlot(this); 409 } 410 this.axes.set(index, axis); 411 if (axis != null) { 412 axis.configure(); 413 axis.addChangeListener(this); 414 } 415 if (notify) { 416 fireChangeEvent(); 417 } 418 } 419 420 /** 421 * Returns the location of the primary axis. 422 * 423 * @return The location (never {@code null}). 424 * 425 * @see #setAxisLocation(PolarAxisLocation) 426 * 427 * @since 1.0.14 428 */ 429 public PolarAxisLocation getAxisLocation() { 430 return getAxisLocation(0); 431 } 432 433 /** 434 * Returns the location for an axis. 435 * 436 * @param index the axis index. 437 * 438 * @return The location (never {@code null}). 439 * 440 * @see #setAxisLocation(int, PolarAxisLocation) 441 * 442 * @since 1.0.14 443 */ 444 public PolarAxisLocation getAxisLocation(int index) { 445 PolarAxisLocation result = null; 446 if (index < this.axisLocations.size()) { 447 result = (PolarAxisLocation) this.axisLocations.get(index); 448 } 449 return result; 450 } 451 452 /** 453 * Sets the location of the primary axis and sends a 454 * {@link PlotChangeEvent} to all registered listeners. 455 * 456 * @param location the location ({@code null} not permitted). 457 * 458 * @see #getAxisLocation() 459 * 460 * @since 1.0.14 461 */ 462 public void setAxisLocation(PolarAxisLocation location) { 463 // delegate... 464 setAxisLocation(0, location, true); 465 } 466 467 /** 468 * Sets the location of the primary axis and, if requested, sends a 469 * {@link PlotChangeEvent} to all registered listeners. 470 * 471 * @param location the location ({@code null} not permitted). 472 * @param notify notify listeners? 473 * 474 * @see #getAxisLocation() 475 * 476 * @since 1.0.14 477 */ 478 public void setAxisLocation(PolarAxisLocation location, boolean notify) { 479 // delegate... 480 setAxisLocation(0, location, notify); 481 } 482 483 /** 484 * Sets the location for an axis and sends a {@link PlotChangeEvent} 485 * to all registered listeners. 486 * 487 * @param index the axis index. 488 * @param location the location ({@code null} not permitted). 489 * 490 * @see #getAxisLocation(int) 491 * 492 * @since 1.0.14 493 */ 494 public void setAxisLocation(int index, PolarAxisLocation location) { 495 // delegate... 496 setAxisLocation(index, location, true); 497 } 498 499 /** 500 * Sets the axis location for an axis and, if requested, sends a 501 * {@link PlotChangeEvent} to all registered listeners. 502 * 503 * @param index the axis index. 504 * @param location the location ({@code null} not permitted). 505 * @param notify notify listeners? 506 * 507 * @since 1.0.14 508 */ 509 public void setAxisLocation(int index, PolarAxisLocation location, 510 boolean notify) { 511 Args.nullNotPermitted(location, "location"); 512 this.axisLocations.set(index, location); 513 if (notify) { 514 fireChangeEvent(); 515 } 516 } 517 518 /** 519 * Returns the number of domain axes. 520 * 521 * @return The axis count. 522 * 523 * @since 1.0.14 524 **/ 525 public int getAxisCount() { 526 return this.axes.size(); 527 } 528 529 /** 530 * Returns the primary dataset for the plot. 531 * 532 * @return The primary dataset (possibly {@code null}). 533 * 534 * @see #setDataset(XYDataset) 535 */ 536 public XYDataset getDataset() { 537 return getDataset(0); 538 } 539 540 /** 541 * Returns the dataset with the specified index, if any. 542 * 543 * @param index the dataset index. 544 * 545 * @return The dataset (possibly {@code null}). 546 * 547 * @see #setDataset(int, XYDataset) 548 * 549 * @since 1.0.14 550 */ 551 public XYDataset getDataset(int index) { 552 XYDataset result = null; 553 if (index < this.datasets.size()) { 554 result = (XYDataset) this.datasets.get(index); 555 } 556 return result; 557 } 558 559 /** 560 * Sets the primary dataset for the plot, replacing the existing dataset 561 * if there is one, and sends a {@code link PlotChangeEvent} to all 562 * registered listeners. 563 * 564 * @param dataset the dataset ({@code null} permitted). 565 * 566 * @see #getDataset() 567 */ 568 public void setDataset(XYDataset dataset) { 569 setDataset(0, dataset); 570 } 571 572 /** 573 * Sets a dataset for the plot, replacing the existing dataset at the same 574 * index if there is one, and sends a {@code link PlotChangeEvent} to all 575 * registered listeners. 576 * 577 * @param index the dataset index. 578 * @param dataset the dataset ({@code null} permitted). 579 * 580 * @see #getDataset(int) 581 * 582 * @since 1.0.14 583 */ 584 public void setDataset(int index, XYDataset dataset) { 585 XYDataset existing = getDataset(index); 586 if (existing != null) { 587 existing.removeChangeListener(this); 588 } 589 this.datasets.set(index, dataset); 590 if (dataset != null) { 591 dataset.addChangeListener(this); 592 } 593 594 // send a dataset change event to self... 595 DatasetChangeEvent event = new DatasetChangeEvent(this, dataset); 596 datasetChanged(event); 597 } 598 599 /** 600 * Returns the number of datasets. 601 * 602 * @return The number of datasets. 603 * 604 * @since 1.0.14 605 */ 606 public int getDatasetCount() { 607 return this.datasets.size(); 608 } 609 610 /** 611 * Returns the index of the specified dataset, or {@code -1} if the 612 * dataset does not belong to the plot. 613 * 614 * @param dataset the dataset ({@code null} not permitted). 615 * 616 * @return The index. 617 * 618 * @since 1.0.14 619 */ 620 public int indexOf(XYDataset dataset) { 621 int result = -1; 622 for (int i = 0; i < this.datasets.size(); i++) { 623 if (dataset == this.datasets.get(i)) { 624 result = i; 625 break; 626 } 627 } 628 return result; 629 } 630 631 /** 632 * Returns the primary renderer. 633 * 634 * @return The renderer (possibly {@code null}). 635 * 636 * @see #setRenderer(PolarItemRenderer) 637 */ 638 public PolarItemRenderer getRenderer() { 639 return getRenderer(0); 640 } 641 642 /** 643 * Returns the renderer at the specified index, if there is one. 644 * 645 * @param index the renderer index. 646 * 647 * @return The renderer (possibly {@code null}). 648 * 649 * @see #setRenderer(int, PolarItemRenderer) 650 * 651 * @since 1.0.14 652 */ 653 public PolarItemRenderer getRenderer(int index) { 654 PolarItemRenderer result = null; 655 if (index < this.renderers.size()) { 656 result = (PolarItemRenderer) this.renderers.get(index); 657 } 658 return result; 659 } 660 661 /** 662 * Sets the primary renderer, and notifies all listeners of a change to the 663 * plot. If the renderer is set to {@code null}, no data items will 664 * be drawn for the corresponding dataset. 665 * 666 * @param renderer the new renderer ({@code null} permitted). 667 * 668 * @see #getRenderer() 669 */ 670 public void setRenderer(PolarItemRenderer renderer) { 671 setRenderer(0, renderer); 672 } 673 674 /** 675 * Sets a renderer and sends a {@link PlotChangeEvent} to all 676 * registered listeners. 677 * 678 * @param index the index. 679 * @param renderer the renderer. 680 * 681 * @see #getRenderer(int) 682 * 683 * @since 1.0.14 684 */ 685 public void setRenderer(int index, PolarItemRenderer renderer) { 686 setRenderer(index, renderer, true); 687 } 688 689 /** 690 * Sets a renderer and, if requested, sends a {@link PlotChangeEvent} to 691 * all registered listeners. 692 * 693 * @param index the index. 694 * @param renderer the renderer. 695 * @param notify notify listeners? 696 * 697 * @see #getRenderer(int) 698 * 699 * @since 1.0.14 700 */ 701 public void setRenderer(int index, PolarItemRenderer renderer, 702 boolean notify) { 703 PolarItemRenderer existing = getRenderer(index); 704 if (existing != null) { 705 existing.removeChangeListener(this); 706 } 707 this.renderers.set(index, renderer); 708 if (renderer != null) { 709 renderer.setPlot(this); 710 renderer.addChangeListener(this); 711 } 712 if (notify) { 713 fireChangeEvent(); 714 } 715 } 716 717 /** 718 * Returns the tick unit that controls the spacing of the angular grid 719 * lines. 720 * 721 * @return The tick unit (never {@code null}). 722 * 723 * @since 1.0.10 724 */ 725 public TickUnit getAngleTickUnit() { 726 return this.angleTickUnit; 727 } 728 729 /** 730 * Sets the tick unit that controls the spacing of the angular grid 731 * lines, and sends a {@link PlotChangeEvent} to all registered listeners. 732 * 733 * @param unit the tick unit ({@code null} not permitted). 734 * 735 * @since 1.0.10 736 */ 737 public void setAngleTickUnit(TickUnit unit) { 738 Args.nullNotPermitted(unit, "unit"); 739 this.angleTickUnit = unit; 740 fireChangeEvent(); 741 } 742 743 /** 744 * Returns the offset that is used for all angles. 745 * 746 * @return The offset for the angles. 747 * @since 1.0.14 748 */ 749 public double getAngleOffset() { 750 return this.angleOffset; 751 } 752 753 /** 754 * Sets the offset that is used for all angles and sends a 755 * {@link PlotChangeEvent} to all registered listeners. 756 * 757 * This is useful to let 0 degrees be at the north, east, south or west 758 * side of the chart. 759 * 760 * @param offset The offset 761 * @since 1.0.14 762 */ 763 public void setAngleOffset(double offset) { 764 this.angleOffset = offset; 765 fireChangeEvent(); 766 } 767 768 /** 769 * Get the direction for growing angle degrees. 770 * 771 * @return {@code true} if angle increases counterclockwise, 772 * {@code false} otherwise. 773 * @since 1.0.14 774 */ 775 public boolean isCounterClockwise() { 776 return this.counterClockwise; 777 } 778 779 /** 780 * Sets the flag for increasing angle degrees direction. 781 * 782 * {@code true} for counterclockwise, {@code false} for 783 * clockwise. 784 * 785 * @param counterClockwise The flag. 786 * @since 1.0.14 787 */ 788 public void setCounterClockwise(boolean counterClockwise) 789 { 790 this.counterClockwise = counterClockwise; 791 } 792 793 /** 794 * Returns a flag that controls whether or not the angle labels are visible. 795 * 796 * @return A boolean. 797 * 798 * @see #setAngleLabelsVisible(boolean) 799 */ 800 public boolean isAngleLabelsVisible() { 801 return this.angleLabelsVisible; 802 } 803 804 /** 805 * Sets the flag that controls whether or not the angle labels are visible, 806 * and sends a {@link PlotChangeEvent} to all registered listeners. 807 * 808 * @param visible the flag. 809 * 810 * @see #isAngleLabelsVisible() 811 */ 812 public void setAngleLabelsVisible(boolean visible) { 813 if (this.angleLabelsVisible != visible) { 814 this.angleLabelsVisible = visible; 815 fireChangeEvent(); 816 } 817 } 818 819 /** 820 * Returns the font used to display the angle labels. 821 * 822 * @return A font (never {@code null}). 823 * 824 * @see #setAngleLabelFont(Font) 825 */ 826 public Font getAngleLabelFont() { 827 return this.angleLabelFont; 828 } 829 830 /** 831 * Sets the font used to display the angle labels and sends a 832 * {@link PlotChangeEvent} to all registered listeners. 833 * 834 * @param font the font ({@code null} not permitted). 835 * 836 * @see #getAngleLabelFont() 837 */ 838 public void setAngleLabelFont(Font font) { 839 Args.nullNotPermitted(font, "font"); 840 this.angleLabelFont = font; 841 fireChangeEvent(); 842 } 843 844 /** 845 * Returns the paint used to display the angle labels. 846 * 847 * @return A paint (never {@code null}). 848 * 849 * @see #setAngleLabelPaint(Paint) 850 */ 851 public Paint getAngleLabelPaint() { 852 return this.angleLabelPaint; 853 } 854 855 /** 856 * Sets the paint used to display the angle labels and sends a 857 * {@link PlotChangeEvent} to all registered listeners. 858 * 859 * @param paint the paint ({@code null} not permitted). 860 */ 861 public void setAngleLabelPaint(Paint paint) { 862 Args.nullNotPermitted(paint, "paint"); 863 this.angleLabelPaint = paint; 864 fireChangeEvent(); 865 } 866 867 /** 868 * Returns {@code true} if the angular gridlines are visible, and 869 * {@code false} otherwise. 870 * 871 * @return {@code true} or {@code false}. 872 * 873 * @see #setAngleGridlinesVisible(boolean) 874 */ 875 public boolean isAngleGridlinesVisible() { 876 return this.angleGridlinesVisible; 877 } 878 879 /** 880 * Sets the flag that controls whether or not the angular grid-lines are 881 * visible. 882 * <p> 883 * If the flag value is changed, a {@link PlotChangeEvent} is sent to all 884 * registered listeners. 885 * 886 * @param visible the new value of the flag. 887 * 888 * @see #isAngleGridlinesVisible() 889 */ 890 public void setAngleGridlinesVisible(boolean visible) { 891 if (this.angleGridlinesVisible != visible) { 892 this.angleGridlinesVisible = visible; 893 fireChangeEvent(); 894 } 895 } 896 897 /** 898 * Returns the stroke for the grid-lines (if any) plotted against the 899 * angular axis. 900 * 901 * @return The stroke (possibly {@code null}). 902 * 903 * @see #setAngleGridlineStroke(Stroke) 904 */ 905 public Stroke getAngleGridlineStroke() { 906 return this.angleGridlineStroke; 907 } 908 909 /** 910 * Sets the stroke for the grid lines plotted against the angular axis and 911 * sends a {@link PlotChangeEvent} to all registered listeners. 912 * <p> 913 * If you set this to {@code null}, no grid lines will be drawn. 914 * 915 * @param stroke the stroke ({@code null} permitted). 916 * 917 * @see #getAngleGridlineStroke() 918 */ 919 public void setAngleGridlineStroke(Stroke stroke) { 920 this.angleGridlineStroke = stroke; 921 fireChangeEvent(); 922 } 923 924 /** 925 * Returns the paint for the grid lines (if any) plotted against the 926 * angular axis. 927 * 928 * @return The paint (possibly {@code null}). 929 * 930 * @see #setAngleGridlinePaint(Paint) 931 */ 932 public Paint getAngleGridlinePaint() { 933 return this.angleGridlinePaint; 934 } 935 936 /** 937 * Sets the paint for the grid lines plotted against the angular axis. 938 * <p> 939 * If you set this to {@code null}, no grid lines will be drawn. 940 * 941 * @param paint the paint ({@code null} permitted). 942 * 943 * @see #getAngleGridlinePaint() 944 */ 945 public void setAngleGridlinePaint(Paint paint) { 946 this.angleGridlinePaint = paint; 947 fireChangeEvent(); 948 } 949 950 /** 951 * Returns {@code true} if the radius axis grid is visible, and 952 * {@code false} otherwise. 953 * 954 * @return {@code true} or {@code false}. 955 * 956 * @see #setRadiusGridlinesVisible(boolean) 957 */ 958 public boolean isRadiusGridlinesVisible() { 959 return this.radiusGridlinesVisible; 960 } 961 962 /** 963 * Sets the flag that controls whether or not the radius axis grid lines 964 * are visible. 965 * <p> 966 * If the flag value is changed, a {@link PlotChangeEvent} is sent to all 967 * registered listeners. 968 * 969 * @param visible the new value of the flag. 970 * 971 * @see #isRadiusGridlinesVisible() 972 */ 973 public void setRadiusGridlinesVisible(boolean visible) { 974 if (this.radiusGridlinesVisible != visible) { 975 this.radiusGridlinesVisible = visible; 976 fireChangeEvent(); 977 } 978 } 979 980 /** 981 * Returns the stroke for the grid lines (if any) plotted against the 982 * radius axis. 983 * 984 * @return The stroke (possibly {@code null}). 985 * 986 * @see #setRadiusGridlineStroke(Stroke) 987 */ 988 public Stroke getRadiusGridlineStroke() { 989 return this.radiusGridlineStroke; 990 } 991 992 /** 993 * Sets the stroke for the grid lines plotted against the radius axis and 994 * sends a {@link PlotChangeEvent} to all registered listeners. 995 * <p> 996 * If you set this to {@code null}, no grid lines will be drawn. 997 * 998 * @param stroke the stroke ({@code null} permitted). 999 * 1000 * @see #getRadiusGridlineStroke() 1001 */ 1002 public void setRadiusGridlineStroke(Stroke stroke) { 1003 this.radiusGridlineStroke = stroke; 1004 fireChangeEvent(); 1005 } 1006 1007 /** 1008 * Returns the paint for the grid lines (if any) plotted against the radius 1009 * axis. 1010 * 1011 * @return The paint (possibly {@code null}). 1012 * 1013 * @see #setRadiusGridlinePaint(Paint) 1014 */ 1015 public Paint getRadiusGridlinePaint() { 1016 return this.radiusGridlinePaint; 1017 } 1018 1019 /** 1020 * Sets the paint for the grid lines plotted against the radius axis and 1021 * sends a {@link PlotChangeEvent} to all registered listeners. 1022 * <p> 1023 * If you set this to {@code null}, no grid lines will be drawn. 1024 * 1025 * @param paint the paint ({@code null} permitted). 1026 * 1027 * @see #getRadiusGridlinePaint() 1028 */ 1029 public void setRadiusGridlinePaint(Paint paint) { 1030 this.radiusGridlinePaint = paint; 1031 fireChangeEvent(); 1032 } 1033 1034 /** 1035 * Return the current value of the flag indicating if radial minor 1036 * grid-lines will be drawn or not. 1037 * 1038 * @return Returns {@code true} if radial minor grid-lines are drawn. 1039 * @since 1.0.15 1040 */ 1041 public boolean isRadiusMinorGridlinesVisible() { 1042 return this.radiusMinorGridlinesVisible; 1043 } 1044 1045 /** 1046 * Set the flag that determines if radial minor grid-lines will be drawn, 1047 * and sends a {@link PlotChangeEvent} to all registered listeners. 1048 * 1049 * @param flag {@code true} to draw the radial minor grid-lines, 1050 * {@code false} to hide them. 1051 * @since 1.0.15 1052 */ 1053 public void setRadiusMinorGridlinesVisible(boolean flag) { 1054 this.radiusMinorGridlinesVisible = flag; 1055 fireChangeEvent(); 1056 } 1057 1058 /** 1059 * Returns the margin around the plot area. 1060 * 1061 * @return The actual margin in pixels. 1062 * 1063 * @since 1.0.14 1064 */ 1065 public int getMargin() { 1066 return this.margin; 1067 } 1068 1069 /** 1070 * Set the margin around the plot area and sends a 1071 * {@link PlotChangeEvent} to all registered listeners. 1072 * 1073 * @param margin The new margin in pixels. 1074 * 1075 * @since 1.0.14 1076 */ 1077 public void setMargin(int margin) { 1078 this.margin = margin; 1079 fireChangeEvent(); 1080 } 1081 1082 /** 1083 * Returns the fixed legend items, if any. 1084 * 1085 * @return The legend items (possibly {@code null}). 1086 * 1087 * @see #setFixedLegendItems(LegendItemCollection) 1088 * 1089 * @since 1.0.14 1090 */ 1091 public LegendItemCollection getFixedLegendItems() { 1092 return this.fixedLegendItems; 1093 } 1094 1095 /** 1096 * Sets the fixed legend items for the plot. Leave this set to 1097 * {@code null} if you prefer the legend items to be created 1098 * automatically. 1099 * 1100 * @param items the legend items ({@code null} permitted). 1101 * 1102 * @see #getFixedLegendItems() 1103 * 1104 * @since 1.0.14 1105 */ 1106 public void setFixedLegendItems(LegendItemCollection items) { 1107 this.fixedLegendItems = items; 1108 fireChangeEvent(); 1109 } 1110 1111 /** 1112 * Add text to be displayed in the lower right hand corner and sends a 1113 * {@link PlotChangeEvent} to all registered listeners. 1114 * 1115 * @param text the text to display ({@code null} not permitted). 1116 * 1117 * @see #removeCornerTextItem(String) 1118 */ 1119 public void addCornerTextItem(String text) { 1120 Args.nullNotPermitted(text, "text"); 1121 this.cornerTextItems.add(text); 1122 fireChangeEvent(); 1123 } 1124 1125 /** 1126 * Remove the given text from the list of corner text items and 1127 * sends a {@link PlotChangeEvent} to all registered listeners. 1128 * 1129 * @param text the text to remove ({@code null} ignored). 1130 * 1131 * @see #addCornerTextItem(String) 1132 */ 1133 public void removeCornerTextItem(String text) { 1134 boolean removed = this.cornerTextItems.remove(text); 1135 if (removed) { 1136 fireChangeEvent(); 1137 } 1138 } 1139 1140 /** 1141 * Clear the list of corner text items and sends a {@link PlotChangeEvent} 1142 * to all registered listeners. 1143 * 1144 * @see #addCornerTextItem(String) 1145 * @see #removeCornerTextItem(String) 1146 */ 1147 public void clearCornerTextItems() { 1148 if (this.cornerTextItems.size() > 0) { 1149 this.cornerTextItems.clear(); 1150 fireChangeEvent(); 1151 } 1152 } 1153 1154 /** 1155 * Generates a list of tick values for the angular tick marks. 1156 * 1157 * @return A list of {@link NumberTick} instances. 1158 * 1159 * @since 1.0.10 1160 */ 1161 protected List refreshAngleTicks() { 1162 List ticks = new ArrayList(); 1163 for (double currentTickVal = 0.0; currentTickVal < 360.0; 1164 currentTickVal += this.angleTickUnit.getSize()) { 1165 1166 TextAnchor ta = calculateTextAnchor(currentTickVal); 1167 NumberTick tick = new NumberTick(currentTickVal, 1168 this.angleTickUnit.valueToString(currentTickVal), 1169 ta, TextAnchor.CENTER, 0.0); 1170 ticks.add(tick); 1171 } 1172 return ticks; 1173 } 1174 1175 /** 1176 * Calculate the text position for the given degrees. 1177 * 1178 * @param angleDegrees the angle in degrees. 1179 * 1180 * @return The optimal text anchor. 1181 * @since 1.0.14 1182 */ 1183 protected TextAnchor calculateTextAnchor(double angleDegrees) { 1184 TextAnchor ta = TextAnchor.CENTER; 1185 1186 // normalize angle 1187 double offset = this.angleOffset; 1188 while (offset < 0.0) { 1189 offset += 360.0; 1190 } 1191 double normalizedAngle = (((this.counterClockwise ? -1 : 1) 1192 * angleDegrees) + offset) % 360; 1193 while (this.counterClockwise && (normalizedAngle < 0.0)) { 1194 normalizedAngle += 360.0; 1195 } 1196 1197 if (normalizedAngle == 0.0) { 1198 ta = TextAnchor.CENTER_LEFT; 1199 } 1200 else if (normalizedAngle > 0.0 && normalizedAngle < 90.0) { 1201 ta = TextAnchor.TOP_LEFT; 1202 } 1203 else if (normalizedAngle == 90.0) { 1204 ta = TextAnchor.TOP_CENTER; 1205 } 1206 else if (normalizedAngle > 90.0 && normalizedAngle < 180.0) { 1207 ta = TextAnchor.TOP_RIGHT; 1208 } 1209 else if (normalizedAngle == 180) { 1210 ta = TextAnchor.CENTER_RIGHT; 1211 } 1212 else if (normalizedAngle > 180.0 && normalizedAngle < 270.0) { 1213 ta = TextAnchor.BOTTOM_RIGHT; 1214 } 1215 else if (normalizedAngle == 270) { 1216 ta = TextAnchor.BOTTOM_CENTER; 1217 } 1218 else if (normalizedAngle > 270.0 && normalizedAngle < 360.0) { 1219 ta = TextAnchor.BOTTOM_LEFT; 1220 } 1221 return ta; 1222 } 1223 1224 /** 1225 * Maps a dataset to a particular axis. All data will be plotted 1226 * against axis zero by default, no mapping is required for this case. 1227 * 1228 * @param index the dataset index (zero-based). 1229 * @param axisIndex the axis index. 1230 * 1231 * @since 1.0.14 1232 */ 1233 public void mapDatasetToAxis(int index, int axisIndex) { 1234 List<Integer> axisIndices = new ArrayList<>(1); 1235 axisIndices.add(axisIndex); 1236 mapDatasetToAxes(index, axisIndices); 1237 } 1238 1239 /** 1240 * Maps the specified dataset to the axes in the list. Note that the 1241 * conversion of data values into Java2D space is always performed using 1242 * the first axis in the list. 1243 * 1244 * @param index the dataset index (zero-based). 1245 * @param axisIndices the axis indices ({@code null} permitted). 1246 * 1247 * @since 1.0.14 1248 */ 1249 public void mapDatasetToAxes(int index, List axisIndices) { 1250 if (index < 0) { 1251 throw new IllegalArgumentException("Requires 'index' >= 0."); 1252 } 1253 checkAxisIndices(axisIndices); 1254 Integer key = index; 1255 this.datasetToAxesMap.put(key, new ArrayList(axisIndices)); 1256 // fake a dataset change event to update axes... 1257 datasetChanged(new DatasetChangeEvent(this, getDataset(index))); 1258 } 1259 1260 /** 1261 * This method is used to perform argument checking on the list of 1262 * axis indices passed to mapDatasetToAxes(). 1263 * 1264 * @param indices the list of indices ({@code null} permitted). 1265 */ 1266 private void checkAxisIndices(List indices) { 1267 // axisIndices can be: 1268 // 1. null; 1269 // 2. non-empty, containing only Integer objects that are unique. 1270 if (indices == null) { 1271 return; // OK 1272 } 1273 int count = indices.size(); 1274 if (count == 0) { 1275 throw new IllegalArgumentException("Empty list not permitted."); 1276 } 1277 HashSet set = new HashSet(); 1278 for (int i = 0; i < count; i++) { 1279 Object item = indices.get(i); 1280 if (!(item instanceof Integer)) { 1281 throw new IllegalArgumentException( 1282 "Indices must be Integer instances."); 1283 } 1284 if (set.contains(item)) { 1285 throw new IllegalArgumentException("Indices must be unique."); 1286 } 1287 set.add(item); 1288 } 1289 } 1290 1291 /** 1292 * Returns the axis for a dataset. 1293 * 1294 * @param index the dataset index. 1295 * 1296 * @return The axis. 1297 * 1298 * @since 1.0.14 1299 */ 1300 public ValueAxis getAxisForDataset(int index) { 1301 ValueAxis valueAxis; 1302 List axisIndices = (List) this.datasetToAxesMap.get(index); 1303 if (axisIndices != null) { 1304 // the first axis in the list is used for data <--> Java2D 1305 Integer axisIndex = (Integer) axisIndices.get(0); 1306 valueAxis = getAxis(axisIndex); 1307 } 1308 else { 1309 valueAxis = getAxis(0); 1310 } 1311 return valueAxis; 1312 } 1313 1314 /** 1315 * Returns the index of the given axis. 1316 * 1317 * @param axis the axis. 1318 * 1319 * @return The axis index or -1 if axis is not used in this plot. 1320 * 1321 * @since 1.0.14 1322 */ 1323 public int getAxisIndex(ValueAxis axis) { 1324 int result = this.axes.indexOf(axis); 1325 if (result < 0) { 1326 // try the parent plot 1327 Plot parent = getParent(); 1328 if (parent instanceof PolarPlot) { 1329 PolarPlot p = (PolarPlot) parent; 1330 result = p.getAxisIndex(axis); 1331 } 1332 } 1333 return result; 1334 } 1335 1336 /** 1337 * Returns the index of the specified renderer, or {@code -1} if the 1338 * renderer is not assigned to this plot. 1339 * 1340 * @param renderer the renderer ({@code null} permitted). 1341 * 1342 * @return The renderer index. 1343 * 1344 * @since 1.0.14 1345 */ 1346 public int getIndexOf(PolarItemRenderer renderer) { 1347 return this.renderers.indexOf(renderer); 1348 } 1349 1350 /** 1351 * Draws the plot on a Java 2D graphics device (such as the screen or a 1352 * printer). 1353 * <P> 1354 * This plot relies on a {@link PolarItemRenderer} to draw each 1355 * item in the plot. This allows the visual representation of the data to 1356 * be changed easily. 1357 * <P> 1358 * The optional info argument collects information about the rendering of 1359 * the plot (dimensions, tooltip information etc). Just pass in 1360 * {@code null} if you do not need this information. 1361 * 1362 * @param g2 the graphics device. 1363 * @param area the area within which the plot (including axes and 1364 * labels) should be drawn. 1365 * @param anchor the anchor point ({@code null} permitted). 1366 * @param parentState ignored. 1367 * @param info collects chart drawing information ({@code null} 1368 * permitted). 1369 */ 1370 @Override 1371 public void draw(Graphics2D g2, Rectangle2D area, Point2D anchor, 1372 PlotState parentState, PlotRenderingInfo info) { 1373 1374 // if the plot area is too small, just return... 1375 boolean b1 = (area.getWidth() <= MINIMUM_WIDTH_TO_DRAW); 1376 boolean b2 = (area.getHeight() <= MINIMUM_HEIGHT_TO_DRAW); 1377 if (b1 || b2) { 1378 return; 1379 } 1380 1381 // record the plot area... 1382 if (info != null) { 1383 info.setPlotArea(area); 1384 } 1385 1386 // adjust the drawing area for the plot insets (if any)... 1387 RectangleInsets insets = getInsets(); 1388 insets.trim(area); 1389 1390 Rectangle2D dataArea = area; 1391 if (info != null) { 1392 info.setDataArea(dataArea); 1393 } 1394 1395 // draw the plot background and axes... 1396 drawBackground(g2, dataArea); 1397 int axisCount = this.axes.size(); 1398 AxisState state = null; 1399 for (int i = 0; i < axisCount; i++) { 1400 ValueAxis axis = getAxis(i); 1401 if (axis != null) { 1402 PolarAxisLocation location 1403 = (PolarAxisLocation) this.axisLocations.get(i); 1404 AxisState s = this.drawAxis(axis, location, g2, dataArea); 1405 if (i == 0) { 1406 state = s; 1407 } 1408 } 1409 } 1410 1411 // now for each dataset, get the renderer and the appropriate axis 1412 // and render the dataset... 1413 Shape originalClip = g2.getClip(); 1414 Composite originalComposite = g2.getComposite(); 1415 1416 g2.clip(dataArea); 1417 g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1418 getForegroundAlpha())); 1419 this.angleTicks = refreshAngleTicks(); 1420 drawGridlines(g2, dataArea, this.angleTicks, state.getTicks()); 1421 render(g2, dataArea, info); 1422 g2.setClip(originalClip); 1423 g2.setComposite(originalComposite); 1424 drawOutline(g2, dataArea); 1425 drawCornerTextItems(g2, dataArea); 1426 } 1427 1428 /** 1429 * Draws the corner text items. 1430 * 1431 * @param g2 the drawing surface. 1432 * @param area the area. 1433 */ 1434 protected void drawCornerTextItems(Graphics2D g2, Rectangle2D area) { 1435 if (this.cornerTextItems.isEmpty()) { 1436 return; 1437 } 1438 1439 g2.setColor(Color.BLACK); 1440 double width = 0.0; 1441 double height = 0.0; 1442 for (Iterator it = this.cornerTextItems.iterator(); it.hasNext();) { 1443 String msg = (String) it.next(); 1444 FontMetrics fm = g2.getFontMetrics(); 1445 Rectangle2D bounds = TextUtils.getTextBounds(msg, g2, fm); 1446 width = Math.max(width, bounds.getWidth()); 1447 height += bounds.getHeight(); 1448 } 1449 1450 double xadj = ANNOTATION_MARGIN * 2.0; 1451 double yadj = ANNOTATION_MARGIN; 1452 width += xadj; 1453 height += yadj; 1454 1455 double x = area.getMaxX() - width; 1456 double y = area.getMaxY() - height; 1457 g2.drawRect((int) x, (int) y, (int) width, (int) height); 1458 x += ANNOTATION_MARGIN; 1459 for (Iterator it = this.cornerTextItems.iterator(); it.hasNext();) { 1460 String msg = (String) it.next(); 1461 Rectangle2D bounds = TextUtils.getTextBounds(msg, g2, 1462 g2.getFontMetrics()); 1463 y += bounds.getHeight(); 1464 g2.drawString(msg, (int) x, (int) y); 1465 } 1466 } 1467 1468 /** 1469 * Draws the axis with the specified index. 1470 * 1471 * @param axis the axis. 1472 * @param location the axis location. 1473 * @param g2 the graphics target. 1474 * @param plotArea the plot area. 1475 * 1476 * @return The axis state. 1477 * 1478 * @since 1.0.14 1479 */ 1480 protected AxisState drawAxis(ValueAxis axis, PolarAxisLocation location, 1481 Graphics2D g2, Rectangle2D plotArea) { 1482 1483 double centerX = plotArea.getCenterX(); 1484 double centerY = plotArea.getCenterY(); 1485 double r = Math.min(plotArea.getWidth() / 2.0, 1486 plotArea.getHeight() / 2.0) - this.margin; 1487 double x = centerX - r; 1488 double y = centerY - r; 1489 1490 Rectangle2D dataArea; 1491 AxisState result = null; 1492 if (location == PolarAxisLocation.NORTH_RIGHT) { 1493 dataArea = new Rectangle2D.Double(x, y, r, r); 1494 result = axis.draw(g2, centerX, plotArea, dataArea, 1495 RectangleEdge.RIGHT, null); 1496 } 1497 else if (location == PolarAxisLocation.NORTH_LEFT) { 1498 dataArea = new Rectangle2D.Double(centerX, y, r, r); 1499 result = axis.draw(g2, centerX, plotArea, dataArea, 1500 RectangleEdge.LEFT, null); 1501 } 1502 else if (location == PolarAxisLocation.SOUTH_LEFT) { 1503 dataArea = new Rectangle2D.Double(centerX, centerY, r, r); 1504 result = axis.draw(g2, centerX, plotArea, dataArea, 1505 RectangleEdge.LEFT, null); 1506 } 1507 else if (location == PolarAxisLocation.SOUTH_RIGHT) { 1508 dataArea = new Rectangle2D.Double(x, centerY, r, r); 1509 result = axis.draw(g2, centerX, plotArea, dataArea, 1510 RectangleEdge.RIGHT, null); 1511 } 1512 else if (location == PolarAxisLocation.EAST_ABOVE) { 1513 dataArea = new Rectangle2D.Double(centerX, centerY, r, r); 1514 result = axis.draw(g2, centerY, plotArea, dataArea, 1515 RectangleEdge.TOP, null); 1516 } 1517 else if (location == PolarAxisLocation.EAST_BELOW) { 1518 dataArea = new Rectangle2D.Double(centerX, y, r, r); 1519 result = axis.draw(g2, centerY, plotArea, dataArea, 1520 RectangleEdge.BOTTOM, null); 1521 } 1522 else if (location == PolarAxisLocation.WEST_ABOVE) { 1523 dataArea = new Rectangle2D.Double(x, centerY, r, r); 1524 result = axis.draw(g2, centerY, plotArea, dataArea, 1525 RectangleEdge.TOP, null); 1526 } 1527 else if (location == PolarAxisLocation.WEST_BELOW) { 1528 dataArea = new Rectangle2D.Double(x, y, r, r); 1529 result = axis.draw(g2, centerY, plotArea, dataArea, 1530 RectangleEdge.BOTTOM, null); 1531 } 1532 1533 return result; 1534 } 1535 1536 /** 1537 * Draws a representation of the data within the dataArea region, using the 1538 * current m_Renderer. 1539 * 1540 * @param g2 the graphics device. 1541 * @param dataArea the region in which the data is to be drawn. 1542 * @param info an optional object for collection dimension 1543 * information ({@code null} permitted). 1544 */ 1545 protected void render(Graphics2D g2, Rectangle2D dataArea, 1546 PlotRenderingInfo info) { 1547 1548 // now get the data and plot it (the visual representation will depend 1549 // on the m_Renderer that has been set)... 1550 boolean hasData = false; 1551 int datasetCount = this.datasets.size(); 1552 for (int i = datasetCount - 1; i >= 0; i--) { 1553 XYDataset dataset = getDataset(i); 1554 if (dataset == null) { 1555 continue; 1556 } 1557 PolarItemRenderer renderer = getRenderer(i); 1558 if (renderer == null) { 1559 continue; 1560 } 1561 if (!DatasetUtils.isEmptyOrNull(dataset)) { 1562 hasData = true; 1563 int seriesCount = dataset.getSeriesCount(); 1564 for (int series = 0; series < seriesCount; series++) { 1565 renderer.drawSeries(g2, dataArea, info, this, dataset, 1566 series); 1567 } 1568 } 1569 } 1570 if (!hasData) { 1571 drawNoDataMessage(g2, dataArea); 1572 } 1573 } 1574 1575 /** 1576 * Draws the gridlines for the plot, if they are visible. 1577 * 1578 * @param g2 the graphics device. 1579 * @param dataArea the data area. 1580 * @param angularTicks the ticks for the angular axis. 1581 * @param radialTicks the ticks for the radial axis. 1582 */ 1583 protected void drawGridlines(Graphics2D g2, Rectangle2D dataArea, 1584 List angularTicks, List radialTicks) { 1585 1586 PolarItemRenderer renderer = getRenderer(); 1587 // no renderer, no gridlines... 1588 if (renderer == null) { 1589 return; 1590 } 1591 1592 // draw the domain grid lines, if any... 1593 if (isAngleGridlinesVisible()) { 1594 Stroke gridStroke = getAngleGridlineStroke(); 1595 Paint gridPaint = getAngleGridlinePaint(); 1596 if ((gridStroke != null) && (gridPaint != null)) { 1597 renderer.drawAngularGridLines(g2, this, angularTicks, 1598 dataArea); 1599 } 1600 } 1601 1602 // draw the radius grid lines, if any... 1603 if (isRadiusGridlinesVisible()) { 1604 Stroke gridStroke = getRadiusGridlineStroke(); 1605 Paint gridPaint = getRadiusGridlinePaint(); 1606 if ((gridStroke != null) && (gridPaint != null)) { 1607 List ticks = buildRadialTicks(radialTicks); 1608 renderer.drawRadialGridLines(g2, this, getAxis(), 1609 ticks, dataArea); 1610 } 1611 } 1612 } 1613 1614 /** 1615 * Create a list of ticks based on the given list and plot properties. 1616 * Only ticks of a specific type may be in the result list. 1617 * 1618 * @param allTicks A list of all available ticks for the primary axis. 1619 * {@code null} not permitted. 1620 * @return Ticks to use for radial gridlines. 1621 * @since 1.0.15 1622 */ 1623 protected List buildRadialTicks(List allTicks) 1624 { 1625 List ticks = new ArrayList(); 1626 Iterator it = allTicks.iterator(); 1627 while (it.hasNext()) { 1628 ValueTick tick = (ValueTick) it.next(); 1629 if (isRadiusMinorGridlinesVisible() || 1630 TickType.MAJOR.equals(tick.getTickType())) { 1631 ticks.add(tick); 1632 } 1633 } 1634 return ticks; 1635 } 1636 1637 /** 1638 * Zooms the axis ranges by the specified percentage about the anchor point. 1639 * 1640 * @param percent the amount of the zoom. 1641 */ 1642 @Override 1643 public void zoom(double percent) { 1644 for (int axisIdx = 0; axisIdx < getAxisCount(); axisIdx++) { 1645 final ValueAxis axis = getAxis(axisIdx); 1646 if (axis != null) { 1647 if (percent > 0.0) { 1648 double radius = axis.getUpperBound(); 1649 double scaledRadius = radius * percent; 1650 axis.setUpperBound(scaledRadius); 1651 axis.setAutoRange(false); 1652 } 1653 else { 1654 axis.setAutoRange(true); 1655 } 1656 } 1657 } 1658 } 1659 1660 /** 1661 * A utility method that returns a list of datasets that are mapped to a 1662 * particular axis. 1663 * 1664 * @param axisIndex the axis index ({@code null} not permitted). 1665 * 1666 * @return A list of datasets. 1667 * 1668 * @since 1.0.14 1669 */ 1670 private List getDatasetsMappedToAxis(Integer axisIndex) { 1671 Args.nullNotPermitted(axisIndex, "axisIndex"); 1672 List result = new ArrayList(); 1673 for (int i = 0; i < this.datasets.size(); i++) { 1674 List mappedAxes = (List) this.datasetToAxesMap.get(i); 1675 if (mappedAxes == null) { 1676 if (axisIndex.equals(ZERO)) { 1677 result.add(this.datasets.get(i)); 1678 } 1679 } 1680 else { 1681 if (mappedAxes.contains(axisIndex)) { 1682 result.add(this.datasets.get(i)); 1683 } 1684 } 1685 } 1686 return result; 1687 } 1688 1689 /** 1690 * Returns the range for the specified axis. 1691 * 1692 * @param axis the axis. 1693 * 1694 * @return The range. 1695 */ 1696 @Override 1697 public Range getDataRange(ValueAxis axis) { 1698 Range result = null; 1699 int axisIdx = getAxisIndex(axis); 1700 List mappedDatasets = new ArrayList(); 1701 1702 if (axisIdx >= 0) { 1703 mappedDatasets = getDatasetsMappedToAxis(axisIdx); 1704 } 1705 1706 // iterate through the datasets that map to the axis and get the union 1707 // of the ranges. 1708 Iterator iterator = mappedDatasets.iterator(); 1709 int datasetIdx = -1; 1710 while (iterator.hasNext()) { 1711 datasetIdx++; 1712 XYDataset d = (XYDataset) iterator.next(); 1713 if (d != null) { 1714 // FIXME better ask the renderer instead of DatasetUtilities 1715 result = Range.combine(result, 1716 DatasetUtils.findRangeBounds(d)); 1717 } 1718 } 1719 1720 return result; 1721 } 1722 1723 /** 1724 * Receives notification of a change to the plot's m_Dataset. 1725 * <P> 1726 * The axis ranges are updated if necessary. 1727 * 1728 * @param event information about the event (not used here). 1729 */ 1730 @Override 1731 public void datasetChanged(DatasetChangeEvent event) { 1732 for (int i = 0; i < this.axes.size(); i++) { 1733 final ValueAxis axis = (ValueAxis) this.axes.get(i); 1734 if (axis != null) { 1735 axis.configure(); 1736 } 1737 } 1738 if (getParent() != null) { 1739 getParent().datasetChanged(event); 1740 } 1741 else { 1742 super.datasetChanged(event); 1743 } 1744 } 1745 1746 /** 1747 * Notifies all registered listeners of a property change. 1748 * <P> 1749 * One source of property change events is the plot's m_Renderer. 1750 * 1751 * @param event information about the property change. 1752 */ 1753 @Override 1754 public void rendererChanged(RendererChangeEvent event) { 1755 fireChangeEvent(); 1756 } 1757 1758 /** 1759 * Returns the legend items for the plot. Each legend item is generated by 1760 * the plot's m_Renderer, since the m_Renderer is responsible for the visual 1761 * representation of the data. 1762 * 1763 * @return The legend items. 1764 */ 1765 @Override 1766 public LegendItemCollection getLegendItems() { 1767 if (this.fixedLegendItems != null) { 1768 return this.fixedLegendItems; 1769 } 1770 LegendItemCollection result = new LegendItemCollection(); 1771 int count = this.datasets.size(); 1772 for (int datasetIndex = 0; datasetIndex < count; datasetIndex++) { 1773 XYDataset dataset = getDataset(datasetIndex); 1774 PolarItemRenderer renderer = getRenderer(datasetIndex); 1775 if (dataset != null && renderer != null) { 1776 int seriesCount = dataset.getSeriesCount(); 1777 for (int i = 0; i < seriesCount; i++) { 1778 LegendItem item = renderer.getLegendItem(i); 1779 result.add(item); 1780 } 1781 } 1782 } 1783 return result; 1784 } 1785 1786 /** 1787 * Tests this plot for equality with another object. 1788 * 1789 * @param obj the object ({@code null} permitted). 1790 * 1791 * @return {@code true} or {@code false}. 1792 */ 1793 @Override 1794 public boolean equals(Object obj) { 1795 if (obj == this) { 1796 return true; 1797 } 1798 if (!(obj instanceof PolarPlot)) { 1799 return false; 1800 } 1801 PolarPlot that = (PolarPlot) obj; 1802 if (!this.axes.equals(that.axes)) { 1803 return false; 1804 } 1805 if (!this.axisLocations.equals(that.axisLocations)) { 1806 return false; 1807 } 1808 if (!this.renderers.equals(that.renderers)) { 1809 return false; 1810 } 1811 if (!this.angleTickUnit.equals(that.angleTickUnit)) { 1812 return false; 1813 } 1814 if (this.angleGridlinesVisible != that.angleGridlinesVisible) { 1815 return false; 1816 } 1817 if (this.angleOffset != that.angleOffset) 1818 { 1819 return false; 1820 } 1821 if (this.counterClockwise != that.counterClockwise) 1822 { 1823 return false; 1824 } 1825 if (this.angleLabelsVisible != that.angleLabelsVisible) { 1826 return false; 1827 } 1828 if (!this.angleLabelFont.equals(that.angleLabelFont)) { 1829 return false; 1830 } 1831 if (!PaintUtils.equal(this.angleLabelPaint, that.angleLabelPaint)) { 1832 return false; 1833 } 1834 if (!ObjectUtils.equal(this.angleGridlineStroke, 1835 that.angleGridlineStroke)) { 1836 return false; 1837 } 1838 if (!PaintUtils.equal( 1839 this.angleGridlinePaint, that.angleGridlinePaint 1840 )) { 1841 return false; 1842 } 1843 if (this.radiusGridlinesVisible != that.radiusGridlinesVisible) { 1844 return false; 1845 } 1846 if (!ObjectUtils.equal(this.radiusGridlineStroke, 1847 that.radiusGridlineStroke)) { 1848 return false; 1849 } 1850 if (!PaintUtils.equal(this.radiusGridlinePaint, 1851 that.radiusGridlinePaint)) { 1852 return false; 1853 } 1854 if (this.radiusMinorGridlinesVisible != 1855 that.radiusMinorGridlinesVisible) { 1856 return false; 1857 } 1858 if (!this.cornerTextItems.equals(that.cornerTextItems)) { 1859 return false; 1860 } 1861 if (this.margin != that.margin) { 1862 return false; 1863 } 1864 if (!ObjectUtils.equal(this.fixedLegendItems, 1865 that.fixedLegendItems)) { 1866 return false; 1867 } 1868 return super.equals(obj); 1869 } 1870 1871 /** 1872 * Returns a clone of the plot. 1873 * 1874 * @return A clone. 1875 * 1876 * @throws CloneNotSupportedException this can occur if some component of 1877 * the plot cannot be cloned. 1878 */ 1879 @Override 1880 public Object clone() throws CloneNotSupportedException { 1881 PolarPlot clone = (PolarPlot) super.clone(); 1882 clone.axes = (ObjectList) ObjectUtils.clone(this.axes); 1883 for (int i = 0; i < this.axes.size(); i++) { 1884 ValueAxis axis = (ValueAxis) this.axes.get(i); 1885 if (axis != null) { 1886 ValueAxis clonedAxis = (ValueAxis) axis.clone(); 1887 clone.axes.set(i, clonedAxis); 1888 clonedAxis.setPlot(clone); 1889 clonedAxis.addChangeListener(clone); 1890 } 1891 } 1892 1893 // the datasets are not cloned, but listeners need to be added... 1894 clone.datasets = (ObjectList) ObjectUtils.clone(this.datasets); 1895 for (int i = 0; i < clone.datasets.size(); ++i) { 1896 XYDataset d = getDataset(i); 1897 if (d != null) { 1898 d.addChangeListener(clone); 1899 } 1900 } 1901 1902 clone.renderers = (ObjectList) ObjectUtils.clone(this.renderers); 1903 for (int i = 0; i < this.renderers.size(); i++) { 1904 PolarItemRenderer renderer2 = (PolarItemRenderer) this.renderers.get(i); 1905 if (renderer2 instanceof PublicCloneable) { 1906 PublicCloneable pc = (PublicCloneable) renderer2; 1907 PolarItemRenderer rc = (PolarItemRenderer) pc.clone(); 1908 clone.renderers.set(i, rc); 1909 rc.setPlot(clone); 1910 rc.addChangeListener(clone); 1911 } 1912 } 1913 1914 clone.cornerTextItems = new ArrayList(this.cornerTextItems); 1915 1916 return clone; 1917 } 1918 1919 /** 1920 * Provides serialization support. 1921 * 1922 * @param stream the output stream. 1923 * 1924 * @throws IOException if there is an I/O error. 1925 */ 1926 private void writeObject(ObjectOutputStream stream) throws IOException { 1927 stream.defaultWriteObject(); 1928 SerialUtils.writeStroke(this.angleGridlineStroke, stream); 1929 SerialUtils.writePaint(this.angleGridlinePaint, stream); 1930 SerialUtils.writeStroke(this.radiusGridlineStroke, stream); 1931 SerialUtils.writePaint(this.radiusGridlinePaint, stream); 1932 SerialUtils.writePaint(this.angleLabelPaint, stream); 1933 } 1934 1935 /** 1936 * Provides serialization support. 1937 * 1938 * @param stream the input stream. 1939 * 1940 * @throws IOException if there is an I/O error. 1941 * @throws ClassNotFoundException if there is a classpath problem. 1942 */ 1943 private void readObject(ObjectInputStream stream) 1944 throws IOException, ClassNotFoundException { 1945 1946 stream.defaultReadObject(); 1947 this.angleGridlineStroke = SerialUtils.readStroke(stream); 1948 this.angleGridlinePaint = SerialUtils.readPaint(stream); 1949 this.radiusGridlineStroke = SerialUtils.readStroke(stream); 1950 this.radiusGridlinePaint = SerialUtils.readPaint(stream); 1951 this.angleLabelPaint = SerialUtils.readPaint(stream); 1952 1953 int rangeAxisCount = this.axes.size(); 1954 for (int i = 0; i < rangeAxisCount; i++) { 1955 Axis axis = (Axis) this.axes.get(i); 1956 if (axis != null) { 1957 axis.setPlot(this); 1958 axis.addChangeListener(this); 1959 } 1960 } 1961 int datasetCount = this.datasets.size(); 1962 for (int i = 0; i < datasetCount; i++) { 1963 Dataset dataset = (Dataset) this.datasets.get(i); 1964 if (dataset != null) { 1965 dataset.addChangeListener(this); 1966 } 1967 } 1968 int rendererCount = this.renderers.size(); 1969 for (int i = 0; i < rendererCount; i++) { 1970 PolarItemRenderer renderer = (PolarItemRenderer) this.renderers.get(i); 1971 if (renderer != null) { 1972 renderer.addChangeListener(this); 1973 } 1974 } 1975 } 1976 1977 /** 1978 * This method is required by the {@link Zoomable} interface, but since 1979 * the plot does not have any domain axes, it does nothing. 1980 * 1981 * @param factor the zoom factor. 1982 * @param state the plot state. 1983 * @param source the source point (in Java2D coordinates). 1984 */ 1985 @Override 1986 public void zoomDomainAxes(double factor, PlotRenderingInfo state, 1987 Point2D source) { 1988 // do nothing 1989 } 1990 1991 /** 1992 * This method is required by the {@link Zoomable} interface, but since 1993 * the plot does not have any domain axes, it does nothing. 1994 * 1995 * @param factor the zoom factor. 1996 * @param state the plot state. 1997 * @param source the source point (in Java2D coordinates). 1998 * @param useAnchor use source point as zoom anchor? 1999 * 2000 * @since 1.0.7 2001 */ 2002 @Override 2003 public void zoomDomainAxes(double factor, PlotRenderingInfo state, 2004 Point2D source, boolean useAnchor) { 2005 // do nothing 2006 } 2007 2008 /** 2009 * This method is required by the {@link Zoomable} interface, but since 2010 * the plot does not have any domain axes, it does nothing. 2011 * 2012 * @param lowerPercent the new lower bound. 2013 * @param upperPercent the new upper bound. 2014 * @param state the plot state. 2015 * @param source the source point (in Java2D coordinates). 2016 */ 2017 @Override 2018 public void zoomDomainAxes(double lowerPercent, double upperPercent, 2019 PlotRenderingInfo state, Point2D source) { 2020 // do nothing 2021 } 2022 2023 /** 2024 * Multiplies the range on the range axis/axes by the specified factor. 2025 * 2026 * @param factor the zoom factor. 2027 * @param state the plot state. 2028 * @param source the source point (in Java2D coordinates). 2029 */ 2030 @Override 2031 public void zoomRangeAxes(double factor, PlotRenderingInfo state, 2032 Point2D source) { 2033 zoom(factor); 2034 } 2035 2036 /** 2037 * Multiplies the range on the range axis by the specified factor. 2038 * 2039 * @param factor the zoom factor. 2040 * @param info the plot rendering info. 2041 * @param source the source point (in Java2D space). 2042 * @param useAnchor use source point as zoom anchor? 2043 * 2044 * @see #zoomDomainAxes(double, PlotRenderingInfo, Point2D, boolean) 2045 * 2046 * @since 1.0.7 2047 */ 2048 @Override 2049 public void zoomRangeAxes(double factor, PlotRenderingInfo info, 2050 Point2D source, boolean useAnchor) { 2051 // get the source coordinate - this plot has always a VERTICAL 2052 // orientation 2053 final double sourceX = source.getX(); 2054 2055 for (int axisIdx = 0; axisIdx < getAxisCount(); axisIdx++) { 2056 final ValueAxis axis = getAxis(axisIdx); 2057 if (axis != null) { 2058 if (useAnchor) { 2059 double anchorX = axis.java2DToValue(sourceX, 2060 info.getDataArea(), RectangleEdge.BOTTOM); 2061 axis.resizeRange(factor, anchorX); 2062 } 2063 else { 2064 axis.resizeRange(factor); 2065 } 2066 } 2067 } 2068 } 2069 2070 /** 2071 * Zooms in on the range axes. 2072 * 2073 * @param lowerPercent the new lower bound. 2074 * @param upperPercent the new upper bound. 2075 * @param state the plot state. 2076 * @param source the source point (in Java2D coordinates). 2077 */ 2078 @Override 2079 public void zoomRangeAxes(double lowerPercent, double upperPercent, 2080 PlotRenderingInfo state, Point2D source) { 2081 zoom((upperPercent + lowerPercent) / 2.0); 2082 } 2083 2084 /** 2085 * Returns {@code false} always. 2086 * 2087 * @return {@code false} always. 2088 */ 2089 @Override 2090 public boolean isDomainZoomable() { 2091 return false; 2092 } 2093 2094 /** 2095 * Returns {@code true} to indicate that the range axis is zoomable. 2096 * 2097 * @return {@code true}. 2098 */ 2099 @Override 2100 public boolean isRangeZoomable() { 2101 return true; 2102 } 2103 2104 /** 2105 * Returns the orientation of the plot. 2106 * 2107 * @return The orientation. 2108 */ 2109 @Override 2110 public PlotOrientation getOrientation() { 2111 return PlotOrientation.HORIZONTAL; 2112 } 2113 2114 /** 2115 * Translates a (theta, radius) pair into Java2D coordinates. If 2116 * {@code radius} is less than the lower bound of the axis, then 2117 * this method returns the centre point. 2118 * 2119 * @param angleDegrees the angle in degrees. 2120 * @param radius the radius. 2121 * @param axis the axis. 2122 * @param dataArea the data area. 2123 * 2124 * @return A point in Java2D space. 2125 * 2126 * @since 1.0.14 2127 */ 2128 public Point translateToJava2D(double angleDegrees, double radius, 2129 ValueAxis axis, Rectangle2D dataArea) { 2130 2131 if (counterClockwise) { 2132 angleDegrees = -angleDegrees; 2133 } 2134 double radians = Math.toRadians(angleDegrees + this.angleOffset); 2135 2136 double minx = dataArea.getMinX() + this.margin; 2137 double maxx = dataArea.getMaxX() - this.margin; 2138 double miny = dataArea.getMinY() + this.margin; 2139 double maxy = dataArea.getMaxY() - this.margin; 2140 2141 double halfWidth = (maxx - minx) / 2.0; 2142 double halfHeight = (maxy - miny) / 2.0; 2143 2144 double midX = minx + halfWidth; 2145 double midY = miny + halfHeight; 2146 2147 double l = Math.min(halfWidth, halfHeight); 2148 Rectangle2D quadrant = new Rectangle2D.Double(midX, midY, l, l); 2149 2150 double axisMin = axis.getLowerBound(); 2151 double adjustedRadius = Math.max(radius, axisMin); 2152 2153 double length = axis.valueToJava2D(adjustedRadius, quadrant, RectangleEdge.BOTTOM) - midX; 2154 float x = (float) (midX + Math.cos(radians) * length); 2155 float y = (float) (midY + Math.sin(radians) * length); 2156 2157 int ix = Math.round(x); 2158 int iy = Math.round(y); 2159 2160 Point p = new Point(ix, iy); 2161 return p; 2162 2163 } 2164 2165}