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