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