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