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