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