Source for org.jfree.chart.axis.CategoryAxis

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