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