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