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