1 /* ===========================================================
2  * JFreeChart : a free chart library for the Java(tm) platform
3  * ===========================================================
4  *
5  * (C) Copyright 2000-2013, by Object Refinery Limited and Contributors.
6  *
7  * Project Info:  http://www.jfree.org/jfreechart/index.html
8  *
9  * This library is free software; you can redistribute it and/or modify it
10  * under the terms of the GNU Lesser General Public License as published by
11  * the Free Software Foundation; either version 2.1 of the License, or
12  * (at your option) any later version.
13  *
14  * This library is distributed in the hope that it will be useful, but
15  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
17  * License for more details.
18  *
19  * You should have received a copy of the GNU Lesser General Public
20  * License along with this library; if not, write to the Free Software
21  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
22  * USA.
23  *
24  * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
25  * Other names may be trademarks of their respective owners.]
26  *
27  * ---------------
28  * SymbolAxis.java
29  * ---------------
30  * (C) Copyright 2002-2013, by Anthony Boulestreau and Contributors.
31  *
32  * Original Author:  Anthony Boulestreau;
33  * Contributor(s):   David Gilbert (for Object Refinery Limited);
34  *
35  *
36  * Changes
37  * -------
38  * 29-Mar-2002 : First version (AB);
39  * 19-Apr-2002 : Updated formatting and import statements (DG);
40  * 21-Jun-2002 : Make change to use the class TickUnit - remove valueToString()
41  *               method and add SymbolicTickUnit (AB);
42  * 25-Jun-2002 : Removed redundant code (DG);
43  * 25-Jul-2002 : Changed order of parameters in ValueAxis constructor (DG);
44  * 05-Sep-2002 : Updated constructor to reflect changes in the Axis class (DG);
45  * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG);
46  * 14-Feb-2003 : Added back missing constructor code (DG);
47  * 26-Mar-2003 : Implemented Serializable (DG);
48  * 14-May-2003 : Renamed HorizontalSymbolicAxis --> SymbolicAxis and merged in
49  *               VerticalSymbolicAxis (DG);
50  * 12-Aug-2003 : Fixed bug where refreshTicks() method has different signature
51  *               to super class (DG);
52  * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);
53  * 02-Nov-2003 : Added code to avoid overlapping labels (MR);
54  * 07-Nov-2003 : Modified to use new tick classes (DG);
55  * 18-Nov-2003 : Fixed bug where symbols are not being displayed on the
56  *               axis (DG);
57  * 24-Nov-2003 : Added fix for gridlines on zooming (bug id 834643) (DG);
58  * 21-Jan-2004 : Update for renamed method in ValueAxis (DG);
59  * 11-Mar-2004 : Modified the way the background grid color is being drawn, see
60  *               this thread:
61  *               http://www.jfree.org/phpBB2/viewtopic.php?p=22973 (DG);
62  * 16-Mar-2004 : Added plotState to draw() method (DG);
63  * 07-Apr-2004 : Modified string bounds calculation (DG);
64  * 28-Mar-2005 : Renamed autoRangeIncludesZero() --> getAutoRangeIncludesZero()
65  *               and autoRangeStickyZero() --> getAutoRangeStickyZero() (DG);
66  * 05-Jul-2005 : Fixed signature on refreshTicks() method - see bug report
67  *               1232264 (DG);
68  * 06-Jul-2005 : Renamed SymbolicAxis --> SymbolAxis, added equals() method,
69  *               renamed getSymbolicValue() --> getSymbols(), renamed
70  *               symbolicGridPaint --> gridBandPaint, fixed serialization of
71  *               gridBandPaint, renamed symbolicGridLinesVisible -->
72  *               gridBandsVisible, eliminated symbolicGridLineList (DG);
73  * ------------- JFREECHART 1.0.x ---------------------------------------------
74  * 02-Feb-2007 : Removed author tags all over JFreeChart sources (DG);
75  * 28-Feb-2007 : Fixed bug 1669302 (tick label overlap) (DG);
76  * 25-Jul-2007 : Added new field for alternate grid band paint (DG);
77  * 15-Aug-2008 : Use alternate grid band paint when drawing (DG);
78  * 02-Jul-2013 : Use ParamChecks (DG);
79  *
80  */
81 
82 package org.jfree.chart.axis;
83 
84 import java.awt.BasicStroke;
85 import java.awt.Color;
86 import java.awt.Font;
87 import java.awt.Graphics2D;
88 import java.awt.Paint;
89 import java.awt.Shape;
90 import java.awt.Stroke;
91 import java.awt.geom.Rectangle2D;
92 import java.io.IOException;
93 import java.io.ObjectInputStream;
94 import java.io.ObjectOutputStream;
95 import java.io.Serializable;
96 import java.text.NumberFormat;
97 import java.util.Arrays;
98 import java.util.Iterator;
99 import java.util.List;
100 
101 import org.jfree.chart.event.AxisChangeEvent;
102 import org.jfree.chart.plot.Plot;
103 import org.jfree.chart.plot.PlotRenderingInfo;
104 import org.jfree.chart.plot.ValueAxisPlot;
105 import org.jfree.chart.util.ParamChecks;
106 import org.jfree.data.Range;
107 import org.jfree.io.SerialUtilities;
108 import org.jfree.text.TextUtilities;
109 import org.jfree.ui.RectangleEdge;
110 import org.jfree.ui.TextAnchor;
111 import org.jfree.util.PaintUtilities;
112 
113 /**
114  * A standard linear value axis that replaces integer values with symbols.
115  */
116 public 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      */
SymbolAxis(String label, String[] sv)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      */
getSymbols()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      */
isGridBandsVisible()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      */
setGridBandsVisible(boolean flag)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      */
getGridBandPaint()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      */
setGridBandPaint(Paint paint)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      */
getGridBandAlternatePaint()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      */
setGridBandAlternatePaint(Paint paint)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
selectAutoTickUnit(Graphics2D g2, Rectangle2D dataArea, RectangleEdge edge)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
draw(Graphics2D g2, double cursor, Rectangle2D plotArea, Rectangle2D dataArea, RectangleEdge edge, PlotRenderingInfo plotState)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      */
drawGridBands(Graphics2D g2, Rectangle2D plotArea, Rectangle2D dataArea, RectangleEdge edge, List ticks)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      */
drawGridBandsHorizontal(Graphics2D g2, Rectangle2D plotArea, Rectangle2D dataArea, boolean firstGridBandIsDark, List ticks)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      */
drawGridBandsVertical(Graphics2D g2, Rectangle2D drawArea, Rectangle2D plotArea, boolean firstGridBandIsDark, List ticks)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
autoAdjustRange()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
refreshTicks(Graphics2D g2, AxisState state, Rectangle2D dataArea, RectangleEdge edge)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
refreshTicksHorizontal(Graphics2D g2, Rectangle2D dataArea, RectangleEdge edge)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
refreshTicksVertical(Graphics2D g2, Rectangle2D dataArea, RectangleEdge edge)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      */
valueToString(double value)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
equals(Object obj)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      */
writeObject(ObjectOutputStream stream)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      */
readObject(ObjectInputStream stream)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 }
812