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
081package org.jfree.chart.axis;
082
083import java.awt.BasicStroke;
084import java.awt.Color;
085import java.awt.Font;
086import java.awt.Graphics2D;
087import java.awt.Paint;
088import java.awt.Shape;
089import java.awt.Stroke;
090import java.awt.geom.Rectangle2D;
091import java.io.IOException;
092import java.io.ObjectInputStream;
093import java.io.ObjectOutputStream;
094import java.io.Serializable;
095import java.text.NumberFormat;
096import java.util.Arrays;
097import java.util.Iterator;
098import java.util.List;
099
100import org.jfree.chart.event.AxisChangeEvent;
101import org.jfree.chart.plot.Plot;
102import org.jfree.chart.plot.PlotRenderingInfo;
103import org.jfree.chart.plot.ValueAxisPlot;
104import org.jfree.data.Range;
105import org.jfree.io.SerialUtilities;
106import org.jfree.text.TextUtilities;
107import org.jfree.ui.RectangleEdge;
108import org.jfree.ui.TextAnchor;
109import org.jfree.util.PaintUtilities;
110
111/**
112 * A standard linear value axis that replaces integer values with symbols.
113 */
114public 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}