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 * CategoryAxis.java 029 * ----------------- 030 * (C) Copyright 2000-2022, by David Gilbert and Contributors. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): Pady Srinivasan (patch 1217634); 034 * Peter Kolb (patches 2497611 and 2603321); 035 * 036 */ 037 038package org.jfree.chart.axis; 039 040import java.awt.Font; 041import java.awt.Graphics2D; 042import java.awt.Paint; 043import java.awt.RenderingHints; 044import java.awt.Shape; 045import java.awt.geom.Line2D; 046import java.awt.geom.Point2D; 047import java.awt.geom.Rectangle2D; 048import java.io.IOException; 049import java.io.ObjectInputStream; 050import java.io.ObjectOutputStream; 051import java.io.Serializable; 052import java.util.HashMap; 053import java.util.List; 054import java.util.Map; 055import java.util.Objects; 056import java.util.Set; 057 058import org.jfree.chart.entity.CategoryLabelEntity; 059import org.jfree.chart.entity.EntityCollection; 060import org.jfree.chart.event.AxisChangeEvent; 061import org.jfree.chart.plot.CategoryPlot; 062import org.jfree.chart.plot.Plot; 063import org.jfree.chart.plot.PlotRenderingInfo; 064import org.jfree.chart.text.G2TextMeasurer; 065import org.jfree.chart.text.TextBlock; 066import org.jfree.chart.text.TextUtils; 067import org.jfree.chart.api.RectangleEdge; 068import org.jfree.chart.api.RectangleInsets; 069import org.jfree.chart.block.Size2D; 070import org.jfree.chart.internal.Args; 071import org.jfree.chart.internal.PaintUtils; 072import org.jfree.chart.internal.SerialUtils; 073import org.jfree.chart.internal.ShapeUtils; 074import org.jfree.data.category.CategoryDataset; 075 076/** 077 * An axis that displays categories. 078 */ 079public class CategoryAxis extends Axis implements Cloneable, Serializable { 080 081 /** For serialization. */ 082 private static final long serialVersionUID = 5886554608114265863L; 083 084 /** 085 * The default margin for the axis (used for both lower and upper margins). 086 */ 087 public static final double DEFAULT_AXIS_MARGIN = 0.05; 088 089 /** 090 * The default margin between categories (a percentage of the overall axis 091 * length). 092 */ 093 public static final double DEFAULT_CATEGORY_MARGIN = 0.20; 094 095 /** The amount of space reserved at the start of the axis. */ 096 private double lowerMargin; 097 098 /** The amount of space reserved at the end of the axis. */ 099 private double upperMargin; 100 101 /** The amount of space reserved between categories. */ 102 private double categoryMargin; 103 104 /** The maximum number of lines for category labels. */ 105 private int maximumCategoryLabelLines; 106 107 /** 108 * A ratio that is multiplied by the width of one category to determine the 109 * maximum label width. 110 */ 111 private float maximumCategoryLabelWidthRatio; 112 113 /** The category label offset. */ 114 private int categoryLabelPositionOffset; 115 116 /** 117 * A structure defining the category label positions for each axis 118 * location. 119 */ 120 private CategoryLabelPositions categoryLabelPositions; 121 122 /** Storage for tick label font overrides (if any). */ 123 private Map<Comparable, Font> tickLabelFontMap; 124 125 /** Storage for tick label paint overrides (if any). */ 126 private transient Map<Comparable, Paint> tickLabelPaintMap; 127 128 /** Storage for the category label tooltips (if any). */ 129 private Map<Comparable, String> categoryLabelToolTips; 130 131 /** Storage for the category label URLs (if any). */ 132 private Map<Comparable, String> categoryLabelURLs; 133 134 /** 135 * Creates a new category axis with no label. 136 */ 137 public CategoryAxis() { 138 this(null); 139 } 140 141 /** 142 * Constructs a category axis, using default values where necessary. 143 * 144 * @param label the axis label ({@code null} permitted). 145 */ 146 public CategoryAxis(String label) { 147 super(label); 148 149 this.lowerMargin = DEFAULT_AXIS_MARGIN; 150 this.upperMargin = DEFAULT_AXIS_MARGIN; 151 this.categoryMargin = DEFAULT_CATEGORY_MARGIN; 152 this.maximumCategoryLabelLines = 1; 153 this.maximumCategoryLabelWidthRatio = 0.0f; 154 155 this.categoryLabelPositionOffset = 4; 156 this.categoryLabelPositions = CategoryLabelPositions.STANDARD; 157 this.tickLabelFontMap = new HashMap<>(); 158 this.tickLabelPaintMap = new HashMap<>(); 159 this.categoryLabelToolTips = new HashMap<>(); 160 this.categoryLabelURLs = new HashMap<>(); 161 } 162 163 /** 164 * Returns the lower margin for the axis. 165 * 166 * @return The margin. 167 * 168 * @see #getUpperMargin() 169 * @see #setLowerMargin(double) 170 */ 171 public double getLowerMargin() { 172 return this.lowerMargin; 173 } 174 175 /** 176 * Sets the lower margin for the axis and sends an {@link AxisChangeEvent} 177 * to all registered listeners. 178 * 179 * @param margin the margin as a percentage of the axis length (for 180 * example, 0.05 is five percent). 181 * 182 * @see #getLowerMargin() 183 */ 184 public void setLowerMargin(double margin) { 185 this.lowerMargin = margin; 186 fireChangeEvent(); 187 } 188 189 /** 190 * Returns the upper margin for the axis. 191 * 192 * @return The margin. 193 * 194 * @see #getLowerMargin() 195 * @see #setUpperMargin(double) 196 */ 197 public double getUpperMargin() { 198 return this.upperMargin; 199 } 200 201 /** 202 * Sets the upper margin for the axis and sends an {@link AxisChangeEvent} 203 * to all registered listeners. 204 * 205 * @param margin the margin as a percentage of the axis length (for 206 * example, 0.05 is five percent). 207 * 208 * @see #getUpperMargin() 209 */ 210 public void setUpperMargin(double margin) { 211 this.upperMargin = margin; 212 fireChangeEvent(); 213 } 214 215 /** 216 * Returns the category margin. 217 * 218 * @return The margin. 219 * 220 * @see #setCategoryMargin(double) 221 */ 222 public double getCategoryMargin() { 223 return this.categoryMargin; 224 } 225 226 /** 227 * Sets the category margin and sends an {@link AxisChangeEvent} to all 228 * registered listeners. The overall category margin is distributed over 229 * N-1 gaps, where N is the number of categories on the axis. 230 * 231 * @param margin the margin as a percentage of the axis length (for 232 * example, 0.05 is five percent). 233 * 234 * @see #getCategoryMargin() 235 */ 236 public void setCategoryMargin(double margin) { 237 this.categoryMargin = margin; 238 fireChangeEvent(); 239 } 240 241 /** 242 * Returns the maximum number of lines to use for each category label. 243 * 244 * @return The maximum number of lines. 245 * 246 * @see #setMaximumCategoryLabelLines(int) 247 */ 248 public int getMaximumCategoryLabelLines() { 249 return this.maximumCategoryLabelLines; 250 } 251 252 /** 253 * Sets the maximum number of lines to use for each category label and 254 * sends an {@link AxisChangeEvent} to all registered listeners. 255 * 256 * @param lines the maximum number of lines. 257 * 258 * @see #getMaximumCategoryLabelLines() 259 */ 260 public void setMaximumCategoryLabelLines(int lines) { 261 this.maximumCategoryLabelLines = lines; 262 fireChangeEvent(); 263 } 264 265 /** 266 * Returns the category label width ratio. 267 * 268 * @return The ratio. 269 * 270 * @see #setMaximumCategoryLabelWidthRatio(float) 271 */ 272 public float getMaximumCategoryLabelWidthRatio() { 273 return this.maximumCategoryLabelWidthRatio; 274 } 275 276 /** 277 * Sets the maximum category label width ratio and sends an 278 * {@link AxisChangeEvent} to all registered listeners. 279 * 280 * @param ratio the ratio. 281 * 282 * @see #getMaximumCategoryLabelWidthRatio() 283 */ 284 public void setMaximumCategoryLabelWidthRatio(float ratio) { 285 this.maximumCategoryLabelWidthRatio = ratio; 286 fireChangeEvent(); 287 } 288 289 /** 290 * Returns the offset between the axis and the category labels (before 291 * label positioning is taken into account). 292 * 293 * @return The offset (in Java2D units). 294 * 295 * @see #setCategoryLabelPositionOffset(int) 296 */ 297 public int getCategoryLabelPositionOffset() { 298 return this.categoryLabelPositionOffset; 299 } 300 301 /** 302 * Sets the offset between the axis and the category labels (before label 303 * positioning is taken into account) and sends a change event to all 304 * registered listeners. 305 * 306 * @param offset the offset (in Java2D units). 307 * 308 * @see #getCategoryLabelPositionOffset() 309 */ 310 public void setCategoryLabelPositionOffset(int offset) { 311 this.categoryLabelPositionOffset = offset; 312 fireChangeEvent(); 313 } 314 315 /** 316 * Returns the category label position specification (this contains label 317 * positioning info for all four possible axis locations). 318 * 319 * @return The positions (never {@code null}). 320 * 321 * @see #setCategoryLabelPositions(CategoryLabelPositions) 322 */ 323 public CategoryLabelPositions getCategoryLabelPositions() { 324 return this.categoryLabelPositions; 325 } 326 327 /** 328 * Sets the category label position specification for the axis and sends an 329 * {@link AxisChangeEvent} to all registered listeners. 330 * 331 * @param positions the positions ({@code null} not permitted). 332 * 333 * @see #getCategoryLabelPositions() 334 */ 335 public void setCategoryLabelPositions(CategoryLabelPositions positions) { 336 Args.nullNotPermitted(positions, "positions"); 337 this.categoryLabelPositions = positions; 338 fireChangeEvent(); 339 } 340 341 /** 342 * Returns the font for the tick label for the given category. 343 * 344 * @param category the category ({@code null} not permitted). 345 * 346 * @return The font (never {@code null}). 347 * 348 * @see #setTickLabelFont(Comparable, Font) 349 */ 350 public Font getTickLabelFont(Comparable category) { 351 Args.nullNotPermitted(category, "category"); 352 Font result = this.tickLabelFontMap.get(category); 353 // if there is no specific font, use the general one... 354 if (result == null) { 355 result = getTickLabelFont(); 356 } 357 return result; 358 } 359 360 /** 361 * Sets the font for the tick label for the specified category and sends 362 * an {@link AxisChangeEvent} to all registered listeners. 363 * 364 * @param category the category ({@code null} not permitted). 365 * @param font the font ({@code null} permitted). 366 * 367 * @see #getTickLabelFont(Comparable) 368 */ 369 public void setTickLabelFont(Comparable category, Font font) { 370 Args.nullNotPermitted(category, "category"); 371 if (font == null) { 372 this.tickLabelFontMap.remove(category); 373 } else { 374 this.tickLabelFontMap.put(category, font); 375 } 376 fireChangeEvent(); 377 } 378 379 /** 380 * Returns the paint for the tick label for the given category. 381 * 382 * @param category the category ({@code null} not permitted). 383 * 384 * @return The paint (never {@code null}). 385 * 386 * @see #setTickLabelPaint(Paint) 387 */ 388 public Paint getTickLabelPaint(Comparable category) { 389 Args.nullNotPermitted(category, "category"); 390 Paint result = this.tickLabelPaintMap.get(category); 391 // if there is no specific paint, use the general one... 392 if (result == null) { 393 result = getTickLabelPaint(); 394 } 395 return result; 396 } 397 398 /** 399 * Sets the paint for the tick label for the specified category and sends 400 * an {@link AxisChangeEvent} to all registered listeners. 401 * 402 * @param category the category ({@code null} not permitted). 403 * @param paint the paint ({@code null} permitted). 404 * 405 * @see #getTickLabelPaint(Comparable) 406 */ 407 public void setTickLabelPaint(Comparable category, Paint paint) { 408 Args.nullNotPermitted(category, "category"); 409 if (paint == null) { 410 this.tickLabelPaintMap.remove(category); 411 } else { 412 this.tickLabelPaintMap.put(category, paint); 413 } 414 fireChangeEvent(); 415 } 416 417 /** 418 * Adds a tooltip to the specified category and sends an 419 * {@link AxisChangeEvent} to all registered listeners. 420 * 421 * @param category the category ({@code null} not permitted). 422 * @param tooltip the tooltip text ({@code null} permitted). 423 * 424 * @see #removeCategoryLabelToolTip(Comparable) 425 */ 426 public void addCategoryLabelToolTip(Comparable category, String tooltip) { 427 Args.nullNotPermitted(category, "category"); 428 this.categoryLabelToolTips.put(category, tooltip); 429 fireChangeEvent(); 430 } 431 432 /** 433 * Returns the tool tip text for the label belonging to the specified 434 * category. 435 * 436 * @param category the category ({@code null} not permitted). 437 * 438 * @return The tool tip text (possibly {@code null}). 439 * 440 * @see #addCategoryLabelToolTip(Comparable, String) 441 * @see #removeCategoryLabelToolTip(Comparable) 442 */ 443 public String getCategoryLabelToolTip(Comparable category) { 444 Args.nullNotPermitted(category, "category"); 445 return this.categoryLabelToolTips.get(category); 446 } 447 448 /** 449 * Removes the tooltip for the specified category and, if there was a value 450 * associated with that category, sends an {@link AxisChangeEvent} to all 451 * registered listeners. 452 * 453 * @param category the category ({@code null} not permitted). 454 * 455 * @see #addCategoryLabelToolTip(Comparable, String) 456 * @see #clearCategoryLabelToolTips() 457 */ 458 public void removeCategoryLabelToolTip(Comparable category) { 459 Args.nullNotPermitted(category, "category"); 460 if (this.categoryLabelToolTips.remove(category) != null) { 461 fireChangeEvent(); 462 } 463 } 464 465 /** 466 * Clears the category label tooltips and sends an {@link AxisChangeEvent} 467 * to all registered listeners. 468 * 469 * @see #addCategoryLabelToolTip(Comparable, String) 470 * @see #removeCategoryLabelToolTip(Comparable) 471 */ 472 public void clearCategoryLabelToolTips() { 473 this.categoryLabelToolTips.clear(); 474 fireChangeEvent(); 475 } 476 477 /** 478 * Adds a URL (to be used in image maps) to the specified category and 479 * sends an {@link AxisChangeEvent} to all registered listeners. 480 * 481 * @param category the category ({@code null} not permitted). 482 * @param url the URL text ({@code null} permitted). 483 * 484 * @see #removeCategoryLabelURL(Comparable) 485 */ 486 public void addCategoryLabelURL(Comparable category, String url) { 487 Args.nullNotPermitted(category, "category"); 488 this.categoryLabelURLs.put(category, url); 489 fireChangeEvent(); 490 } 491 492 /** 493 * Returns the URL for the label belonging to the specified category. 494 * 495 * @param category the category ({@code null} not permitted). 496 * 497 * @return The URL text (possibly {@code null}). 498 * 499 * @see #addCategoryLabelURL(Comparable, String) 500 * @see #removeCategoryLabelURL(Comparable) 501 */ 502 public String getCategoryLabelURL(Comparable category) { 503 Args.nullNotPermitted(category, "category"); 504 return this.categoryLabelURLs.get(category); 505 } 506 507 /** 508 * Removes the URL for the specified category and, if there was a URL 509 * associated with that category, sends an {@link AxisChangeEvent} to all 510 * registered listeners. 511 * 512 * @param category the category ({@code null} not permitted). 513 * 514 * @see #addCategoryLabelURL(Comparable, String) 515 * @see #clearCategoryLabelURLs() 516 */ 517 public void removeCategoryLabelURL(Comparable category) { 518 Args.nullNotPermitted(category, "category"); 519 if (this.categoryLabelURLs.remove(category) != null) { 520 fireChangeEvent(); 521 } 522 } 523 524 /** 525 * Clears the category label URLs and sends an {@link AxisChangeEvent} 526 * to all registered listeners. 527 * 528 * @see #addCategoryLabelURL(Comparable, String) 529 * @see #removeCategoryLabelURL(Comparable) 530 */ 531 public void clearCategoryLabelURLs() { 532 this.categoryLabelURLs.clear(); 533 fireChangeEvent(); 534 } 535 536 /** 537 * Returns the Java 2D coordinate for a category. 538 * 539 * @param anchor the anchor point ({@code null} not permitted). 540 * @param category the category index. 541 * @param categoryCount the category count. 542 * @param area the data area. 543 * @param edge the location of the axis. 544 * 545 * @return The coordinate. 546 */ 547 public double getCategoryJava2DCoordinate(CategoryAnchor anchor, 548 int category, int categoryCount, Rectangle2D area, 549 RectangleEdge edge) { 550 Args.nullNotPermitted(anchor, "anchor"); 551 double result = 0.0; 552 switch (anchor) { 553 case START: 554 result = getCategoryStart(category, categoryCount, area, edge); 555 break; 556 case MIDDLE: 557 result = getCategoryMiddle(category, categoryCount, area, edge); 558 break; 559 case END: 560 result = getCategoryEnd(category, categoryCount, area, edge); 561 break; 562 default: 563 throw new IllegalStateException("Unexpected anchor value."); 564 } 565 return result; 566 567 } 568 569 /** 570 * Returns the starting coordinate for the specified category. 571 * 572 * @param category the category. 573 * @param categoryCount the number of categories. 574 * @param area the data area. 575 * @param edge the axis location. 576 * 577 * @return The coordinate. 578 * 579 * @see #getCategoryMiddle(int, int, Rectangle2D, RectangleEdge) 580 * @see #getCategoryEnd(int, int, Rectangle2D, RectangleEdge) 581 */ 582 public double getCategoryStart(int category, int categoryCount, 583 Rectangle2D area, RectangleEdge edge) { 584 585 double result = 0.0; 586 if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) { 587 result = area.getX() + area.getWidth() * getLowerMargin(); 588 } 589 else if ((edge == RectangleEdge.LEFT) 590 || (edge == RectangleEdge.RIGHT)) { 591 result = area.getMinY() + area.getHeight() * getLowerMargin(); 592 } 593 594 double categorySize = calculateCategorySize(categoryCount, area, edge); 595 double categoryGapWidth = calculateCategoryGapSize(categoryCount, area, 596 edge); 597 598 result = result + category * (categorySize + categoryGapWidth); 599 return result; 600 } 601 602 /** 603 * Returns the middle coordinate for the specified category. 604 * 605 * @param category the category. 606 * @param categoryCount the number of categories. 607 * @param area the data area. 608 * @param edge the axis location. 609 * 610 * @return The coordinate. 611 * 612 * @see #getCategoryStart(int, int, Rectangle2D, RectangleEdge) 613 * @see #getCategoryEnd(int, int, Rectangle2D, RectangleEdge) 614 */ 615 public double getCategoryMiddle(int category, int categoryCount, 616 Rectangle2D area, RectangleEdge edge) { 617 618 if (category < 0 || category >= categoryCount) { 619 throw new IllegalArgumentException("Invalid category index: " 620 + category); 621 } 622 return getCategoryStart(category, categoryCount, area, edge) 623 + calculateCategorySize(categoryCount, area, edge) / 2; 624 625 } 626 627 /** 628 * Returns the end coordinate for the specified category. 629 * 630 * @param category the category. 631 * @param categoryCount the number of categories. 632 * @param area the data area. 633 * @param edge the axis location. 634 * 635 * @return The coordinate. 636 * 637 * @see #getCategoryStart(int, int, Rectangle2D, RectangleEdge) 638 * @see #getCategoryMiddle(int, int, Rectangle2D, RectangleEdge) 639 */ 640 public double getCategoryEnd(int category, int categoryCount, 641 Rectangle2D area, RectangleEdge edge) { 642 return getCategoryStart(category, categoryCount, area, edge) 643 + calculateCategorySize(categoryCount, area, edge); 644 } 645 646 /** 647 * A convenience method that returns the axis coordinate for the centre of 648 * a category. 649 * 650 * @param category the category key ({@code null} not permitted). 651 * @param categories the categories ({@code null} not permitted). 652 * @param area the data area ({@code null} not permitted). 653 * @param edge the edge along which the axis lies ({@code null} not 654 * permitted). 655 * 656 * @return The centre coordinate. 657 * 658 * @see #getCategorySeriesMiddle(Comparable, Comparable, CategoryDataset, 659 * double, Rectangle2D, RectangleEdge) 660 */ 661 public double getCategoryMiddle(Comparable category, 662 List categories, Rectangle2D area, RectangleEdge edge) { 663 Args.nullNotPermitted(categories, "categories"); 664 int categoryIndex = categories.indexOf(category); 665 int categoryCount = categories.size(); 666 return getCategoryMiddle(categoryIndex, categoryCount, area, edge); 667 } 668 669 /** 670 * Returns the middle coordinate (in Java2D space) for a series within a 671 * category. 672 * 673 * @param category the category ({@code null} not permitted). 674 * @param seriesKey the series key ({@code null} not permitted). 675 * @param dataset the dataset ({@code null} not permitted). 676 * @param itemMargin the item margin (0.0 <= itemMargin < 1.0); 677 * @param area the area ({@code null} not permitted). 678 * @param edge the edge ({@code null} not permitted). 679 * 680 * @return The coordinate in Java2D space. 681 */ 682 public double getCategorySeriesMiddle(Comparable category, 683 Comparable seriesKey, CategoryDataset dataset, double itemMargin, 684 Rectangle2D area, RectangleEdge edge) { 685 686 int categoryIndex = dataset.getColumnIndex(category); 687 int categoryCount = dataset.getColumnCount(); 688 int seriesIndex = dataset.getRowIndex(seriesKey); 689 int seriesCount = dataset.getRowCount(); 690 double start = getCategoryStart(categoryIndex, categoryCount, area, 691 edge); 692 double end = getCategoryEnd(categoryIndex, categoryCount, area, edge); 693 double width = end - start; 694 if (seriesCount == 1) { 695 return start + width / 2.0; 696 } 697 else { 698 double gap = (width * itemMargin) / (seriesCount - 1); 699 double ww = (width * (1 - itemMargin)) / seriesCount; 700 return start + (seriesIndex * (ww + gap)) + ww / 2.0; 701 } 702 } 703 704 /** 705 * Returns the middle coordinate (in Java2D space) for a series within a 706 * category. 707 * 708 * @param categoryIndex the category index. 709 * @param categoryCount the category count. 710 * @param seriesIndex the series index. 711 * @param seriesCount the series count. 712 * @param itemMargin the item margin (0.0 <= itemMargin < 1.0); 713 * @param area the area ({@code null} not permitted). 714 * @param edge the edge ({@code null} not permitted). 715 * 716 * @return The coordinate in Java2D space. 717 */ 718 public double getCategorySeriesMiddle(int categoryIndex, int categoryCount, 719 int seriesIndex, int seriesCount, double itemMargin, 720 Rectangle2D area, RectangleEdge edge) { 721 722 double start = getCategoryStart(categoryIndex, categoryCount, area, 723 edge); 724 double end = getCategoryEnd(categoryIndex, categoryCount, area, edge); 725 double width = end - start; 726 if (seriesCount == 1) { 727 return start + width / 2.0; 728 } 729 else { 730 double gap = (width * itemMargin) / (seriesCount - 1); 731 double ww = (width * (1 - itemMargin)) / seriesCount; 732 return start + (seriesIndex * (ww + gap)) + ww / 2.0; 733 } 734 } 735 736 /** 737 * Calculates the size (width or height, depending on the location of the 738 * axis) of a category. 739 * 740 * @param categoryCount the number of categories. 741 * @param area the area within which the categories will be drawn. 742 * @param edge the axis location. 743 * 744 * @return The category size. 745 */ 746 protected double calculateCategorySize(int categoryCount, Rectangle2D area, 747 RectangleEdge edge) { 748 double result; 749 double available = 0.0; 750 751 if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) { 752 available = area.getWidth(); 753 } 754 else if ((edge == RectangleEdge.LEFT) 755 || (edge == RectangleEdge.RIGHT)) { 756 available = area.getHeight(); 757 } 758 if (categoryCount > 1) { 759 result = available * (1 - getLowerMargin() - getUpperMargin() 760 - getCategoryMargin()); 761 result = result / categoryCount; 762 } 763 else { 764 result = available * (1 - getLowerMargin() - getUpperMargin()); 765 } 766 return result; 767 } 768 769 /** 770 * Calculates the size (width or height, depending on the location of the 771 * axis) of a category gap. 772 * 773 * @param categoryCount the number of categories. 774 * @param area the area within which the categories will be drawn. 775 * @param edge the axis location. 776 * 777 * @return The category gap width. 778 */ 779 protected double calculateCategoryGapSize(int categoryCount, 780 Rectangle2D area, RectangleEdge edge) { 781 782 double result = 0.0; 783 double available = 0.0; 784 785 if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) { 786 available = area.getWidth(); 787 } 788 else if ((edge == RectangleEdge.LEFT) 789 || (edge == RectangleEdge.RIGHT)) { 790 available = area.getHeight(); 791 } 792 793 if (categoryCount > 1) { 794 result = available * getCategoryMargin() / (categoryCount - 1); 795 } 796 return result; 797 } 798 799 /** 800 * Estimates the space required for the axis, given a specific drawing area. 801 * 802 * @param g2 the graphics device (used to obtain font information). 803 * @param plot the plot that the axis belongs to. 804 * @param plotArea the area within which the axis should be drawn. 805 * @param edge the axis location ({@code null} not permitted). 806 * @param space the space already reserved. 807 * 808 * @return The space required to draw the axis. 809 */ 810 @Override 811 public AxisSpace reserveSpace(Graphics2D g2, Plot plot, 812 Rectangle2D plotArea, RectangleEdge edge, AxisSpace space) { 813 814 // create a new space object if one wasn't supplied... 815 if (space == null) { 816 space = new AxisSpace(); 817 } 818 819 // if the axis is not visible, no additional space is required... 820 if (!isVisible()) { 821 return space; 822 } 823 824 // calculate the max size of the tick labels (if visible)... 825 double tickLabelHeight = 0.0; 826 double tickLabelWidth = 0.0; 827 if (isTickLabelsVisible()) { 828 g2.setFont(getTickLabelFont()); 829 AxisState state = new AxisState(); 830 // we call refresh ticks just to get the maximum width or height 831 refreshTicks(g2, state, plotArea, edge); 832 switch (edge) { 833 case TOP: 834 tickLabelHeight = state.getMax(); 835 break; 836 case BOTTOM: 837 tickLabelHeight = state.getMax(); 838 break; 839 case LEFT: 840 tickLabelWidth = state.getMax(); 841 break; 842 case RIGHT: 843 tickLabelWidth = state.getMax(); 844 break; 845 default: 846 throw new IllegalStateException("Unexpected RectangleEdge value."); 847 } 848 } 849 850 // get the axis label size and update the space object... 851 Rectangle2D labelEnclosure = getLabelEnclosure(g2, edge); 852 double labelHeight, labelWidth; 853 if (RectangleEdge.isTopOrBottom(edge)) { 854 labelHeight = labelEnclosure.getHeight(); 855 space.add(labelHeight + tickLabelHeight 856 + this.categoryLabelPositionOffset, edge); 857 } 858 else if (RectangleEdge.isLeftOrRight(edge)) { 859 labelWidth = labelEnclosure.getWidth(); 860 space.add(labelWidth + tickLabelWidth 861 + this.categoryLabelPositionOffset, edge); 862 } 863 return space; 864 } 865 866 /** 867 * Configures the axis against the current plot. 868 */ 869 @Override 870 public void configure() { 871 // nothing required 872 } 873 874 /** 875 * Draws the axis on a Java 2D graphics device (such as the screen or a 876 * printer). 877 * 878 * @param g2 the graphics device ({@code null} not permitted). 879 * @param cursor the cursor location. 880 * @param plotArea the area within which the axis should be drawn 881 * ({@code null} not permitted). 882 * @param dataArea the area within which the plot is being drawn 883 * ({@code null} not permitted). 884 * @param edge the location of the axis ({@code null} not permitted). 885 * @param plotState collects information about the plot 886 * ({@code null} permitted). 887 * 888 * @return The axis state (never {@code null}). 889 */ 890 @Override 891 public AxisState draw(Graphics2D g2, double cursor, Rectangle2D plotArea, 892 Rectangle2D dataArea, RectangleEdge edge, 893 PlotRenderingInfo plotState) { 894 895 // if the axis is not visible, don't draw it... 896 if (!isVisible()) { 897 return new AxisState(cursor); 898 } 899 900 if (isAxisLineVisible()) { 901 drawAxisLine(g2, cursor, dataArea, edge); 902 } 903 AxisState state = new AxisState(cursor); 904 if (isTickMarksVisible()) { 905 drawTickMarks(g2, cursor, dataArea, edge, state); 906 } 907 908 createAndAddEntity(cursor, state, dataArea, edge, plotState); 909 910 // draw the category labels and axis label 911 state = drawCategoryLabels(g2, plotArea, dataArea, edge, state, 912 plotState); 913 if (getAttributedLabel() != null) { 914 state = drawAttributedLabel(getAttributedLabel(), g2, plotArea, 915 dataArea, edge, state); 916 917 } else { 918 state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state); 919 } 920 return state; 921 922 } 923 924 /** 925 * Draws the category labels and returns the updated axis state. 926 * 927 * @param g2 the graphics device ({@code null} not permitted). 928 * @param plotArea the plot area ({@code null} not permitted). 929 * @param dataArea the area inside the axes ({@code null} not 930 * permitted). 931 * @param edge the axis location ({@code null} not permitted). 932 * @param state the axis state ({@code null} not permitted). 933 * @param plotState collects information about the plot ({@code null} 934 * permitted). 935 * 936 * @return The updated axis state (never {@code null}). 937 */ 938 protected AxisState drawCategoryLabels(Graphics2D g2, Rectangle2D plotArea, 939 Rectangle2D dataArea, RectangleEdge edge, AxisState state, 940 PlotRenderingInfo plotState) { 941 942 Args.nullNotPermitted(state, "state"); 943 if (!isTickLabelsVisible()) { 944 return state; 945 } 946 947 List ticks = refreshTicks(g2, state, plotArea, edge); 948 state.setTicks(ticks); 949 int categoryIndex = 0; 950 for (Object o : ticks) { 951 CategoryTick tick = (CategoryTick) o; 952 g2.setFont(getTickLabelFont(tick.getCategory())); 953 g2.setPaint(getTickLabelPaint(tick.getCategory())); 954 955 CategoryLabelPosition position 956 = this.categoryLabelPositions.getLabelPosition(edge); 957 double x0 = 0.0; 958 double x1 = 0.0; 959 double y0 = 0.0; 960 double y1 = 0.0; 961 if (edge == RectangleEdge.TOP) { 962 x0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, 963 edge); 964 x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, 965 edge); 966 y1 = state.getCursor() - this.categoryLabelPositionOffset; 967 y0 = y1 - state.getMax(); 968 } 969 else if (edge == RectangleEdge.BOTTOM) { 970 x0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, 971 edge); 972 x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, 973 edge); 974 y0 = state.getCursor() + this.categoryLabelPositionOffset; 975 y1 = y0 + state.getMax(); 976 } 977 else if (edge == RectangleEdge.LEFT) { 978 y0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, 979 edge); 980 y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, 981 edge); 982 x1 = state.getCursor() - this.categoryLabelPositionOffset; 983 x0 = x1 - state.getMax(); 984 } 985 else if (edge == RectangleEdge.RIGHT) { 986 y0 = getCategoryStart(categoryIndex, ticks.size(), dataArea, 987 edge); 988 y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, 989 edge); 990 x0 = state.getCursor() + this.categoryLabelPositionOffset; 991 x1 = x0 - state.getMax(); 992 } 993 Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0), 994 (y1 - y0)); 995 Point2D anchorPoint = position.getCategoryAnchor().getAnchorPoint(area); 996 TextBlock block = tick.getLabel(); 997 block.draw(g2, (float) anchorPoint.getX(), 998 (float) anchorPoint.getY(), position.getLabelAnchor(), 999 (float) anchorPoint.getX(), (float) anchorPoint.getY(), 1000 position.getAngle()); 1001 Shape bounds = block.calculateBounds(g2, 1002 (float) anchorPoint.getX(), (float) anchorPoint.getY(), 1003 position.getLabelAnchor(), (float) anchorPoint.getX(), 1004 (float) anchorPoint.getY(), position.getAngle()); 1005 if (plotState != null && plotState.getOwner() != null) { 1006 EntityCollection entities = plotState.getOwner() 1007 .getEntityCollection(); 1008 if (entities != null) { 1009 String tooltip = getCategoryLabelToolTip( 1010 tick.getCategory()); 1011 String url = getCategoryLabelURL(tick.getCategory()); 1012 entities.add(new CategoryLabelEntity(tick.getCategory(), 1013 bounds, tooltip, url)); 1014 } 1015 } 1016 categoryIndex++; 1017 } 1018 1019 if (edge.equals(RectangleEdge.TOP)) { 1020 double h = state.getMax() + this.categoryLabelPositionOffset; 1021 state.cursorUp(h); 1022 } 1023 else if (edge.equals(RectangleEdge.BOTTOM)) { 1024 double h = state.getMax() + this.categoryLabelPositionOffset; 1025 state.cursorDown(h); 1026 } 1027 else if (edge == RectangleEdge.LEFT) { 1028 double w = state.getMax() + this.categoryLabelPositionOffset; 1029 state.cursorLeft(w); 1030 } 1031 else if (edge == RectangleEdge.RIGHT) { 1032 double w = state.getMax() + this.categoryLabelPositionOffset; 1033 state.cursorRight(w); 1034 } 1035 return state; 1036 } 1037 1038 /** 1039 * Creates a temporary list of ticks that can be used when drawing the axis. 1040 * 1041 * @param g2 the graphics device (used to get font measurements). 1042 * @param state the axis state. 1043 * @param dataArea the area inside the axes. 1044 * @param edge the location of the axis. 1045 * 1046 * @return A list of ticks. 1047 */ 1048 @Override 1049 public List refreshTicks(Graphics2D g2, AxisState state, 1050 Rectangle2D dataArea, RectangleEdge edge) { 1051 1052 List ticks = new java.util.ArrayList(); // FIXME generics 1053 1054 // sanity check for data area... 1055 if (dataArea.getHeight() <= 0.0 || dataArea.getWidth() < 0.0) { 1056 return ticks; 1057 } 1058 1059 CategoryPlot plot = (CategoryPlot) getPlot(); 1060 List categories = plot.getCategoriesForAxis(this); 1061 double max = 0.0; 1062 1063 if (categories != null) { 1064 CategoryLabelPosition position 1065 = this.categoryLabelPositions.getLabelPosition(edge); 1066 float r = this.maximumCategoryLabelWidthRatio; 1067 if (r <= 0.0) { 1068 r = position.getWidthRatio(); 1069 } 1070 1071 float l; 1072 if (position.getWidthType() == CategoryLabelWidthType.CATEGORY) { 1073 l = (float) calculateCategorySize(categories.size(), dataArea, 1074 edge); 1075 } 1076 else { 1077 if (RectangleEdge.isLeftOrRight(edge)) { 1078 l = (float) dataArea.getWidth(); 1079 } 1080 else { 1081 l = (float) dataArea.getHeight(); 1082 } 1083 } 1084 int categoryIndex = 0; 1085 for (Object o : categories) { 1086 Comparable category = (Comparable) o; 1087 g2.setFont(getTickLabelFont(category)); 1088 TextBlock label = createLabel(category, l * r, edge, g2); 1089 if (edge == RectangleEdge.TOP || edge == RectangleEdge.BOTTOM) { 1090 max = Math.max(max, calculateCategoryLabelHeight(label, 1091 position, getTickLabelInsets(), g2)); 1092 } else if (edge == RectangleEdge.LEFT 1093 || edge == RectangleEdge.RIGHT) { 1094 max = Math.max(max, calculateCategoryLabelWidth(label, 1095 position, getTickLabelInsets(), g2)); 1096 } 1097 Tick tick = new CategoryTick(category, label, 1098 position.getLabelAnchor(), 1099 position.getRotationAnchor(), position.getAngle()); 1100 ticks.add(tick); 1101 categoryIndex = categoryIndex + 1; 1102 } 1103 } 1104 state.setMax(max); 1105 return ticks; 1106 1107 } 1108 1109 /** 1110 * Draws the tick marks. 1111 * 1112 * @param g2 the graphics target. 1113 * @param cursor the cursor position (an offset when drawing multiple axes) 1114 * @param dataArea the area for plotting the data. 1115 * @param edge the location of the axis. 1116 * @param state the axis state. 1117 */ 1118 public void drawTickMarks(Graphics2D g2, double cursor, 1119 Rectangle2D dataArea, RectangleEdge edge, AxisState state) { 1120 1121 Plot p = getPlot(); 1122 if (p == null) { 1123 return; 1124 } 1125 CategoryPlot plot = (CategoryPlot) p; 1126 double il = getTickMarkInsideLength(); 1127 double ol = getTickMarkOutsideLength(); 1128 Line2D line = new Line2D.Double(); 1129 List<Comparable> categories = plot.getCategoriesForAxis(this); 1130 g2.setPaint(getTickMarkPaint()); 1131 g2.setStroke(getTickMarkStroke()); 1132 Object saved = g2.getRenderingHint(RenderingHints.KEY_STROKE_CONTROL); 1133 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, 1134 RenderingHints.VALUE_STROKE_NORMALIZE); 1135 if (edge.equals(RectangleEdge.TOP)) { 1136 for (Comparable category : categories) { 1137 double x = getCategoryMiddle(category, categories, dataArea, edge); 1138 line.setLine(x, cursor, x, cursor + il); 1139 g2.draw(line); 1140 line.setLine(x, cursor, x, cursor - ol); 1141 g2.draw(line); 1142 } 1143 state.cursorUp(ol); 1144 } else if (edge.equals(RectangleEdge.BOTTOM)) { 1145 for (Comparable category : categories) { 1146 double x = getCategoryMiddle(category, categories, dataArea, edge); 1147 line.setLine(x, cursor, x, cursor - il); 1148 g2.draw(line); 1149 line.setLine(x, cursor, x, cursor + ol); 1150 g2.draw(line); 1151 } 1152 state.cursorDown(ol); 1153 } else if (edge.equals(RectangleEdge.LEFT)) { 1154 for (Comparable category : categories) { 1155 double y = getCategoryMiddle(category, categories, dataArea, edge); 1156 line.setLine(cursor, y, cursor + il, y); 1157 g2.draw(line); 1158 line.setLine(cursor, y, cursor - ol, y); 1159 g2.draw(line); 1160 } 1161 state.cursorLeft(ol); 1162 } else if (edge.equals(RectangleEdge.RIGHT)) { 1163 for (Comparable category : categories) { 1164 double y = getCategoryMiddle(category, categories, dataArea, edge); 1165 line.setLine(cursor, y, cursor - il, y); 1166 g2.draw(line); 1167 line.setLine(cursor, y, cursor + ol, y); 1168 g2.draw(line); 1169 } 1170 state.cursorRight(ol); 1171 } 1172 g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, saved); 1173 } 1174 1175 /** 1176 * Creates a label. 1177 * 1178 * @param category the category. 1179 * @param width the available width. 1180 * @param edge the edge on which the axis appears. 1181 * @param g2 the graphics device. 1182 * 1183 * @return A label. 1184 */ 1185 protected TextBlock createLabel(Comparable category, float width, 1186 RectangleEdge edge, Graphics2D g2) { 1187 TextBlock label = TextUtils.createTextBlock(category.toString(), 1188 getTickLabelFont(category), getTickLabelPaint(category), width, 1189 this.maximumCategoryLabelLines, new G2TextMeasurer(g2)); 1190 return label; 1191 } 1192 1193 /** 1194 * Calculates the width of a category label when rendered. 1195 * 1196 * @param label the text block ({@code null} not permitted). 1197 * @param position the position. 1198 * @param insets the label insets. 1199 * @param g2 the graphics device. 1200 * 1201 * @return The width. 1202 */ 1203 protected double calculateCategoryLabelWidth(TextBlock label, 1204 CategoryLabelPosition position, RectangleInsets insets, Graphics2D g2) { 1205 Size2D size = label.calculateDimensions(g2); 1206 Rectangle2D box = new Rectangle2D.Double(0.0, 0.0, size.getWidth(), 1207 size.getHeight()); 1208 Shape rotatedBox = ShapeUtils.rotateShape(box, position.getAngle(), 1209 0.0f, 0.0f); 1210 double w = rotatedBox.getBounds2D().getWidth() + insets.getLeft() 1211 + insets.getRight(); 1212 return w; 1213 } 1214 1215 /** 1216 * Calculates the height of a category label when rendered. 1217 * 1218 * @param block the text block ({@code null} not permitted). 1219 * @param position the label position ({@code null} not permitted). 1220 * @param insets the label insets ({@code null} not permitted). 1221 * @param g2 the graphics device ({@code null} not permitted). 1222 * 1223 * @return The height. 1224 */ 1225 protected double calculateCategoryLabelHeight(TextBlock block, 1226 CategoryLabelPosition position, RectangleInsets insets, Graphics2D g2) { 1227 Size2D size = block.calculateDimensions(g2); 1228 Rectangle2D box = new Rectangle2D.Double(0.0, 0.0, size.getWidth(), 1229 size.getHeight()); 1230 Shape rotatedBox = ShapeUtils.rotateShape(box, position.getAngle(), 1231 0.0f, 0.0f); 1232 double h = rotatedBox.getBounds2D().getHeight() 1233 + insets.getTop() + insets.getBottom(); 1234 return h; 1235 } 1236 1237 /** 1238 * Creates a clone of the axis. 1239 * 1240 * @return A clone. 1241 * 1242 * @throws CloneNotSupportedException if some component of the axis does 1243 * not support cloning. 1244 */ 1245 @Override 1246 public Object clone() throws CloneNotSupportedException { 1247 CategoryAxis clone = (CategoryAxis) super.clone(); 1248 clone.tickLabelFontMap = new HashMap<>(this.tickLabelFontMap); 1249 clone.tickLabelPaintMap = new HashMap<>(this.tickLabelPaintMap); 1250 clone.categoryLabelToolTips = new HashMap<>(this.categoryLabelToolTips); 1251 clone.categoryLabelURLs = new HashMap<>(this.categoryLabelToolTips); 1252 return clone; 1253 } 1254 1255 /** 1256 * Tests this axis for equality with an arbitrary object. 1257 * 1258 * @param obj the object ({@code null} permitted). 1259 * 1260 * @return A boolean. 1261 */ 1262 @Override 1263 public boolean equals(Object obj) { 1264 if (obj == this) { 1265 return true; 1266 } 1267 if (!(obj instanceof CategoryAxis)) { 1268 return false; 1269 } 1270 if (!super.equals(obj)) { 1271 return false; 1272 } 1273 CategoryAxis that = (CategoryAxis) obj; 1274 if (that.lowerMargin != this.lowerMargin) { 1275 return false; 1276 } 1277 if (that.upperMargin != this.upperMargin) { 1278 return false; 1279 } 1280 if (that.categoryMargin != this.categoryMargin) { 1281 return false; 1282 } 1283 if (that.maximumCategoryLabelWidthRatio 1284 != this.maximumCategoryLabelWidthRatio) { 1285 return false; 1286 } 1287 if (that.categoryLabelPositionOffset 1288 != this.categoryLabelPositionOffset) { 1289 return false; 1290 } 1291 if (!Objects.equals(that.categoryLabelPositions, this.categoryLabelPositions)) { 1292 return false; 1293 } 1294 if (!Objects.equals(that.categoryLabelToolTips, this.categoryLabelToolTips)) { 1295 return false; 1296 } 1297 if (!Objects.equals(this.categoryLabelURLs, that.categoryLabelURLs)) { 1298 return false; 1299 } 1300 if (!Objects.equals(this.tickLabelFontMap, that.tickLabelFontMap)) { 1301 return false; 1302 } 1303 if (!PaintUtils.equal(this.tickLabelPaintMap, that.tickLabelPaintMap)) { 1304 return false; 1305 } 1306 return true; 1307 } 1308 1309 /** 1310 * Returns a hash code for this object. 1311 * 1312 * @return A hash code. 1313 */ 1314 @Override 1315 public int hashCode() { 1316 return super.hashCode(); 1317 } 1318 1319 /** 1320 * Provides serialization support. 1321 * 1322 * @param stream the output stream. 1323 * 1324 * @throws IOException if there is an I/O error. 1325 */ 1326 private void writeObject(ObjectOutputStream stream) throws IOException { 1327 stream.defaultWriteObject(); 1328 writePaintMap(this.tickLabelPaintMap, stream); 1329 } 1330 1331 /** 1332 * Provides serialization support. 1333 * 1334 * @param stream the input stream. 1335 * 1336 * @throws IOException if there is an I/O error. 1337 * @throws ClassNotFoundException if there is a classpath problem. 1338 */ 1339 private void readObject(ObjectInputStream stream) 1340 throws IOException, ClassNotFoundException { 1341 stream.defaultReadObject(); 1342 this.tickLabelPaintMap = readPaintMap(stream); 1343 } 1344 1345 /** 1346 * Reads a {@code Map} of ({@code Comparable}, {@code Paint}) 1347 * elements from a stream. 1348 * 1349 * @param in the input stream. 1350 * 1351 * @return The map. 1352 * 1353 * @throws IOException 1354 * @throws ClassNotFoundException 1355 * 1356 * @see #writePaintMap(Map, ObjectOutputStream) 1357 */ 1358 private Map readPaintMap(ObjectInputStream in) 1359 throws IOException, ClassNotFoundException { 1360 boolean isNull = in.readBoolean(); 1361 if (isNull) { 1362 return null; 1363 } 1364 Map result = new HashMap(); 1365 int count = in.readInt(); 1366 for (int i = 0; i < count; i++) { 1367 Comparable category = (Comparable) in.readObject(); 1368 Paint paint = SerialUtils.readPaint(in); 1369 result.put(category, paint); 1370 } 1371 return result; 1372 } 1373 1374 /** 1375 * Writes a map of ({@code Comparable}, {@code Paint}) 1376 * elements to a stream. 1377 * 1378 * @param map the map ({@code null} permitted). 1379 * 1380 * @param out 1381 * @throws IOException 1382 * 1383 * @see #readPaintMap(ObjectInputStream) 1384 */ 1385 private void writePaintMap(Map map, ObjectOutputStream out) 1386 throws IOException { 1387 if (map == null) { 1388 out.writeBoolean(true); 1389 } 1390 else { 1391 out.writeBoolean(false); 1392 Set keys = map.keySet(); 1393 int count = keys.size(); 1394 out.writeInt(count); 1395 for (Object o : keys) { 1396 Comparable key = (Comparable) o; 1397 out.writeObject(key); 1398 SerialUtils.writePaint((Paint) map.get(key), out); 1399 } 1400 } 1401 } 1402 1403}