    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
    100    package org.jfree.chart.axis;
    101
    102    import java.awt.Font;
    103    import java.awt.Graphics2D;
    104    import java.awt.Paint;
    105    import java.awt.Shape;
    106    import java.awt.geom.Line2D;
    107    import java.awt.geom.Point2D;
    108    import java.awt.geom.Rectangle2D;
    109    import java.io.IOException;
    110    import java.io.ObjectInputStream;
    111    import java.io.ObjectOutputStream;
    112    import java.io.Serializable;
    113    import java.util.HashMap;
    114    import java.util.Iterator;
    115    import java.util.List;
    116    import java.util.Map;
    117    import java.util.Set;
    118
    119    import org.jfree.chart.entity.CategoryLabelEntity;
    120    import org.jfree.chart.entity.EntityCollection;
    121    import org.jfree.chart.event.AxisChangeEvent;
    122    import org.jfree.chart.plot.CategoryPlot;
    123    import org.jfree.chart.plot.Plot;
    124    import org.jfree.chart.plot.PlotRenderingInfo;
    125    import org.jfree.data.category.CategoryDataset;
    126    import org.jfree.io.SerialUtilities;
    127    import org.jfree.text.G2TextMeasurer;
    128    import org.jfree.text.TextBlock;
    129    import org.jfree.text.TextUtilities;
    130    import org.jfree.ui.RectangleAnchor;
    131    import org.jfree.ui.RectangleEdge;
    132    import org.jfree.ui.RectangleInsets;
    133    import org.jfree.ui.Size2D;
    134    import org.jfree.util.ObjectUtilities;
    135    import org.jfree.util.PaintUtilities;
    136    import org.jfree.util.ShapeUtilities;
    137
    138    /**
    139     * An axis that displays categories.
    140     */
    141    public 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    }