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