    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     * SymbolAxis.java
    029     * ---------------
    030     * (C) Copyright 2002-2005, by Object Refinery Limited and Contributors.
    031     *
    032     * Original Author:  Anthony Boulestreau;
    033     * Contributor(s):   David Gilbert (for Object Refinery Limited);
    034     *
    035     *
    036     * Changes (from 23-Jun-2001)
    037     * --------------------------
    038     * 29-Mar-2002 : First version (AB);
    039     * 19-Apr-2002 : Updated formatting and import statements (DG);
    040     * 21-Jun-2002 : Make change to use the class TickUnit - remove valueToString()
    041     *               method and add SymbolicTickUnit (AB);
    042     * 25-Jun-2002 : Removed redundant code (DG);
    043     * 25-Jul-2002 : Changed order of parameters in ValueAxis constructor (DG);
    044     * 05-Sep-2002 : Updated constructor to reflect changes in the Axis class (DG);
    045     * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG);
    046     * 14-Feb-2003 : Added back missing constructor code (DG);
    047     * 26-Mar-2003 : Implemented Serializable (DG);
    048     * 14-May-2003 : Renamed HorizontalSymbolicAxis --> SymbolicAxis and merged in
    049     *               VerticalSymbolicAxis (DG);
    050     * 12-Aug-2003 : Fixed bug where refreshTicks() method has different signature
    051     *               to super class (DG);
    052     * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);
    053     * 02-Nov-2003 : Added code to avoid overlapping labels (MR);
    054     * 07-Nov-2003 : Modified to use new tick classes (DG);
    055     * 18-Nov-2003 : Fixed bug where symbols are not being displayed on the
    056     *               axis (DG);
    057     * 24-Nov-2003 : Added fix for gridlines on zooming (bug id 834643) (DG);
    058     * 21-Jan-2004 : Update for renamed method in ValueAxis (DG);
    059     * 11-Mar-2004 : Modified the way the background grid color is being drawn, see
    060     *               this thread:
    061     *               http://www.jfree.org/phpBB2/viewtopic.php?p=22973 (DG);
    062     * 16-Mar-2004 : Added plotState to draw() method (DG);
    063     * 07-Apr-2004 : Modified string bounds calculation (DG);
    064     * 28-Mar-2005 : Renamed autoRangeIncludesZero() --> getAutoRangeIncludesZero()
    065     *               and autoRangeStickyZero() --> getAutoRangeStickyZero() (DG);
    066     * 05-Jul-2005 : Fixed signature on refreshTicks() method - see bug report
    067     *               1232264 (DG);
    068     * 06-Jul-2005 : Renamed SymbolicAxis --> SymbolAxis, added equals() method,
    069     *               renamed getSymbolicValue() --> getSymbols(), renamed
    070     *               symbolicGridPaint --> gridBandPaint, fixed serialization of
    071     *               gridBandPaint, renamed symbolicGridLinesVisible -->
    072     *               gridBandsVisible, eliminated symbolicGridLineList (DG);
    073     *
    074     */
    075
    076    package org.jfree.chart.axis;
    077
    078    import java.awt.BasicStroke;
    079    import java.awt.Color;
    080    import java.awt.Font;
    081    import java.awt.Graphics2D;
    082    import java.awt.Paint;
    083    import java.awt.Shape;
    084    import java.awt.geom.Rectangle2D;
    085    import java.io.IOException;
    086    import java.io.ObjectInputStream;
    087    import java.io.ObjectOutputStream;
    088    import java.io.Serializable;
    089    import java.text.NumberFormat;
    090    import java.util.Arrays;
    091    import java.util.Iterator;
    092    import java.util.List;
    093
    094    import org.jfree.chart.event.AxisChangeEvent;
    095    import org.jfree.chart.plot.Plot;
    096    import org.jfree.chart.plot.PlotRenderingInfo;
    097    import org.jfree.chart.plot.ValueAxisPlot;
    098    import org.jfree.data.Range;
    099    import org.jfree.io.SerialUtilities;
    100    import org.jfree.text.TextUtilities;
    101    import org.jfree.ui.RectangleEdge;
    102    import org.jfree.ui.TextAnchor;
    103    import org.jfree.util.PaintUtilities;
    104
    105    /**
    106     * A standard linear value axis that replaces integer values with symbols.
    107     *
    108     * @author Anthony Boulestreau
    109     */
    110    public class SymbolAxis extends NumberAxis implements Serializable {
    111
    112        /** For serialization. */
    113        private static final long serialVersionUID = 7216330468770619716L;
    114
    115        /** The default grid band paint. */
    116        public static final Paint DEFAULT_GRID_BAND_PAINT
    117            = new Color(232, 234, 232, 128);
    118
    119        /** The list of symbols to display instead of the numeric values. */
    120        private List symbols;
    121
    122        /** The paint used to color the grid bands (if the bands are visible). */
    123        private transient Paint gridBandPaint;
    124
    125        /** Flag that indicates whether or not grid bands are visible. */
    126        private boolean gridBandsVisible;
    127
    128        /**
    129         * Constructs a symbol axis, using default attribute values where
    130         * necessary.
    131         *
    132         * @param label  the axis label (null permitted).
    133         * @param sv  the list of symbols to display instead of the numeric
    134         *            values.
    135         */
    136        public SymbolAxis(String label, String[] sv) {
    137            super(label);
    138            this.symbols = Arrays.asList(sv);
    139            this.gridBandsVisible = true;
    140            this.gridBandPaint = DEFAULT_GRID_BAND_PAINT;
    141
    142            setAutoTickUnitSelection(false, false);
    143            setAutoRangeStickyZero(false);
    144
    145        }
    146
    147        /**
    148         * Returns an array of the symbols for the axis.
    149         *
    150         * @return The symbols.
    151         */
    152        public String[] getSymbols() {
    153            String[] result = new String[this.symbols.size()];
    154            result = (String[]) this.symbols.toArray(result);
    155            return result;
    156        }
    157
    158        /**
    159         * Returns the paint used to color the grid bands.
    160         *
    161         * @return The grid band paint (never <code>null</code>).
    162         *
    163         * @see #isGridBandsVisible()
    164         */
    165        public Paint getGridBandPaint() {
    166            return this.gridBandPaint;
    167        }
    168
    169        /**
    170         * Sets the grid band paint and sends an {@link AxisChangeEvent} to
    171         * all registered listeners.
    172         *
    173         * @param paint  the paint (<code>null</code> not permitted).
    174         */
    175        public void setGridBandPaint(Paint paint) {
    176            if (paint == null) {
    177                throw new IllegalArgumentException("Null 'paint' argument.");
    178            }
    179            this.gridBandPaint = paint;
    180            notifyListeners(new AxisChangeEvent(this));
    181        }
    182
    183        /**
    184         * Returns <code>true</code> if the grid bands are showing, and
    185         * <code>false</code> otherwise.
    186         *
    187         * @return <code>true</code> if the grid bands are showing, and
    188         *         <code>false</code> otherwise.
    189         */
    190        public boolean isGridBandsVisible() {
    191            return this.gridBandsVisible;
    192        }
    193
    194        /**
    195         * Sets the visibility of the grid bands and notifies registered
    196         * listeners that the axis has been modified.
    197         *
    198         * @param flag  the new setting.
    199         */
    200        public void setGridBandsVisible(boolean flag) {
    201            if (this.gridBandsVisible != flag) {
    202                this.gridBandsVisible = flag;
    203                notifyListeners(new AxisChangeEvent(this));
    204            }
    205        }
    206
    207        /**
    208         * This operation is not supported by this axis.
    209         *
    210         * @param g2  the graphics device.
    211         * @param dataArea  the area in which the plot and axes should be drawn.
    212         * @param edge  the edge along which the axis is drawn.
    213         */
    214        protected void selectAutoTickUnit(Graphics2D g2, Rectangle2D dataArea,
    215                                          RectangleEdge edge) {
    216            throw new UnsupportedOperationException();
    217        }
    218
    219        /**
    220         * Draws the axis on a Java 2D graphics device (such as the screen or a
    221         * printer).
    222         *
    223         * @param g2  the graphics device (<code>null</code> not permitted).
    224         * @param cursor  the cursor location.
    225         * @param plotArea  the area within which the plot and axes should be drawn
    226         *                  (<code>null</code> not permitted).
    227         * @param dataArea  the area within which the data should be drawn
    228         *                  (<code>null</code> not permitted).
    229         * @param edge  the axis location (<code>null</code> not permitted).
    230         * @param plotState  collects information about the plot
    231         *                   (<code>null</code> permitted).
    232         *
    233         * @return The axis state (never <code>null</code>).
    234         */
    235        public AxisState draw(Graphics2D g2,
    236                              double cursor,
    237                              Rectangle2D plotArea,
    238                              Rectangle2D dataArea,
    239                              RectangleEdge edge,
    240                              PlotRenderingInfo plotState) {
    241
    242            AxisState info = new AxisState(cursor);
    243            if (isVisible()) {
    244                info = super.draw(g2, cursor, plotArea, dataArea, edge, plotState);
    245            }
    246            if (this.gridBandsVisible) {
    247                drawGridBands(g2, plotArea, dataArea, edge, info.getTicks());
    248            }
    249            return info;
    250
    251        }
    252
    253        /**
    254         * Draws the grid bands.  Alternate bands are colored using
    255         * <CODE>gridBandPaint<CODE> (<CODE>DEFAULT_GRID_BAND_PAINT</CODE> by
    256         * default).
    257         *
    258         * @param g2  the graphics device.
    259         * @param plotArea  the area within which the chart should be drawn.
    260         * @param dataArea  the area within which the plot should be drawn (a
    261         *                  subset of the drawArea).
    262         * @param edge  the axis location.
    263         * @param ticks  the ticks.
    264         */
    265        protected void drawGridBands(Graphics2D g2,
    266                                     Rectangle2D plotArea,
    267                                     Rectangle2D dataArea,
    268                                     RectangleEdge edge,
    269                                     List ticks) {
    270
    271            Shape savedClip = g2.getClip();
    272            g2.clip(dataArea);
    273            if (RectangleEdge.isTopOrBottom(edge)) {
    274                drawGridBandsHorizontal(g2, plotArea, dataArea, true, ticks);
    275            }
    276            else if (RectangleEdge.isLeftOrRight(edge)) {
    277                drawGridBandsVertical(g2, plotArea, dataArea, true, ticks);
    278            }
    279            g2.setClip(savedClip);
    280
    281        }
    282
    283        /**
    284         * Draws the grid bands for the axis when it is at the top or bottom of
    285         * the plot.
    286         *
    287         * @param g2  the graphics device.
    288         * @param plotArea  the area within which the chart should be drawn.
    289         * @param dataArea  the area within which the plot should be drawn
    290         *                  (a subset of the drawArea).
    291         * @param firstGridBandIsDark  True: the first grid band takes the
    292         *                             color of <CODE>gridBandPaint<CODE>.
    293         *                             False: the second grid band takes the
    294         *                             color of <CODE>gridBandPaint<CODE>.
    295         * @param ticks  the ticks.
    296         */
    297        protected void drawGridBandsHorizontal(Graphics2D g2,
    298                                               Rectangle2D plotArea,
    299                                               Rectangle2D dataArea,
    300                                               boolean firstGridBandIsDark,
    301                                               List ticks) {
    302
    303            boolean currentGridBandIsDark = firstGridBandIsDark;
    304            double yy = dataArea.getY();
    305            double xx1, xx2;
    306
    307            //gets the outline stroke width of the plot
    308            double outlineStrokeWidth;
    309            if (getPlot().getOutlineStroke() !=  null) {
    310                outlineStrokeWidth
    311                    = ((BasicStroke) getPlot().getOutlineStroke()).getLineWidth();
    312            }
    313            else {
    314                outlineStrokeWidth = 1d;
    315            }
    316
    317            Iterator iterator = ticks.iterator();
    318            ValueTick tick;
    319            Rectangle2D band;
    320            while (iterator.hasNext()) {
    321                tick = (ValueTick) iterator.next();
    322                xx1 = valueToJava2D(
    323                    tick.getValue() - 0.5d, dataArea, RectangleEdge.BOTTOM
    324                );
    325                xx2 = valueToJava2D(
    326                    tick.getValue() + 0.5d, dataArea, RectangleEdge.BOTTOM
    327                );
    328                if (currentGridBandIsDark) {
    329                    g2.setPaint(this.gridBandPaint);
    330                }
    331                else {
    332                    g2.setPaint(Color.white);
    333                }
    334                band = new Rectangle2D.Double(xx1, yy + outlineStrokeWidth,
    335                    xx2 - xx1, dataArea.getMaxY() - yy - outlineStrokeWidth);
    336                g2.fill(band);
    337                currentGridBandIsDark = !currentGridBandIsDark;
    338            }
    339            g2.setPaintMode();
    340        }
    341
    342        /**
    343         * Draws the grid bands for the axis when it is at the top or bottom of
    344         * the plot.
    345         *
    346         * @param g2  the graphics device.
    347         * @param drawArea  the area within which the chart should be drawn.
    348         * @param plotArea  the area within which the plot should be drawn (a
    349         *                  subset of the drawArea).
    350         * @param firstGridBandIsDark  True: the first grid band takes the
    351         *                             color of <CODE>gridBandPaint<CODE>.
    352         *                             False: the second grid band takes the
    353         *                             color of <CODE>gridBandPaint<CODE>.
    354         * @param ticks  a list of ticks.
    355         */
    356        protected void drawGridBandsVertical(Graphics2D g2,
    357                                             Rectangle2D drawArea,
    358                                             Rectangle2D plotArea,
    359                                             boolean firstGridBandIsDark,
    360                                             List ticks) {
    361
    362            boolean currentGridBandIsDark = firstGridBandIsDark;
    363            double xx = plotArea.getX();
    364            double yy1, yy2;
    365
    366            //gets the outline stroke width of the plot
    367            double outlineStrokeWidth;
    368            if (getPlot().getOutlineStroke() != null) {
    369                outlineStrokeWidth
    370                    = ((BasicStroke) getPlot().getOutlineStroke()).getLineWidth();
    371            }
    372            else {
    373                outlineStrokeWidth = 1d;
    374            }
    375
    376            Iterator iterator = ticks.iterator();
    377            ValueTick tick;
    378            Rectangle2D band;
    379            while (iterator.hasNext()) {
    380                tick = (ValueTick) iterator.next();
    381                yy1 = valueToJava2D(
    382                    tick.getValue() + 0.5d, plotArea, RectangleEdge.LEFT
    383                );
    384                yy2 = valueToJava2D(
    385                    tick.getValue() - 0.5d, plotArea, RectangleEdge.LEFT
    386                );
    387                if (currentGridBandIsDark) {
    388                    g2.setPaint(this.gridBandPaint);
    389                }
    390                else {
    391                    g2.setPaint(Color.white);
    392                }
    393                band = new Rectangle2D.Double(xx + outlineStrokeWidth,
    394                    yy1, plotArea.getMaxX() - xx - outlineStrokeWidth, yy2 - yy1);
    395                g2.fill(band);
    396                currentGridBandIsDark = !currentGridBandIsDark;
    397            }
    398            g2.setPaintMode();
    399        }
    400
    401        /**
    402         * Rescales the axis to ensure that all data is visible.
    403         */
    404        protected void autoAdjustRange() {
    405
    406            Plot plot = getPlot();
    407            if (plot == null) {
    408                return;  // no plot, no data
    409            }
    410
    411            if (plot instanceof ValueAxisPlot) {
    412
    413                // ensure that all the symbols are displayed
    414                double upper = this.symbols.size() - 1;
    415                double lower = 0;
    416                double range = upper - lower;
    417
    418                // ensure the autorange is at least <minRange> in size...
    419                double minRange = getAutoRangeMinimumSize();
    420                if (range < minRange) {
    421                    upper = (upper + lower + minRange) / 2;
    422                    lower = (upper + lower - minRange) / 2;
    423                }
    424
    425                // this ensure that the grid bands will be displayed correctly.
    426                double upperMargin = 0.5;
    427                double lowerMargin = 0.5;
    428
    429                if (getAutoRangeIncludesZero()) {
    430                    if (getAutoRangeStickyZero()) {
    431                        if (upper <= 0.0) {
    432                            upper = 0.0;
    433                        }
    434                        else {
    435                            upper = upper + upperMargin;
    436                        }
    437                        if (lower >= 0.0) {
    438                            lower = 0.0;
    439                        }
    440                        else {
    441                            lower = lower - lowerMargin;
    442                        }
    443                    }
    444                    else {
    445                        upper = Math.max(0.0, upper + upperMargin);
    446                        lower = Math.min(0.0, lower - lowerMargin);
    447                    }
    448                }
    449                else {
    450                    if (getAutoRangeStickyZero()) {
    451                        if (upper <= 0.0) {
    452                            upper = Math.min(0.0, upper + upperMargin);
    453                        }
    454                        else {
    455                            upper = upper + upperMargin * range;
    456                        }
    457                        if (lower >= 0.0) {
    458                            lower = Math.max(0.0, lower - lowerMargin);
    459                        }
    460                        else {
    461                            lower = lower - lowerMargin;
    462                        }
    463                    }
    464                    else {
    465                        upper = upper + upperMargin;
    466                        lower = lower - lowerMargin;
    467                    }
    468                }
    469
    470                setRange(new Range(lower, upper), false, false);
    471
    472            }
    473
    474        }
    475
    476        /**
    477         * Calculates the positions of the tick labels for the axis, storing the
    478         * results in the tick label list (ready for drawing).
    479         *
    480         * @param g2  the graphics device.
    481         * @param state  the axis state.
    482         * @param dataArea  the area in which the data should be drawn.
    483         * @param edge  the location of the axis.
    484         *
    485         * @return A list of ticks.
    486         */
    487        public List refreshTicks(Graphics2D g2,
    488                                 AxisState state,
    489                                 Rectangle2D dataArea,
    490                                 RectangleEdge edge) {
    491            List ticks = null;
    492            if (RectangleEdge.isTopOrBottom(edge)) {
    493                ticks = refreshTicksHorizontal(g2, dataArea, edge);
    494            }
    495            else if (RectangleEdge.isLeftOrRight(edge)) {
    496                ticks = refreshTicksVertical(g2, dataArea, edge);
    497            }
    498            return ticks;
    499        }
    500
    501        /**
    502         * Calculates the positions of the tick labels for the axis, storing the
    503         * results in the tick label list (ready for drawing).
    504         *
    505         * @param g2  the graphics device.
    506         * @param dataArea  the area in which the data should be drawn.
    507         * @param edge  the location of the axis.
    508         *
    509         * @return The ticks.
    510         */
    511        protected List refreshTicksHorizontal(Graphics2D g2,
    512                                              Rectangle2D dataArea,
    513                                              RectangleEdge edge) {
    514
    515            List ticks = new java.util.ArrayList();
    516
    517            Font tickLabelFont = getTickLabelFont();
    518            g2.setFont(tickLabelFont);
    519
    520            double size = getTickUnit().getSize();
    521            int count = calculateVisibleTickCount();
    522            double lowestTickValue = calculateLowestVisibleTickValue();
    523
    524            double previousDrawnTickLabelPos = 0.0;
    525            double previousDrawnTickLabelLength = 0.0;
    526
    527            if (count <= ValueAxis.MAXIMUM_TICK_COUNT) {
    528                for (int i = 0; i < count; i++) {
    529                    double currentTickValue = lowestTickValue + (i * size);
    530                    double xx = valueToJava2D(currentTickValue, dataArea, edge);
    531                    String tickLabel;
    532                    NumberFormat formatter = getNumberFormatOverride();
    533                    if (formatter != null) {
    534                        tickLabel = formatter.format(currentTickValue);
    535                    }
    536                    else {
    537                        tickLabel = valueToString(currentTickValue);
    538                    }
    539
    540                    // avoid to draw overlapping tick labels
    541                    Rectangle2D bounds = TextUtilities.getTextBounds(
    542                        tickLabel, g2, g2.getFontMetrics()
    543                    );
    544                    double tickLabelLength = isVerticalTickLabels()
    545                        ? bounds.getHeight() : bounds.getWidth();
    546                    boolean tickLabelsOverlapping = false;
    547                    if (i > 0) {
    548                        double avgTickLabelLength = (previousDrawnTickLabelLength
    549                            + tickLabelLength) / 2.0;
    550                        if (Math.abs(xx - previousDrawnTickLabelPos)
    551                                < avgTickLabelLength) {
    552                            tickLabelsOverlapping = true;
    553                        }
    554                    }
    555                    if (tickLabelsOverlapping) {
    556                        tickLabel = ""; // don't draw this tick label
    557                    }
    558                    else {
    559                        // remember these values for next comparison
    560                        previousDrawnTickLabelPos = xx;
    561                        previousDrawnTickLabelLength = tickLabelLength;
    562                    }
    563
    564                    TextAnchor anchor = null;
    565                    TextAnchor rotationAnchor = null;
    566                    double angle = 0.0;
    567                    if (isVerticalTickLabels()) {
    568                        anchor = TextAnchor.CENTER_RIGHT;
    569                        rotationAnchor = TextAnchor.CENTER_RIGHT;
    570                        if (edge == RectangleEdge.TOP) {
    571                            angle = Math.PI / 2.0;
    572                        }
    573                        else {
    574                            angle = -Math.PI / 2.0;
    575                        }
    576                    }
    577                    else {
    578                        if (edge == RectangleEdge.TOP) {
    579                            anchor = TextAnchor.BOTTOM_CENTER;
    580                            rotationAnchor = TextAnchor.BOTTOM_CENTER;
    581                        }
    582                        else {
    583                            anchor = TextAnchor.TOP_CENTER;
    584                            rotationAnchor = TextAnchor.TOP_CENTER;
    585                        }
    586                    }
    587                    Tick tick = new NumberTick(
    588                        new Double(currentTickValue), tickLabel, anchor,
    589                        rotationAnchor, angle
    590                    );
    591                    ticks.add(tick);
    592                }
    593            }
    594            return ticks;
    595
    596        }
    597
    598        /**
    599         * Calculates the positions of the tick labels for the axis, storing the
    600         * results in the tick label list (ready for drawing).
    601         *
    602         * @param g2  the graphics device.
    603         * @param dataArea  the area in which the plot should be drawn.
    604         * @param edge  the location of the axis.
    605         *
    606         * @return The ticks.
    607         */
    608        protected List refreshTicksVertical(Graphics2D g2,
    609                                            Rectangle2D dataArea,
    610                                            RectangleEdge edge) {
    611
    612            List ticks = new java.util.ArrayList();
    613
    614            Font tickLabelFont = getTickLabelFont();
    615            g2.setFont(tickLabelFont);
    616
    617            double size = getTickUnit().getSize();
    618            int count = calculateVisibleTickCount();
    619            double lowestTickValue = calculateLowestVisibleTickValue();
    620
    621            double previousDrawnTickLabelPos = 0.0;
    622            double previousDrawnTickLabelLength = 0.0;
    623
    624            if (count <= ValueAxis.MAXIMUM_TICK_COUNT) {
    625                for (int i = 0; i < count; i++) {
    626                    double currentTickValue = lowestTickValue + (i * size);
    627                    double yy = valueToJava2D(currentTickValue, dataArea, edge);
    628                    String tickLabel;
    629                    NumberFormat formatter = getNumberFormatOverride();
    630                    if (formatter != null) {
    631                        tickLabel = formatter.format(currentTickValue);
    632                    }
    633                    else {
    634                        tickLabel = valueToString(currentTickValue);
    635                    }
    636
    637                    // avoid to draw overlapping tick labels
    638                    Rectangle2D bounds = TextUtilities.getTextBounds(
    639                        tickLabel, g2, g2.getFontMetrics()
    640                    );
    641                    double tickLabelLength = isVerticalTickLabels()
    642                        ? bounds.getWidth() : bounds.getHeight();
    643                    boolean tickLabelsOverlapping = false;
    644                    if (i > 0) {
    645                        double avgTickLabelLength =
    646                            (previousDrawnTickLabelLength + tickLabelLength) / 2.0;
    647                        if (Math.abs(yy - previousDrawnTickLabelPos)
    648                                < avgTickLabelLength) {
    649                            tickLabelsOverlapping = true;
    650                        }
    651                        if (tickLabelsOverlapping) {
    652                            tickLabel = ""; // don't draw this tick label
    653                        }
    654                        else {
    655                            // remember these values for next comparison
    656                            previousDrawnTickLabelPos = yy;
    657                            previousDrawnTickLabelLength = tickLabelLength;
    658                        }
    659                    }
    660                    TextAnchor anchor = null;
    661                    TextAnchor rotationAnchor = null;
    662                    double angle = 0.0;
    663                    if (isVerticalTickLabels()) {
    664                        anchor = TextAnchor.BOTTOM_CENTER;
    665                        rotationAnchor = TextAnchor.BOTTOM_CENTER;
    666                        if (edge == RectangleEdge.LEFT) {
    667                            angle = -Math.PI / 2.0;
    668                        }
    669                        else {
    670                            angle = Math.PI / 2.0;
    671                        }
    672                    }
    673                    else {
    674                        if (edge == RectangleEdge.LEFT) {
    675                            anchor = TextAnchor.CENTER_RIGHT;
    676                            rotationAnchor = TextAnchor.CENTER_RIGHT;
    677                        }
    678                        else {
    679                            anchor = TextAnchor.CENTER_LEFT;
    680                            rotationAnchor = TextAnchor.CENTER_LEFT;
    681                        }
    682                    }
    683                    Tick tick = new NumberTick(
    684                        new Double(currentTickValue), tickLabel, anchor,
    685                        rotationAnchor, angle
    686                    );
    687                    ticks.add(tick);
    688                }
    689            }
    690            return ticks;
    691
    692        }
    693
    694        /**
    695         * Converts a value to a string, using the list of symbols.
    696         *
    697         * @param value  value to convert.
    698         *
    699         * @return The symbol.
    700         */
    701        public String valueToString(double value) {
    702            String strToReturn;
    703            try {
    704                strToReturn = (String) this.symbols.get((int) value);
    705            }
    706            catch (IndexOutOfBoundsException  ex) {
    707                strToReturn = "";
    708            }
    709            return strToReturn;
    710        }
    711
    712        /**
    713         * Tests this axis for equality with an arbitrary object.
    714         *
    715         * @param obj  the object (<code>null</code> permitted).
    716         *
    717         * @return A boolean.
    718         */
    719        public boolean equals(Object obj) {
    720            if (obj == this) {
    721                return true;
    722            }
    723            if (!(obj instanceof SymbolAxis)) {
    724                return false;
    725            }
    726            SymbolAxis that = (SymbolAxis) obj;
    727            if (!this.symbols.equals(that.symbols)) {
    728                return false;
    729            }
    730            if (this.gridBandsVisible != that.gridBandsVisible) {
    731                return false;
    732            }
    733            if (!PaintUtilities.equal(this.gridBandPaint, that.gridBandPaint)) {
    734                return false;
    735            }
    736            if (!super.equals(obj)) {
    737                return false;
    738            }
    739            return true;
    740        }
    741
    742        /**
    743         * Provides serialization support.
    744         *
    745         * @param stream  the output stream.
    746         *
    747         * @throws IOException  if there is an I/O error.
    748         */
    749        private void writeObject(ObjectOutputStream stream) throws IOException {
    750            stream.defaultWriteObject();
    751            SerialUtilities.writePaint(this.gridBandPaint, stream);
    752        }
    753
    754        /**
    755         * Provides serialization support.
    756         *
    757         * @param stream  the input stream.
    758         *
    759         * @throws IOException  if there is an I/O error.
    760         * @throws ClassNotFoundException  if there is a classpath problem.
    761         */
    762        private void readObject(ObjectInputStream stream)
    763            throws IOException, ClassNotFoundException {
    764            stream.defaultReadObject();
    765            this.gridBandPaint = SerialUtilities.readPaint(stream);
    766        }
    767
    768    }