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