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