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