001/* ===========================================================
002 * Orson Charts : a 3D chart library for the Java(tm) platform
003 * ===========================================================
004 * 
005 * (C)opyright 2013-2022, by David Gilbert.  All rights reserved.
006 * 
007 * https://github.com/jfree/orson-charts
008 * 
009 * This program is free software: you can redistribute it and/or modify
010 * it under the terms of the GNU General Public License as published by
011 * the Free Software Foundation, either version 3 of the License, or
012 * (at your option) any later version.
013 *
014 * This program is distributed in the hope that it will be useful,
015 * but WITHOUT ANY WARRANTY; without even the implied warranty of
016 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
017 * GNU General Public License for more details.
018 *
019 * You should have received a copy of the GNU General Public License
020 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
021 * 
022 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
023 * Other names may be trademarks of their respective owners.]
024 * 
025 * If you do not wish to be bound by the terms of the GPL, an alternative
026 * commercial license can be purchased.  For details, please see visit the
027 * Orson Charts home page:
028 * 
029 * http://www.object-refinery.com/orsoncharts/index.html
030 * 
031 */
032
033package org.jfree.chart3d.axis;
034
035import java.awt.FontMetrics;
036import java.awt.Graphics2D;
037import java.awt.Shape;
038import java.awt.geom.Line2D;
039import java.awt.geom.Point2D;
040import java.awt.font.LineMetrics;
041import java.text.DecimalFormat;
042import java.text.Format;
043import java.io.Serializable;
044import java.util.ArrayList;
045import java.util.HashMap;
046import java.util.Map;
047import java.util.List;
048
049import org.jfree.chart3d.Chart3DHints;
050import org.jfree.chart3d.data.Range;
051import org.jfree.chart3d.graphics2d.TextAnchor;
052import org.jfree.chart3d.graphics3d.RenderedElement;
053import org.jfree.chart3d.graphics3d.RenderingInfo;
054import org.jfree.chart3d.graphics3d.internal.Utils2D;
055import org.jfree.chart3d.interaction.InteractiveElementType;
056import org.jfree.chart3d.internal.TextUtils;
057import org.jfree.chart3d.internal.Args;
058import org.jfree.chart3d.internal.ObjectUtils;
059import org.jfree.chart3d.plot.CategoryPlot3D;
060import org.jfree.chart3d.plot.XYZPlot;
061
062/**
063 * A numerical axis for use with 3D plots (implements {@link ValueAxis3D}).
064 * In a {@link CategoryPlot3D} the value axis (the vertical one) is numerical, 
065 * and in an {@link XYZPlot} all the axes (x, y and z) are numerical - for
066 * all these cases an instance of this class can be used.
067 * <br><br>
068 * NOTE: This class is serializable, but the serialization format is subject 
069 * to change in future releases and should not be relied upon for persisting 
070 * instances of this class. 
071 */
072public class NumberAxis3D extends AbstractValueAxis3D implements ValueAxis3D,
073        Serializable {
074
075    /** 
076     * Default formatter for axis number values. Can be overwritten.
077     */
078    private static final Format DEFAULT_TICK_LABEL_FORMATTER = new DecimalFormat("0.00");
079
080    /** 
081     * A flag indicating whether or not the auto-range calculation should
082     * include zero.
083     */
084    private boolean autoRangeIncludesZero;
085    
086    /**
087     * A flag that controls how zero is handled when it falls within the
088     * margins.  If {@code true}, the margin is truncated at zero, if
089     * {@code false} the margin is not changed.
090     */
091    private boolean autoRangeStickyZero;
092        
093    /** 
094     * The tick selector (if not {@code null}, then auto-tick selection is 
095     * used). 
096     */
097    private TickSelector tickSelector;
098
099    /** 
100     * The tick size.  If the tickSelector is not {@code null} then it is 
101     * used to auto-select an appropriate tick size and format.
102     */
103    private double tickSize;
104
105    /** The tick formatter (never {@code null}). */
106    private Format tickLabelFormatter;
107
108    /**
109     * Creates a new axis with the specified label and default attributes.
110     * 
111     * @param label  the axis label ({@code null} permitted). 
112     */
113    public NumberAxis3D(String label) {
114        this(label, new Range(0.0, 1.0));
115    }
116    
117    /**
118     * Creates a new axis with the specified label and range.
119     *
120     * @param label  the axis label ({@code null} permitted).
121     * @param range  the range ({@code null} not permitted).
122     */
123    public NumberAxis3D(String label, Range range) {
124        super(label, range);
125        this.autoRangeIncludesZero = false;
126        this.autoRangeStickyZero = true;
127        this.tickSelector = new NumberTickSelector();
128        this.tickSize = range.getLength() / 10.0;
129        this.tickLabelFormatter = DEFAULT_TICK_LABEL_FORMATTER;
130    }
131      
132    /**
133     * Returns the flag that determines whether or not the auto range 
134     * mechanism should force zero to be included in the range.  The default
135     * value is {@code false}.
136     * 
137     * @return A boolean.
138     */
139    public boolean getAutoRangeIncludesZero() {
140        return this.autoRangeIncludesZero;
141    }
142    
143    /**
144     * Sets the flag that controls whether or not the auto range mechanism 
145     * should force zero to be included in the axis range, and sends an
146     * {@link Axis3DChangeEvent} to all registered listeners.
147     * 
148     * @param include  the new flag value.
149     */
150    public void setAutoRangeIncludeZero(boolean include) {
151        this.autoRangeIncludesZero = include;
152        fireChangeEvent(true);
153    }
154    
155    /**
156     * Returns the flag that controls the behaviour of the auto range 
157     * mechanism when zero falls into the axis margins.  The default value
158     * is {@code true}.
159     * 
160     * @return A boolean. 
161     * 
162     * @see #setAutoRangeStickyZero(boolean) 
163     */
164    public boolean getAutoRangeStickyZero() {
165        return this.autoRangeStickyZero;
166    }
167    
168    /**
169     * Sets the flag that controls the behaviour of the auto range mechanism 
170     * when zero falls into the axis margins.  If {@code true}, when
171     * zero is in the axis margin the axis range is truncated at zero.  If
172     * {@code false}, there is no special treatment.
173     * 
174     * @param sticky  the new flag value. 
175     */
176    public void setAutoRangeStickyZero(boolean sticky) {
177        this.autoRangeStickyZero = sticky;
178        fireChangeEvent(true);
179    }
180  
181    /**
182     * Returns the tick selector, an object that is responsible for choosing
183     * standard tick units for the axis.  The default value is a default
184     * instance of {@link NumberTickSelector}.
185     * 
186     * @return The tick selector. 
187     * 
188     * @see #setTickSelector(TickSelector) 
189     */
190    public TickSelector getTickSelector() {
191        return this.tickSelector;    
192    }
193    
194    /**
195     * Sets the tick selector and sends an {@link Axis3DChangeEvent} to all
196     * registered listeners.
197     * 
198     * @param selector  the selector ({@code null} permitted).
199     * 
200     * @see #getTickSelector() 
201     */
202    public void setTickSelector(TickSelector selector) {
203        this.tickSelector = selector;
204        fireChangeEvent(false);
205    }
206    
207    /**
208     * Returns the tick size (to be used when the tick selector is 
209     * {@code null}).
210     * 
211     * @return The tick size.
212     */
213    public double getTickSize() {
214        return this.tickSize;
215    }
216
217    /**
218     * Sets the tick size and sends an {@link Axis3DChangeEvent} to all 
219     * registered listeners.
220     * 
221     * @param tickSize  the new tick size.
222     */
223    public void setTickSize(double tickSize) {
224        this.tickSize = tickSize;
225        fireChangeEvent(false);
226    }
227    
228    /**
229     * Returns the tick label formatter.  The default value is
230     * {@code DecimalFormat("0.00")}.
231     * 
232     * @return The tick label formatter (never {@code null}). 
233     */
234    public Format getTickLabelFormatter() {
235        return this.tickLabelFormatter;
236    }
237    
238    /**
239     * Sets the formatter for the tick labels and sends an 
240     * {@link Axis3DChangeEvent} to all registered listeners.
241     * 
242     * @param formatter  the formatter ({@code null} not permitted).
243     */
244    public void setTickLabelFormatter(Format formatter) {
245        Args.nullNotPermitted(formatter, "formatter");
246        this.tickLabelFormatter = formatter;
247        fireChangeEvent(false);
248    }
249    
250    /**
251     * Adjusts the range by adding the lower and upper margins and taking into
252     * account also the {@code autoRangeStickyZero} flag.
253     * 
254     * @param range  the range ({@code null} not permitted).
255     * 
256     * @return The adjusted range. 
257     */
258    @Override
259    protected Range adjustedDataRange(Range range) {
260        Args.nullNotPermitted(range, "range");
261        double lm = range.getLength() * getLowerMargin();
262        double um = range.getLength() * getUpperMargin();
263        double lowerBound = range.getMin() - lm;
264        double upperBound = range.getMax() + um;
265        if (this.autoRangeIncludesZero) {
266            lowerBound = Math.min(lowerBound, 0.0);
267            upperBound = Math.max(upperBound, 0.0);
268        }
269        // does zero fall in the margins?
270        if (this.autoRangeStickyZero) {
271            if (0.0 <= range.getMin() && 0.0 > lowerBound) {
272                lowerBound = 0.0;
273            }
274            if (0.0 >= range.getMax() && 0.0 < upperBound) {
275                upperBound = 0.0;
276            }
277        }
278        if ((upperBound - lowerBound) < getMinAutoRangeLength()) {
279            double adj = (getMinAutoRangeLength() - (upperBound - lowerBound)) 
280                    / 2.0;
281            lowerBound -= adj;
282            upperBound += adj;
283        }
284        return new Range(lowerBound, upperBound);
285    }
286    
287    /**
288     * Draws the axis to the supplied graphics target ({@code g2}, with the
289     * specified starting and ending points for the line.  This method is used
290     * internally, you should not need to call it directly.
291     *
292     * @param g2  the graphics target ({@code null} not permitted).
293     * @param pt0  the starting point ({@code null} not permitted).
294     * @param pt1  the ending point ({@code null} not permitted).
295     * @param opposingPt  an opposing point (to determine which side of the 
296     *     axis line the labels should appear, {@code null} not permitted).
297     * @param tickData  tick details ({@code null} not permitted).
298     * @param info  an object to be populated with rendering info 
299     *     ({@code null} permitted).
300     * @param hinting  perform element hinting?
301     */
302    @Override
303    public void draw(Graphics2D g2, Point2D pt0, Point2D pt1, 
304            Point2D opposingPt, List<TickData> tickData, RenderingInfo info,
305            boolean hinting) {
306        
307        if (!isVisible()) {
308            return;
309        }
310        if (pt0.equals(pt1)) {
311            return;
312        }
313        
314        // draw a line for the axis
315        g2.setStroke(getLineStroke());
316        g2.setPaint(getLineColor());
317        Line2D axisLine = new Line2D.Float(pt0, pt1);  
318        g2.draw(axisLine);
319        
320        // draw the tick marks and labels
321        g2.setFont(getTickLabelFont());
322        // we track the max width or height of the labels to know how far to
323        // offset the axis label when we draw it later
324        double maxTickLabelDim = 0.0;
325        if (getTickLabelOrientation().equals(LabelOrientation.PARALLEL)) {
326            LineMetrics lm = g2.getFontMetrics().getLineMetrics("123", g2);
327            maxTickLabelDim = lm.getHeight();
328        }
329        double tickMarkLength = getTickMarkLength();
330        double tickLabelOffset = getTickLabelOffset();
331        g2.setPaint(getTickMarkPaint());
332        g2.setStroke(getTickMarkStroke());
333        for (TickData t : tickData) {
334            if (tickMarkLength > 0.0) {
335                Line2D tickLine = Utils2D.createPerpendicularLine(axisLine, 
336                       t.getAnchorPt(), tickMarkLength, opposingPt);
337                g2.draw(tickLine);
338            }
339            String tickLabel = this.tickLabelFormatter.format(t.getDataValue());
340            if (getTickLabelOrientation().equals(
341                    LabelOrientation.PERPENDICULAR)) {
342                maxTickLabelDim = Math.max(maxTickLabelDim, 
343                        g2.getFontMetrics().stringWidth(tickLabel));
344            }
345        }
346            
347        if (getTickLabelsVisible()) {
348            g2.setPaint(getTickLabelColor());
349            if (getTickLabelOrientation().equals(
350                    LabelOrientation.PERPENDICULAR)) {
351                drawPerpendicularTickLabels(g2, axisLine, opposingPt, tickData,
352                        info, hinting);
353            } else {
354                drawParallelTickLabels(g2, axisLine, opposingPt, tickData, 
355                        info, hinting);
356            }
357        } else {
358            maxTickLabelDim = 0.0;
359        }
360
361        // draw the axis label (if any)...
362        if (getLabel() != null) {
363            Shape labelBounds = drawAxisLabel(getLabel(), g2, axisLine, 
364                    opposingPt, maxTickLabelDim + tickMarkLength 
365                    + tickLabelOffset + getLabelOffset(), info, hinting);
366        }
367    }
368    
369    /**
370     * Draws tick labels parallel to the axis.
371     * 
372     * @param g2  the graphics target ({@code null} not permitted).
373     * @param axisLine  the axis line ({@code null} not permitted).
374     * @param opposingPt  an opposing point (to determine on which side the 
375     *     labels appear, {@code null} not permitted).
376     * @param tickData  the tick data ({@code null} not permitted).
377     * @param info  if not {@code null} this object will be updated with
378     *     {@link RenderedElement} instances for each of the tick labels.
379     */
380    private void drawParallelTickLabels(Graphics2D g2, Line2D axisLine,
381            Point2D opposingPt, List<TickData> tickData, RenderingInfo info,
382            boolean hinting) {
383        
384        g2.setFont(getTickLabelFont());
385        double halfAscent = g2.getFontMetrics().getAscent() / 2.0;
386        for (TickData t : tickData) {
387            Line2D perpLine = Utils2D.createPerpendicularLine(axisLine, 
388                    t.getAnchorPt(), getTickMarkLength()
389                    + getTickLabelOffset() + halfAscent, opposingPt);
390            double axisTheta = Utils2D.calculateTheta(axisLine);
391            TextAnchor textAnchor = TextAnchor.CENTER;
392            if (axisTheta >= Math.PI / 2.0) {
393                axisTheta = axisTheta - Math.PI;
394            } else if (axisTheta <= -Math.PI / 2) {
395                axisTheta = axisTheta + Math.PI;  
396            }
397            String tickLabel = this.tickLabelFormatter.format(
398                    t.getDataValue());
399            if (hinting) {
400                Map<String, String> m = new HashMap<>();
401                m.put("ref", "{\"type\": \"valueTickLabel\", \"axis\": \"" 
402                        + axisStr() + "\", \"value\": \"" 
403                        + t.getDataValue() + "\"}");
404                g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);
405            }
406            Shape bounds = TextUtils.drawRotatedString(tickLabel, g2, 
407                    (float) perpLine.getX2(), (float) perpLine.getY2(), 
408                    textAnchor, axisTheta, textAnchor);
409            if (hinting) {
410                g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true);
411            }
412            if (info != null) {
413                RenderedElement tickLabelElement = new RenderedElement(
414                        InteractiveElementType.VALUE_AXIS_TICK_LABEL, bounds);
415                tickLabelElement.setProperty("axis", axisStr());
416                tickLabelElement.setProperty("value",  t.getDataValue());
417                info.addOffsetElement(tickLabelElement);
418            }
419        }
420    }
421    
422    /**
423     * Draws tick labels perpendicular to the axis.
424     * 
425     * @param g2  the graphics target ({@code null} not permitted).
426     * @param axisLine  the axis line ({@code null} not permitted).
427     * @param opposingPt  an opposing point (to determine on which side the 
428     *     labels appear, {@code null} not permitted).
429     * @param tickData  the tick data ({@code null} not permitted).
430     * @param info  if not {@code null} this object will be updated with
431     *     {@link RenderedElement} instances for each of the tick labels.
432     */
433    private void drawPerpendicularTickLabels(Graphics2D g2, Line2D axisLine,
434            Point2D opposingPt, List<TickData> tickData, RenderingInfo info,
435            boolean hinting) {
436        for (TickData t : tickData) {
437            double theta = Utils2D.calculateTheta(axisLine);
438            double thetaAdj = theta + Math.PI / 2.0;
439            if (thetaAdj < -Math.PI / 2.0) {
440                thetaAdj = thetaAdj + Math.PI;
441            }
442            if (thetaAdj > Math.PI / 2.0) {
443                thetaAdj = thetaAdj - Math.PI;
444            }
445
446            Line2D perpLine = Utils2D.createPerpendicularLine(axisLine, 
447                    t.getAnchorPt(), getTickMarkLength() + getTickLabelOffset(), 
448                    opposingPt);
449            double perpTheta = Utils2D.calculateTheta(perpLine);  
450            TextAnchor textAnchor = TextAnchor.CENTER_LEFT;
451            if (Math.abs(perpTheta) > Math.PI / 2.0) {
452                textAnchor = TextAnchor.CENTER_RIGHT;
453            } 
454            String tickLabel = this.tickLabelFormatter.format(
455                    t.getDataValue());
456            if (hinting) {
457                Map<String, String> m = new HashMap<>();
458                m.put("ref", "{\"type\": \"valueTickLabel\", \"axis\": \"" 
459                        + axisStr() + "\", \"value\": \"" 
460                        + t.getDataValue() + "\"}");
461                g2.setRenderingHint(Chart3DHints.KEY_BEGIN_ELEMENT, m);
462            }
463            Shape bounds = TextUtils.drawRotatedString(tickLabel, g2, 
464                    (float) perpLine.getX2(), (float) perpLine.getY2(), 
465                    textAnchor, thetaAdj, textAnchor);
466            if (hinting) {
467                g2.setRenderingHint(Chart3DHints.KEY_END_ELEMENT, true);
468            }
469            if (info != null) {
470                RenderedElement tickLabelElement = new RenderedElement(
471                        InteractiveElementType.VALUE_AXIS_TICK_LABEL, bounds);
472                tickLabelElement.setProperty("axis", axisStr());
473                tickLabelElement.setProperty("value", t.getDataValue());
474                info.addOffsetElement(tickLabelElement);
475            }
476        }   
477    }
478    
479    /**
480     * Converts a data value to world coordinates, taking into account the
481     * current axis range (assumes the world axis is zero-based and has the
482     * specified length).
483     * 
484     * @param value  the data value (in axis units).
485     * @param length  the length of the (zero based) world axis.
486     * 
487     * @return A world coordinate.
488     */
489    @Override
490    public double translateToWorld(double value, double length) {
491        double p = getRange().percent(value, isInverted());
492        return length * p;
493    }
494  
495    /**
496     * Selects a tick size that is appropriate for drawing the axis from
497     * {@code pt0} to {@code pt1}.
498     * 
499     * @param g2  the graphics target ({@code null} not permitted).
500     * @param pt0  the starting point for the axis.
501     * @param pt1  the ending point for the axis.
502     * @param opposingPt  a point on the opposite side of the line from where
503     *     the labels should be drawn.
504     */
505    @Override
506    public double selectTick(Graphics2D g2, Point2D pt0, Point2D pt1, 
507            Point2D opposingPt) {
508        
509        if (this.tickSelector == null) {
510            return this.tickSize;
511        }
512        g2.setFont(getTickLabelFont()); 
513        FontMetrics fm = g2.getFontMetrics(getTickLabelFont());        
514        double length = pt0.distance(pt1);
515        LabelOrientation orientation = getTickLabelOrientation();
516        if (orientation.equals(LabelOrientation.PERPENDICULAR)) {
517            // based on the font height, we can determine roughly how many tick
518            // labels will fit in the length available
519            double height = fm.getHeight();
520            // the tickLabelFactor allows some control over how dense the labels
521            // will be
522            int maxTicks = (int) (length / (height * getTickLabelFactor()));
523            if (maxTicks > 2 && this.tickSelector != null) {
524                double rangeLength = getRange().getLength();
525                this.tickSelector.select(rangeLength / 2.0);
526                // step through until we have too many ticks OR we run out of 
527                // tick sizes
528                int tickCount = (int) (rangeLength 
529                        / this.tickSelector.getCurrentTickSize());
530                while (tickCount < maxTicks) {
531                    this.tickSelector.previous();
532                    tickCount = (int) (rangeLength
533                            / this.tickSelector.getCurrentTickSize());
534                }
535                this.tickSelector.next();
536                this.tickSize = this.tickSelector.getCurrentTickSize();
537                // TFE, 20180911: don't overwrite any formatter explicitly set
538                if (DEFAULT_TICK_LABEL_FORMATTER.equals(this.tickLabelFormatter)) {
539                    this.tickLabelFormatter 
540                            = this.tickSelector.getCurrentTickLabelFormat();
541                }
542            } else {
543                this.tickSize = Double.NaN;
544            }
545        } else if (orientation.equals(LabelOrientation.PARALLEL)) {
546            // choose a unit that is at least as large as the length of the axis
547            this.tickSelector.select(getRange().getLength());
548            boolean done = false;
549            while (!done) {
550                if (this.tickSelector.previous()) {
551                    // estimate the label widths, and do they overlap?
552                    Format f = this.tickSelector.getCurrentTickLabelFormat();
553                    String s0 = f.format(this.range.getMin());
554                    String s1 = f.format(this.range.getMax());
555                    double w0 = fm.stringWidth(s0);
556                    double w1 = fm.stringWidth(s1);
557                    double w = Math.max(w0, w1);
558                    int n = (int) (length / (w * this.getTickLabelFactor()));
559                    if (n < getRange().getLength() 
560                            / tickSelector.getCurrentTickSize()) {
561                        tickSelector.next();
562                        done = true;
563                    }
564                } else {
565                    done = true;
566                }
567            }
568            this.tickSize = this.tickSelector.getCurrentTickSize();
569            // TFE, 20180911: don't overwrite any formatter explicitly set
570            if (DEFAULT_TICK_LABEL_FORMATTER.equals(this.tickLabelFormatter)) {
571                this.tickLabelFormatter 
572                        = this.tickSelector.getCurrentTickLabelFormat();
573            }
574        }
575        return this.tickSize;
576    }
577
578    /**
579     * Generates a list of tick data items for the specified tick unit.  This
580     * data will be passed to the 3D engine and will be updated with a 2D
581     * projection that can later be used to write the axis tick labels in the
582     * appropriate places.
583     * <br><br>
584     * If {@code tickUnit} is {@code Double.NaN}, then tick data is
585     * generated for just the bounds of the axis.
586     * 
587     * @param tickUnit  the tick unit.
588     * 
589     * @return A list of tick data (never {@code null}). 
590     */
591    @Override
592    public List<TickData> generateTickData(double tickUnit) {
593        List<TickData> result = new ArrayList<>();
594        if (Double.isNaN(tickUnit)) {
595            result.add(new TickData(0, getRange().getMin()));
596            result.add(new TickData(1, getRange().getMax()));
597        } else {
598            double x = tickUnit * Math.ceil(this.range.getMin() / tickUnit);
599            while (x <= this.range.getMax()) {
600                result.add(new TickData(this.range.percent(x, isInverted()), 
601                        x));
602                x += tickUnit;
603            }
604        }
605        return result;
606    }
607
608    /**
609     * Tests this instance for equality with an arbitrary object.
610     * 
611     * @param obj  the object to test against ({@code null} permitted).
612     * 
613     * @return A boolean. 
614     */
615    @Override
616    public boolean equals(Object obj) {
617        if (obj == this) {
618            return true;
619        }
620        if (!(obj instanceof NumberAxis3D)) {
621            return false;
622        }
623        NumberAxis3D that = (NumberAxis3D) obj;
624        if (this.autoRangeIncludesZero != that.autoRangeIncludesZero) {
625            return false;
626        }
627        if (this.autoRangeStickyZero != that.autoRangeStickyZero) {
628            return false;
629        }
630        if (this.tickSize != that.tickSize) {
631            return false;
632        }
633        if (!ObjectUtils.equals(this.tickSelector, that.tickSelector)) {
634            return false;
635        }
636        if (!this.tickLabelFormatter.equals(that.tickLabelFormatter)) {
637            return false;
638        }
639        return super.equals(obj);
640    }
641
642    /**
643     * Returns a hash code for this instance.
644     * 
645     * @return A hash code. 
646     */
647    @Override
648    public int hashCode() {
649        int hash = 3;
650        hash = 59 * hash + (int) (Double.doubleToLongBits(this.tickSize) 
651                ^ (Double.doubleToLongBits(this.tickSize) >>> 32));
652        hash = 59 * hash + ObjectUtils.hashCode(this.tickLabelFormatter);
653        return hash;
654    }
655    
656}