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