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