    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}