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