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    }