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