001 /* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2006, 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 * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 025 * in the United States and other countries.] 026 * 027 * ----------------- 028 * CategoryAxis.java 029 * ----------------- 030 * (C) Copyright 2000-2006, by Object Refinery Limited and Contributors. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): Pady Srinivasan (patch 1217634); 034 * 035 * $Id: CategoryAxis.java,v 1.18.2.6 2006/01/11 16:17:29 mungady Exp $ 036 * 037 * Changes (from 21-Aug-2001) 038 * -------------------------- 039 * 21-Aug-2001 : Added standard header. Fixed DOS encoding problem (DG); 040 * 18-Sep-2001 : Updated header (DG); 041 * 04-Dec-2001 : Changed constructors to protected, and tidied up default 042 * values (DG); 043 * 19-Apr-2002 : Updated import statements (DG); 044 * 05-Sep-2002 : Updated constructor for changes in Axis class (DG); 045 * 06-Nov-2002 : Moved margins from the CategoryPlot class (DG); 046 * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG); 047 * 22-Jan-2002 : Removed monolithic constructor (DG); 048 * 26-Mar-2003 : Implemented Serializable (DG); 049 * 09-May-2003 : Merged HorizontalCategoryAxis and VerticalCategoryAxis into 050 * this class (DG); 051 * 13-Aug-2003 : Implemented Cloneable (DG); 052 * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG); 053 * 05-Nov-2003 : Fixed serialization bug (DG); 054 * 26-Nov-2003 : Added category label offset (DG); 055 * 06-Jan-2004 : Moved axis line attributes to Axis class, rationalised 056 * category label position attributes (DG); 057 * 07-Jan-2004 : Added new implementation for linewrapping of category 058 * labels (DG); 059 * 17-Feb-2004 : Moved deprecated code to bottom of source file (DG); 060 * 10-Mar-2004 : Changed Dimension --> Dimension2D in text classes (DG); 061 * 16-Mar-2004 : Added support for tooltips on category labels (DG); 062 * 01-Apr-2004 : Changed java.awt.geom.Dimension2D to org.jfree.ui.Size2D 063 * because of JDK bug 4976448 which persists on JDK 1.3.1 (DG); 064 * 03-Sep-2004 : Added 'maxCategoryLabelLines' attribute (DG); 065 * 04-Oct-2004 : Renamed ShapeUtils --> ShapeUtilities (DG); 066 * 11-Jan-2005 : Removed deprecated methods in preparation for 1.0.0 067 * release (DG); 068 * 21-Jan-2005 : Modified return type for RectangleAnchor.coordinates() 069 * method (DG); 070 * 21-Apr-2005 : Replaced Insets with RectangleInsets (DG); 071 * 26-Apr-2005 : Removed LOGGER (DG); 072 * 08-Jun-2005 : Fixed bug in axis layout (DG); 073 * 22-Nov-2005 : Added a method to access the tool tip text for a category 074 * label (DG); 075 * 23-Nov-2005 : Added per-category font and paint options - see patch 076 * 1217634 (DG); 077 * ------------- JFreeChart 1.0.0 --------------------------------------------- 078 * 11-Jan-2006 : Fixed null pointer exception in drawCategoryLabels - see bug 079 * 1403043 (DG); 080 * 081 */ 082 083 package org.jfree.chart.axis; 084 085 import java.awt.Font; 086 import java.awt.Graphics2D; 087 import java.awt.Paint; 088 import java.awt.Shape; 089 import java.awt.geom.Point2D; 090 import java.awt.geom.Rectangle2D; 091 import java.io.IOException; 092 import java.io.ObjectInputStream; 093 import java.io.ObjectOutputStream; 094 import java.io.Serializable; 095 import java.util.HashMap; 096 import java.util.Iterator; 097 import java.util.List; 098 import java.util.Map; 099 import java.util.Set; 100 101 import org.jfree.chart.entity.EntityCollection; 102 import org.jfree.chart.entity.TickLabelEntity; 103 import org.jfree.chart.event.AxisChangeEvent; 104 import org.jfree.chart.plot.CategoryPlot; 105 import org.jfree.chart.plot.Plot; 106 import org.jfree.chart.plot.PlotRenderingInfo; 107 import org.jfree.io.SerialUtilities; 108 import org.jfree.text.G2TextMeasurer; 109 import org.jfree.text.TextBlock; 110 import org.jfree.text.TextUtilities; 111 import org.jfree.ui.RectangleAnchor; 112 import org.jfree.ui.RectangleEdge; 113 import org.jfree.ui.RectangleInsets; 114 import org.jfree.ui.Size2D; 115 import org.jfree.util.ObjectUtilities; 116 import org.jfree.util.PaintUtilities; 117 import org.jfree.util.ShapeUtilities; 118 119 /** 120 * An axis that displays categories. 121 */ 122 public class CategoryAxis extends Axis implements Cloneable, Serializable { 123 124 /** For serialization. */ 125 private static final long serialVersionUID = 5886554608114265863L; 126 127 /** 128 * The default margin for the axis (used for both lower and upper margins). 129 */ 130 public static final double DEFAULT_AXIS_MARGIN = 0.05; 131 132 /** 133 * The default margin between categories (a percentage of the overall axis 134 * length). 135 */ 136 public static final double DEFAULT_CATEGORY_MARGIN = 0.20; 137 138 /** The amount of space reserved at the start of the axis. */ 139 private double lowerMargin; 140 141 /** The amount of space reserved at the end of the axis. */ 142 private double upperMargin; 143 144 /** The amount of space reserved between categories. */ 145 private double categoryMargin; 146 147 /** The maximum number of lines for category labels. */ 148 private int maximumCategoryLabelLines; 149 150 /** 151 * A ratio that is multiplied by the width of one category to determine the 152 * maximum label width. 153 */ 154 private float maximumCategoryLabelWidthRatio; 155 156 /** The category label offset. */ 157 private int categoryLabelPositionOffset; 158 159 /** 160 * A structure defining the category label positions for each axis 161 * location. 162 */ 163 private CategoryLabelPositions categoryLabelPositions; 164 165 /** Storage for tick label font overrides (if any). */ 166 private Map tickLabelFontMap; 167 168 /** Storage for tick label paint overrides (if any). */ 169 private transient Map tickLabelPaintMap; 170 171 /** Storage for the category label tooltips (if any). */ 172 private Map categoryLabelToolTips; 173 174 /** 175 * Creates a new category axis with no label. 176 */ 177 public CategoryAxis() { 178 this(null); 179 } 180 181 /** 182 * Constructs a category axis, using default values where necessary. 183 * 184 * @param label the axis label (<code>null</code> permitted). 185 */ 186 public CategoryAxis(String label) { 187 188 super(label); 189 190 this.lowerMargin = DEFAULT_AXIS_MARGIN; 191 this.upperMargin = DEFAULT_AXIS_MARGIN; 192 this.categoryMargin = DEFAULT_CATEGORY_MARGIN; 193 this.maximumCategoryLabelLines = 1; 194 this.maximumCategoryLabelWidthRatio = 0.0f; 195 196 setTickMarksVisible(false); // not supported by this axis type yet 197 198 this.categoryLabelPositionOffset = 4; 199 this.categoryLabelPositions = CategoryLabelPositions.STANDARD; 200 this.tickLabelFontMap = new HashMap(); 201 this.tickLabelPaintMap = new HashMap(); 202 this.categoryLabelToolTips = new HashMap(); 203 204 } 205 206 /** 207 * Returns the lower margin for the axis. 208 * 209 * @return The margin. 210 */ 211 public double getLowerMargin() { 212 return this.lowerMargin; 213 } 214 215 /** 216 * Sets the lower margin for the axis and sends an {@link AxisChangeEvent} 217 * to all registered listeners. 218 * 219 * @param margin the margin as a percentage of the axis length (for 220 * example, 0.05 is five percent). 221 */ 222 public void setLowerMargin(double margin) { 223 this.lowerMargin = margin; 224 notifyListeners(new AxisChangeEvent(this)); 225 } 226 227 /** 228 * Returns the upper margin for the axis. 229 * 230 * @return The margin. 231 */ 232 public double getUpperMargin() { 233 return this.upperMargin; 234 } 235 236 /** 237 * Sets the upper margin for the axis and sends an {@link AxisChangeEvent} 238 * to all registered listeners. 239 * 240 * @param margin the margin as a percentage of the axis length (for 241 * example, 0.05 is five percent). 242 */ 243 public void setUpperMargin(double margin) { 244 this.upperMargin = margin; 245 notifyListeners(new AxisChangeEvent(this)); 246 } 247 248 /** 249 * Returns the category margin. 250 * 251 * @return The margin. 252 */ 253 public double getCategoryMargin() { 254 return this.categoryMargin; 255 } 256 257 /** 258 * Sets the category margin and sends an {@link AxisChangeEvent} to all 259 * registered listeners. The overall category margin is distributed over 260 * N-1 gaps, where N is the number of categories on the axis. 261 * 262 * @param margin the margin as a percentage of the axis length (for 263 * example, 0.05 is five percent). 264 */ 265 public void setCategoryMargin(double margin) { 266 this.categoryMargin = margin; 267 notifyListeners(new AxisChangeEvent(this)); 268 } 269 270 /** 271 * Returns the maximum number of lines to use for each category label. 272 * 273 * @return The maximum number of lines. 274 */ 275 public int getMaximumCategoryLabelLines() { 276 return this.maximumCategoryLabelLines; 277 } 278 279 /** 280 * Sets the maximum number of lines to use for each category label and 281 * sends an {@link AxisChangeEvent} to all registered listeners. 282 * 283 * @param lines the maximum number of lines. 284 */ 285 public void setMaximumCategoryLabelLines(int lines) { 286 this.maximumCategoryLabelLines = lines; 287 notifyListeners(new AxisChangeEvent(this)); 288 } 289 290 /** 291 * Returns the category label width ratio. 292 * 293 * @return The ratio. 294 */ 295 public float getMaximumCategoryLabelWidthRatio() { 296 return this.maximumCategoryLabelWidthRatio; 297 } 298 299 /** 300 * Sets the maximum category label width ratio and sends an 301 * {@link AxisChangeEvent} to all registered listeners. 302 * 303 * @param ratio the ratio. 304 */ 305 public void setMaximumCategoryLabelWidthRatio(float ratio) { 306 this.maximumCategoryLabelWidthRatio = ratio; 307 notifyListeners(new AxisChangeEvent(this)); 308 } 309 310 /** 311 * Returns the offset between the axis and the category labels (before 312 * label positioning is taken into account). 313 * 314 * @return The offset (in Java2D units). 315 */ 316 public int getCategoryLabelPositionOffset() { 317 return this.categoryLabelPositionOffset; 318 } 319 320 /** 321 * Sets the offset between the axis and the category labels (before label 322 * positioning is taken into account). 323 * 324 * @param offset the offset (in Java2D units). 325 */ 326 public void setCategoryLabelPositionOffset(int offset) { 327 this.categoryLabelPositionOffset = offset; 328 notifyListeners(new AxisChangeEvent(this)); 329 } 330 331 /** 332 * Returns the category label position specification (this contains label 333 * positioning info for all four possible axis locations). 334 * 335 * @return The positions (never <code>null</code>). 336 */ 337 public CategoryLabelPositions getCategoryLabelPositions() { 338 return this.categoryLabelPositions; 339 } 340 341 /** 342 * Sets the category label position specification for the axis and sends an 343 * {@link AxisChangeEvent} to all registered listeners. 344 * 345 * @param positions the positions (<code>null</code> not permitted). 346 */ 347 public void setCategoryLabelPositions(CategoryLabelPositions positions) { 348 if (positions == null) { 349 throw new IllegalArgumentException("Null 'positions' argument."); 350 } 351 this.categoryLabelPositions = positions; 352 notifyListeners(new AxisChangeEvent(this)); 353 } 354 355 /** 356 * Returns the font for the tick label for the given category. 357 * 358 * @param category the category (<code>null</code> not permitted). 359 * 360 * @return The font (never <code>null</code>). 361 */ 362 public Font getTickLabelFont(Comparable category) { 363 if (category == null) { 364 throw new IllegalArgumentException("Null 'category' argument."); 365 } 366 Font result = (Font) this.tickLabelFontMap.get(category); 367 // if there is no specific font, use the general one... 368 if (result == null) { 369 result = getTickLabelFont(); 370 } 371 return result; 372 } 373 374 /** 375 * Sets the font for the tick label for the specified category and sends 376 * an {@link AxisChangeEvent} to all registered listeners. 377 * 378 * @param category the category (<code>null</code> not permitted). 379 * @param font the font (<code>null</code> permitted). 380 */ 381 public void setTickLabelFont(Comparable category, Font font) { 382 if (category == null) { 383 throw new IllegalArgumentException("Null 'category' argument."); 384 } 385 if (font == null) { 386 this.tickLabelFontMap.remove(category); 387 } 388 else { 389 this.tickLabelFontMap.put(category, font); 390 } 391 notifyListeners(new AxisChangeEvent(this)); 392 } 393 394 /** 395 * Returns the paint for the tick label for the given category. 396 * 397 * @param category the category (<code>null</code> not permitted). 398 * 399 * @return The paint (never <code>null</code>). 400 */ 401 public Paint getTickLabelPaint(Comparable category) { 402 if (category == null) { 403 throw new IllegalArgumentException("Null 'category' argument."); 404 } 405 Paint result = (Paint) this.tickLabelPaintMap.get(category); 406 // if there is no specific paint, use the general one... 407 if (result == null) { 408 result = getTickLabelPaint(); 409 } 410 return result; 411 } 412 413 /** 414 * Sets the paint for the tick label for the specified category and sends 415 * an {@link AxisChangeEvent} to all registered listeners. 416 * 417 * @param category the category (<code>null</code> not permitted). 418 * @param paint the paint (<code>null</code> permitted). 419 */ 420 public void setTickLabelPaint(Comparable category, Paint paint) { 421 if (category == null) { 422 throw new IllegalArgumentException("Null 'category' argument."); 423 } 424 if (paint == null) { 425 this.tickLabelPaintMap.remove(category); 426 } 427 else { 428 this.tickLabelPaintMap.put(category, paint); 429 } 430 notifyListeners(new AxisChangeEvent(this)); 431 } 432 433 /** 434 * Adds a tooltip to the specified category and sends an 435 * {@link AxisChangeEvent} to all registered listeners. 436 * 437 * @param category the category (<code>null<code> not permitted). 438 * @param tooltip the tooltip text (<code>null</code> permitted). 439 */ 440 public void addCategoryLabelToolTip(Comparable category, String tooltip) { 441 if (category == null) { 442 throw new IllegalArgumentException("Null 'category' argument."); 443 } 444 this.categoryLabelToolTips.put(category, tooltip); 445 notifyListeners(new AxisChangeEvent(this)); 446 } 447 448 /** 449 * Returns the tool tip text for the label belonging to the specified 450 * category. 451 * 452 * @param category the category (<code>null</code> not permitted). 453 * 454 * @return The tool tip text (possibly <code>null</code>). 455 */ 456 public String getCategoryLabelToolTip(Comparable category) { 457 if (category == null) { 458 throw new IllegalArgumentException("Null 'category' argument."); 459 } 460 return (String) this.categoryLabelToolTips.get(category); 461 } 462 463 /** 464 * Removes the tooltip for the specified category and sends an 465 * {@link AxisChangeEvent} to all registered listeners. 466 * 467 * @param category the category (<code>null<code> not permitted). 468 */ 469 public void removeCategoryLabelToolTip(Comparable category) { 470 if (category == null) { 471 throw new IllegalArgumentException("Null 'category' argument."); 472 } 473 this.categoryLabelToolTips.remove(category); 474 notifyListeners(new AxisChangeEvent(this)); 475 } 476 477 /** 478 * Clears the category label tooltips and sends an {@link AxisChangeEvent} 479 * to all registered listeners. 480 */ 481 public void clearCategoryLabelToolTips() { 482 this.categoryLabelToolTips.clear(); 483 notifyListeners(new AxisChangeEvent(this)); 484 } 485 486 /** 487 * Returns the Java 2D coordinate for a category. 488 * 489 * @param anchor the anchor point. 490 * @param category the category index. 491 * @param categoryCount the category count. 492 * @param area the data area. 493 * @param edge the location of the axis. 494 * 495 * @return The coordinate. 496 */ 497 public double getCategoryJava2DCoordinate(CategoryAnchor anchor, 498 int category, 499 int categoryCount, 500 Rectangle2D area, 501 RectangleEdge edge) { 502 503 double result = 0.0; 504 if (anchor == CategoryAnchor.START) { 505 result = getCategoryStart(category, categoryCount, area, edge); 506 } 507 else if (anchor == CategoryAnchor.MIDDLE) { 508 result = getCategoryMiddle(category, categoryCount, area, edge); 509 } 510 else if (anchor == CategoryAnchor.END) { 511 result = getCategoryEnd(category, categoryCount, area, edge); 512 } 513 return result; 514 515 } 516 517 /** 518 * Returns the starting coordinate for the specified category. 519 * 520 * @param category the category. 521 * @param categoryCount the number of categories. 522 * @param area the data area. 523 * @param edge the axis location. 524 * 525 * @return The coordinate. 526 */ 527 public double getCategoryStart(int category, int categoryCount, 528 Rectangle2D area, 529 RectangleEdge edge) { 530 531 double result = 0.0; 532 if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) { 533 result = area.getX() + area.getWidth() * getLowerMargin(); 534 } 535 else if ((edge == RectangleEdge.LEFT) 536 || (edge == RectangleEdge.RIGHT)) { 537 result = area.getMinY() + area.getHeight() * getLowerMargin(); 538 } 539 540 double categorySize = calculateCategorySize(categoryCount, area, edge); 541 double categoryGapWidth = calculateCategoryGapSize( 542 categoryCount, area, edge 543 ); 544 545 result = result + category * (categorySize + categoryGapWidth); 546 547 return result; 548 } 549 550 /** 551 * Returns the middle coordinate for the specified category. 552 * 553 * @param category the category. 554 * @param categoryCount the number of categories. 555 * @param area the data area. 556 * @param edge the axis location. 557 * 558 * @return The coordinate. 559 */ 560 public double getCategoryMiddle(int category, int categoryCount, 561 Rectangle2D area, RectangleEdge edge) { 562 563 return getCategoryStart(category, categoryCount, area, edge) 564 + calculateCategorySize(categoryCount, area, edge) / 2; 565 566 } 567 568 /** 569 * Returns the end coordinate for the specified category. 570 * 571 * @param category the category. 572 * @param categoryCount the number of categories. 573 * @param area the data area. 574 * @param edge the axis location. 575 * 576 * @return The coordinate. 577 */ 578 public double getCategoryEnd(int category, int categoryCount, 579 Rectangle2D area, RectangleEdge edge) { 580 581 return getCategoryStart(category, categoryCount, area, edge) 582 + calculateCategorySize(categoryCount, area, edge); 583 584 } 585 586 /** 587 * Calculates the size (width or height, depending on the location of the 588 * axis) of a category. 589 * 590 * @param categoryCount the number of categories. 591 * @param area the area within which the categories will be drawn. 592 * @param edge the axis location. 593 * 594 * @return The category size. 595 */ 596 protected double calculateCategorySize(int categoryCount, Rectangle2D area, 597 RectangleEdge edge) { 598 599 double result = 0.0; 600 double available = 0.0; 601 602 if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) { 603 available = area.getWidth(); 604 } 605 else if ((edge == RectangleEdge.LEFT) 606 || (edge == RectangleEdge.RIGHT)) { 607 available = area.getHeight(); 608 } 609 if (categoryCount > 1) { 610 result = available * (1 - getLowerMargin() - getUpperMargin() 611 - getCategoryMargin()); 612 result = result / categoryCount; 613 } 614 else { 615 result = available * (1 - getLowerMargin() - getUpperMargin()); 616 } 617 return result; 618 619 } 620 621 /** 622 * Calculates the size (width or height, depending on the location of the 623 * axis) of a category gap. 624 * 625 * @param categoryCount the number of categories. 626 * @param area the area within which the categories will be drawn. 627 * @param edge the axis location. 628 * 629 * @return The category gap width. 630 */ 631 protected double calculateCategoryGapSize(int categoryCount, 632 Rectangle2D area, 633 RectangleEdge edge) { 634 635 double result = 0.0; 636 double available = 0.0; 637 638 if ((edge == RectangleEdge.TOP) || (edge == RectangleEdge.BOTTOM)) { 639 available = area.getWidth(); 640 } 641 else if ((edge == RectangleEdge.LEFT) 642 || (edge == RectangleEdge.RIGHT)) { 643 available = area.getHeight(); 644 } 645 646 if (categoryCount > 1) { 647 result = available * getCategoryMargin() / (categoryCount - 1); 648 } 649 650 return result; 651 652 } 653 654 /** 655 * Estimates the space required for the axis, given a specific drawing area. 656 * 657 * @param g2 the graphics device (used to obtain font information). 658 * @param plot the plot that the axis belongs to. 659 * @param plotArea the area within which the axis should be drawn. 660 * @param edge the axis location (top or bottom). 661 * @param space the space already reserved. 662 * 663 * @return The space required to draw the axis. 664 */ 665 public AxisSpace reserveSpace(Graphics2D g2, Plot plot, 666 Rectangle2D plotArea, 667 RectangleEdge edge, AxisSpace space) { 668 669 // create a new space object if one wasn't supplied... 670 if (space == null) { 671 space = new AxisSpace(); 672 } 673 674 // if the axis is not visible, no additional space is required... 675 if (!isVisible()) { 676 return space; 677 } 678 679 // calculate the max size of the tick labels (if visible)... 680 double tickLabelHeight = 0.0; 681 double tickLabelWidth = 0.0; 682 if (isTickLabelsVisible()) { 683 g2.setFont(getTickLabelFont()); 684 AxisState state = new AxisState(); 685 // we call refresh ticks just to get the maximum width or height 686 refreshTicks(g2, state, plotArea, edge); 687 if (edge == RectangleEdge.TOP) { 688 tickLabelHeight = state.getMax(); 689 } 690 else if (edge == RectangleEdge.BOTTOM) { 691 tickLabelHeight = state.getMax(); 692 } 693 else if (edge == RectangleEdge.LEFT) { 694 tickLabelWidth = state.getMax(); 695 } 696 else if (edge == RectangleEdge.RIGHT) { 697 tickLabelWidth = state.getMax(); 698 } 699 } 700 701 // get the axis label size and update the space object... 702 Rectangle2D labelEnclosure = getLabelEnclosure(g2, edge); 703 double labelHeight = 0.0; 704 double labelWidth = 0.0; 705 if (RectangleEdge.isTopOrBottom(edge)) { 706 labelHeight = labelEnclosure.getHeight(); 707 space.add( 708 labelHeight + tickLabelHeight 709 + this.categoryLabelPositionOffset, edge 710 ); 711 } 712 else if (RectangleEdge.isLeftOrRight(edge)) { 713 labelWidth = labelEnclosure.getWidth(); 714 space.add( 715 labelWidth + tickLabelWidth + this.categoryLabelPositionOffset, 716 edge 717 ); 718 } 719 return space; 720 721 } 722 723 /** 724 * Configures the axis against the current plot. 725 */ 726 public void configure() { 727 // nothing required 728 } 729 730 /** 731 * Draws the axis on a Java 2D graphics device (such as the screen or a 732 * printer). 733 * 734 * @param g2 the graphics device (<code>null</code> not permitted). 735 * @param cursor the cursor location. 736 * @param plotArea the area within which the axis should be drawn 737 * (<code>null</code> not permitted). 738 * @param dataArea the area within which the plot is being drawn 739 * (<code>null</code> not permitted). 740 * @param edge the location of the axis (<code>null</code> not permitted). 741 * @param plotState collects information about the plot 742 * (<code>null</code> permitted). 743 * 744 * @return The axis state (never <code>null</code>). 745 */ 746 public AxisState draw(Graphics2D g2, 747 double cursor, 748 Rectangle2D plotArea, 749 Rectangle2D dataArea, 750 RectangleEdge edge, 751 PlotRenderingInfo plotState) { 752 753 // if the axis is not visible, don't draw it... 754 if (!isVisible()) { 755 return new AxisState(cursor); 756 } 757 758 if (isAxisLineVisible()) { 759 drawAxisLine(g2, cursor, dataArea, edge); 760 } 761 762 // draw the category labels and axis label 763 AxisState state = new AxisState(cursor); 764 state = drawCategoryLabels(g2, dataArea, edge, state, plotState); 765 state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state); 766 767 return state; 768 769 } 770 771 /** 772 * Draws the category labels and returns the updated axis state. 773 * 774 * @param g2 the graphics device (<code>null</code> not permitted). 775 * @param dataArea the area inside the axes (<code>null</code> not 776 * permitted). 777 * @param edge the axis location (<code>null</code> not permitted). 778 * @param state the axis state (<code>null</code> not permitted). 779 * @param plotState collects information about the plot (<code>null</code> 780 * permitted). 781 * 782 * @return The updated axis state (never <code>null</code>). 783 */ 784 protected AxisState drawCategoryLabels(Graphics2D g2, 785 Rectangle2D dataArea, 786 RectangleEdge edge, 787 AxisState state, 788 PlotRenderingInfo plotState) { 789 790 if (state == null) { 791 throw new IllegalArgumentException("Null 'state' argument."); 792 } 793 794 if (isTickLabelsVisible()) { 795 List ticks = refreshTicks(g2, state, dataArea, edge); 796 state.setTicks(ticks); 797 798 int categoryIndex = 0; 799 Iterator iterator = ticks.iterator(); 800 while (iterator.hasNext()) { 801 802 CategoryTick tick = (CategoryTick) iterator.next(); 803 g2.setFont(getTickLabelFont(tick.getCategory())); 804 g2.setPaint(getTickLabelPaint(tick.getCategory())); 805 806 CategoryLabelPosition position 807 = this.categoryLabelPositions.getLabelPosition(edge); 808 double x0 = 0.0; 809 double x1 = 0.0; 810 double y0 = 0.0; 811 double y1 = 0.0; 812 if (edge == RectangleEdge.TOP) { 813 x0 = getCategoryStart(categoryIndex, ticks.size(), 814 dataArea, edge); 815 x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, 816 edge); 817 y1 = state.getCursor() - this.categoryLabelPositionOffset; 818 y0 = y1 - state.getMax(); 819 } 820 else if (edge == RectangleEdge.BOTTOM) { 821 x0 = getCategoryStart(categoryIndex, ticks.size(), 822 dataArea, edge); 823 x1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, 824 edge); 825 y0 = state.getCursor() + this.categoryLabelPositionOffset; 826 y1 = y0 + state.getMax(); 827 } 828 else if (edge == RectangleEdge.LEFT) { 829 y0 = getCategoryStart(categoryIndex, ticks.size(), 830 dataArea, edge); 831 y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, 832 edge); 833 x1 = state.getCursor() - this.categoryLabelPositionOffset; 834 x0 = x1 - state.getMax(); 835 } 836 else if (edge == RectangleEdge.RIGHT) { 837 y0 = getCategoryStart(categoryIndex, ticks.size(), 838 dataArea, edge); 839 y1 = getCategoryEnd(categoryIndex, ticks.size(), dataArea, 840 edge); 841 x0 = state.getCursor() + this.categoryLabelPositionOffset; 842 x1 = x0 - state.getMax(); 843 } 844 Rectangle2D area = new Rectangle2D.Double(x0, y0, (x1 - x0), 845 (y1 - y0)); 846 Point2D anchorPoint = RectangleAnchor.coordinates(area, 847 position.getCategoryAnchor()); 848 TextBlock block = tick.getLabel(); 849 block.draw(g2, (float) anchorPoint.getX(), 850 (float) anchorPoint.getY(), position.getLabelAnchor(), 851 (float) anchorPoint.getX(), (float) anchorPoint.getY(), 852 position.getAngle()); 853 Shape bounds = block.calculateBounds(g2, 854 (float) anchorPoint.getX(), (float) anchorPoint.getY(), 855 position.getLabelAnchor(), (float) anchorPoint.getX(), 856 (float) anchorPoint.getY(), position.getAngle()); 857 if (plotState != null && plotState.getOwner() != null) { 858 EntityCollection entities 859 = plotState.getOwner().getEntityCollection(); 860 if (entities != null) { 861 String tooltip = getCategoryLabelToolTip( 862 tick.getCategory()); 863 entities.add(new TickLabelEntity(bounds, tooltip, 864 null)); 865 } 866 } 867 categoryIndex++; 868 } 869 870 if (edge.equals(RectangleEdge.TOP)) { 871 double h = state.getMax(); 872 state.cursorUp(h); 873 } 874 else if (edge.equals(RectangleEdge.BOTTOM)) { 875 double h = state.getMax(); 876 state.cursorDown(h); 877 } 878 else if (edge == RectangleEdge.LEFT) { 879 double w = state.getMax(); 880 state.cursorLeft(w); 881 } 882 else if (edge == RectangleEdge.RIGHT) { 883 double w = state.getMax(); 884 state.cursorRight(w); 885 } 886 } 887 return state; 888 } 889 890 /** 891 * Creates a temporary list of ticks that can be used when drawing the axis. 892 * 893 * @param g2 the graphics device (used to get font measurements). 894 * @param state the axis state. 895 * @param dataArea the area inside the axes. 896 * @param edge the location of the axis. 897 * 898 * @return A list of ticks. 899 */ 900 public List refreshTicks(Graphics2D g2, 901 AxisState state, 902 Rectangle2D dataArea, 903 RectangleEdge edge) { 904 905 List ticks = new java.util.ArrayList(); 906 907 // sanity check for data area... 908 if (dataArea.getHeight() <= 0.0 || dataArea.getWidth() < 0.0) { 909 return ticks; 910 } 911 912 CategoryPlot plot = (CategoryPlot) getPlot(); 913 List categories = plot.getCategories(); 914 double max = 0.0; 915 916 if (categories != null) { 917 CategoryLabelPosition position 918 = this.categoryLabelPositions.getLabelPosition(edge); 919 float r = this.maximumCategoryLabelWidthRatio; 920 if (r <= 0.0) { 921 r = position.getWidthRatio(); 922 } 923 924 float l = 0.0f; 925 if (position.getWidthType() == CategoryLabelWidthType.CATEGORY) { 926 l = (float) calculateCategorySize(categories.size(), dataArea, 927 edge); 928 } 929 else { 930 if (RectangleEdge.isLeftOrRight(edge)) { 931 l = (float) dataArea.getWidth(); 932 } 933 else { 934 l = (float) dataArea.getHeight(); 935 } 936 } 937 int categoryIndex = 0; 938 Iterator iterator = categories.iterator(); 939 while (iterator.hasNext()) { 940 Comparable category = (Comparable) iterator.next(); 941 TextBlock label = createLabel(category, l * r, edge, g2); 942 if (edge == RectangleEdge.TOP || edge == RectangleEdge.BOTTOM) { 943 max = Math.max(max, 944 calculateTextBlockHeight(label, position, g2)); 945 } 946 else if (edge == RectangleEdge.LEFT 947 || edge == RectangleEdge.RIGHT) { 948 max = Math.max(max, 949 calculateTextBlockWidth(label, position, g2)); 950 } 951 Tick tick = new CategoryTick(category, label, 952 position.getLabelAnchor(), position.getRotationAnchor(), 953 position.getAngle()); 954 ticks.add(tick); 955 categoryIndex = categoryIndex + 1; 956 } 957 } 958 state.setMax(max); 959 return ticks; 960 961 } 962 963 /** 964 * Creates a label. 965 * 966 * @param category the category. 967 * @param width the available width. 968 * @param edge the edge on which the axis appears. 969 * @param g2 the graphics device. 970 * 971 * @return A label. 972 */ 973 protected TextBlock createLabel(Comparable category, float width, 974 RectangleEdge edge, Graphics2D g2) { 975 TextBlock label = TextUtilities.createTextBlock( 976 category.toString(), getTickLabelFont(category), 977 getTickLabelPaint(category), width, this.maximumCategoryLabelLines, 978 new G2TextMeasurer(g2)); 979 return label; 980 } 981 982 /** 983 * A utility method for determining the width of a text block. 984 * 985 * @param block the text block. 986 * @param position the position. 987 * @param g2 the graphics device. 988 * 989 * @return The width. 990 */ 991 protected double calculateTextBlockWidth(TextBlock block, 992 CategoryLabelPosition position, 993 Graphics2D g2) { 994 995 RectangleInsets insets = getTickLabelInsets(); 996 Size2D size = block.calculateDimensions(g2); 997 Rectangle2D box = new Rectangle2D.Double( 998 0.0, 0.0, size.getWidth(), size.getHeight() 999 ); 1000 Shape rotatedBox = ShapeUtilities.rotateShape( 1001 box, position.getAngle(), 0.0f, 0.0f 1002 ); 1003 double w = rotatedBox.getBounds2D().getWidth() 1004 + insets.getTop() + insets.getBottom(); 1005 return w; 1006 1007 } 1008 1009 /** 1010 * A utility method for determining the height of a text block. 1011 * 1012 * @param block the text block. 1013 * @param position the label position. 1014 * @param g2 the graphics device. 1015 * 1016 * @return The height. 1017 */ 1018 protected double calculateTextBlockHeight(TextBlock block, 1019 CategoryLabelPosition position, 1020 Graphics2D g2) { 1021 1022 RectangleInsets insets = getTickLabelInsets(); 1023 Size2D size = block.calculateDimensions(g2); 1024 Rectangle2D box = new Rectangle2D.Double( 1025 0.0, 0.0, size.getWidth(), size.getHeight() 1026 ); 1027 Shape rotatedBox = ShapeUtilities.rotateShape( 1028 box, position.getAngle(), 0.0f, 0.0f 1029 ); 1030 double h = rotatedBox.getBounds2D().getHeight() 1031 + insets.getTop() + insets.getBottom(); 1032 return h; 1033 1034 } 1035 1036 /** 1037 * Creates a clone of the axis. 1038 * 1039 * @return A clone. 1040 * 1041 * @throws CloneNotSupportedException if some component of the axis does 1042 * not support cloning. 1043 */ 1044 public Object clone() throws CloneNotSupportedException { 1045 CategoryAxis clone = (CategoryAxis) super.clone(); 1046 clone.tickLabelFontMap = new HashMap(this.tickLabelFontMap); 1047 clone.tickLabelPaintMap = new HashMap(this.tickLabelPaintMap); 1048 clone.categoryLabelToolTips = new HashMap(this.categoryLabelToolTips); 1049 return clone; 1050 } 1051 1052 /** 1053 * Tests this axis for equality with an arbitrary object. 1054 * 1055 * @param obj the object (<code>null</code> permitted). 1056 * 1057 * @return A boolean. 1058 */ 1059 public boolean equals(Object obj) { 1060 if (obj == this) { 1061 return true; 1062 } 1063 if (!(obj instanceof CategoryAxis)) { 1064 return false; 1065 } 1066 if (!super.equals(obj)) { 1067 return false; 1068 } 1069 CategoryAxis that = (CategoryAxis) obj; 1070 if (that.lowerMargin != this.lowerMargin) { 1071 return false; 1072 } 1073 if (that.upperMargin != this.upperMargin) { 1074 return false; 1075 } 1076 if (that.categoryMargin != this.categoryMargin) { 1077 return false; 1078 } 1079 if (that.maximumCategoryLabelWidthRatio 1080 != this.maximumCategoryLabelWidthRatio) { 1081 return false; 1082 } 1083 if (that.categoryLabelPositionOffset 1084 != this.categoryLabelPositionOffset) { 1085 return false; 1086 } 1087 if (!ObjectUtilities.equal(that.categoryLabelPositions, 1088 this.categoryLabelPositions)) { 1089 return false; 1090 } 1091 if (!ObjectUtilities.equal(that.categoryLabelToolTips, 1092 this.categoryLabelToolTips)) { 1093 return false; 1094 } 1095 if (!ObjectUtilities.equal(this.tickLabelFontMap, 1096 that.tickLabelFontMap)) { 1097 return false; 1098 } 1099 if (!equalPaintMaps(this.tickLabelPaintMap, that.tickLabelPaintMap)) { 1100 return false; 1101 } 1102 return true; 1103 } 1104 1105 /** 1106 * Returns a hash code for this object. 1107 * 1108 * @return A hash code. 1109 */ 1110 public int hashCode() { 1111 if (getLabel() != null) { 1112 return getLabel().hashCode(); 1113 } 1114 else { 1115 return 0; 1116 } 1117 } 1118 1119 /** 1120 * Provides serialization support. 1121 * 1122 * @param stream the output stream. 1123 * 1124 * @throws IOException if there is an I/O error. 1125 */ 1126 private void writeObject(ObjectOutputStream stream) throws IOException { 1127 stream.defaultWriteObject(); 1128 writePaintMap(this.tickLabelPaintMap, stream); 1129 } 1130 1131 /** 1132 * Provides serialization support. 1133 * 1134 * @param stream the input stream. 1135 * 1136 * @throws IOException if there is an I/O error. 1137 * @throws ClassNotFoundException if there is a classpath problem. 1138 */ 1139 private void readObject(ObjectInputStream stream) 1140 throws IOException, ClassNotFoundException { 1141 stream.defaultReadObject(); 1142 this.tickLabelPaintMap = readPaintMap(stream); 1143 } 1144 1145 /** 1146 * Reads a <code>Map</code> of (<code>Comparable</code>, <code>Paint</code>) 1147 * elements from a stream. 1148 * 1149 * @param in the input stream. 1150 * 1151 * @return The map. 1152 * 1153 * @throws IOException 1154 * @throws ClassNotFoundException 1155 * 1156 * @see #writePaintMap(Map, ObjectOutputStream) 1157 */ 1158 private Map readPaintMap(ObjectInputStream in) 1159 throws IOException, ClassNotFoundException { 1160 boolean isNull = in.readBoolean(); 1161 if (isNull) { 1162 return null; 1163 } 1164 Map result = new HashMap(); 1165 int count = in.readInt(); 1166 for (int i = 0; i < count; i++) { 1167 Comparable category = (Comparable) in.readObject(); 1168 Paint paint = SerialUtilities.readPaint(in); 1169 result.put(category, paint); 1170 } 1171 return result; 1172 } 1173 1174 /** 1175 * Writes a map of (<code>Comparable</code>, <code>Paint</code>) 1176 * elements to a stream. 1177 * 1178 * @param map the map (<code>null</code> permitted). 1179 * 1180 * @param out 1181 * @throws IOException 1182 * 1183 * @see #readPaintMap(ObjectInputStream) 1184 */ 1185 private void writePaintMap(Map map, ObjectOutputStream out) 1186 throws IOException { 1187 if (map == null) { 1188 out.writeBoolean(true); 1189 } 1190 else { 1191 out.writeBoolean(false); 1192 Set keys = map.keySet(); 1193 int count = keys.size(); 1194 out.writeInt(count); 1195 Iterator iterator = keys.iterator(); 1196 while (iterator.hasNext()) { 1197 Comparable key = (Comparable) iterator.next(); 1198 out.writeObject(key); 1199 SerialUtilities.writePaint((Paint) map.get(key), out); 1200 } 1201 } 1202 } 1203 1204 /** 1205 * Tests two maps containing (<code>Comparable</code>, <code>Paint</code>) 1206 * elements for equality. 1207 * 1208 * @param map1 the first map (<code>null</code> not permitted). 1209 * @param map2 the second map (<code>null</code> not permitted). 1210 * 1211 * @return A boolean. 1212 */ 1213 private boolean equalPaintMaps(Map map1, Map map2) { 1214 if (map1.size() != map2.size()) { 1215 return false; 1216 } 1217 Set keys = map1.keySet(); 1218 Iterator iterator = keys.iterator(); 1219 while (iterator.hasNext()) { 1220 Comparable key = (Comparable) iterator.next(); 1221 Paint p1 = (Paint) map1.get(key); 1222 Paint p2 = (Paint) map2.get(key); 1223 if (!PaintUtilities.equal(p1, p2)) { 1224 return false; 1225 } 1226 } 1227 return true; 1228 } 1229 1230 }