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