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