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