    001    /* ===========================================================
    002     * JFreeChart : a free chart library for the Java(tm) platform
    003     * ===========================================================
    004     *
    005     * (C) Copyright 2000-2011, 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     * LogAxis.java
    029     * ------------
    030     * (C) Copyright 2006-2009, by Object Refinery Limited and Contributors.
    031     *
    032     * Original Author:  David Gilbert (for Object Refinery Limited);
    033     * Contributor(s):   Andrew Mickish (patch 1868745);
    034     *                   Peter Kolb (patches 1934255 and 2603321);
    035     *
    036     * Changes
    037     * -------
    038     * 24-Aug-2006 : Version 1 (DG);
    039     * 22-Mar-2007 : Use defaultAutoArrange attribute (DG);
    040     * 02-Aug-2007 : Fixed zooming bug, added support for margins (DG);
    041     * 14-Feb-2008 : Changed default minorTickCount to 9 - see bug report
    042     *               1892419 (DG);
    043     * 15-Feb-2008 : Applied a variation of patch 1868745 by Andrew Mickish to
    044     *               fix a labelling bug when the axis appears at the top or
    045     *               right of the chart (DG);
    046     * 19-Mar-2008 : Applied patch 1902418 by Andrew Mickish to fix bug in tick
    047     *               labels for vertical axis (DG);
    048     * 26-Mar-2008 : Changed createTickLabel() method from private to protected -
    049     *               see patch 1918209 by Andrew Mickish (DG);
    050     * 25-Sep-2008 : Moved minor tick fields up to superclass, see patch 1934255
    051     *               by Peter Kolb (DG);
    052     * 14-Jan-2009 : Fetch minor ticks from TickUnit, and corrected
    053     *               createLogTickUnits() (DG);
    054     * 21-Jan-2009 : No need to call setMinorTickCount() in constructor (DG);
    055     * 19-Mar-2009 : Added entity support - see patch 2603321 by Peter Kolb (DG);
    056     * 30-Mar-2009 : Added pan(double) method (DG);
    057     * 28-Oct-2011 : Fixed endless loop for 0 TickUnit, # 3429707 (MH);
    058     *
    059     */
    060
    061    package org.jfree.chart.axis;
    062
    063    import java.awt.Font;
    064    import java.awt.FontMetrics;
    065    import java.awt.Graphics2D;
    066    import java.awt.font.FontRenderContext;
    067    import java.awt.font.LineMetrics;
    068    import java.awt.geom.Rectangle2D;
    069    import java.text.DecimalFormat;
    070    import java.text.NumberFormat;
    071    import java.util.ArrayList;
    072    import java.util.List;
    073    import java.util.Locale;
    074
    075    import org.jfree.chart.event.AxisChangeEvent;
    076    import org.jfree.chart.plot.Plot;
    077    import org.jfree.chart.plot.PlotRenderingInfo;
    078    import org.jfree.chart.plot.ValueAxisPlot;
    079    import org.jfree.chart.util.LogFormat;
    080    import org.jfree.data.Range;
    081    import org.jfree.ui.RectangleEdge;
    082    import org.jfree.ui.RectangleInsets;
    083    import org.jfree.ui.TextAnchor;
    084
    085    /**
    086     * A numerical axis that uses a logarithmic scale.  The class is an
    087     * alternative to the {@link LogarithmicAxis} class.
    088     *
    089     * @since 1.0.7
    090     */
    091    public class LogAxis extends ValueAxis {
    092
    093        /** The logarithm base. */
    094        private double base = 10.0;
    095
    096        /** The logarithm of the base value - cached for performance. */
    097        private double baseLog = Math.log(10.0);
    098
    099        /**  The smallest value permitted on the axis. */
    100        private double smallestValue = 1E-100;
    101
    102        /** The current tick unit. */
    103        private NumberTickUnit tickUnit;
    104
    105        /** The override number format. */
    106        private NumberFormat numberFormatOverride;
    107
    108        /**
    109         * Creates a new <code>LogAxis</code> with no label.
    110         */
    111        public LogAxis() {
    112            this(null);
    113        }
    114
    115        /**
    116         * Creates a new <code>LogAxis</code> with the given label.
    117         *
    118         * @param label  the axis label (<code>null</code> permitted).
    119         */
    120        public LogAxis(String label) {
    121            super(label, createLogTickUnits(Locale.getDefault()));
    122            setDefaultAutoRange(new Range(0.01, 1.0));
    123            this.tickUnit = new NumberTickUnit(1.0, new DecimalFormat("0.#"), 9);
    124        }
    125
    126        /**
    127         * Returns the base for the logarithm calculation.
    128         *
    129         * @return The base for the logarithm calculation.
    130         *
    131         * @see #setBase(double)
    132         */
    133        public double getBase() {
    134            return this.base;
    135        }
    136
    137        /**
    138         * Sets the base for the logarithm calculation and sends an
    139         * {@link AxisChangeEvent} to all registered listeners.
    140         *
    141         * @param base  the base value (must be > 1.0).
    142         *
    143         * @see #getBase()
    144         */
    145        public void setBase(double base) {
    146            if (base <= 1.0) {
    147                throw new IllegalArgumentException("Requires 'base' > 1.0.");
    148            }
    149            this.base = base;
    150            this.baseLog = Math.log(base);
    151            notifyListeners(new AxisChangeEvent(this));
    152        }
    153
    154        /**
    155         * Returns the smallest value represented by the axis.
    156         *
    157         * @return The smallest value represented by the axis.
    158         *
    159         * @see #setSmallestValue(double)
    160         */
    161        public double getSmallestValue() {
    162            return this.smallestValue;
    163        }
    164
    165        /**
    166         * Sets the smallest value represented by the axis and sends an
    167         * {@link AxisChangeEvent} to all registered listeners.
    168         *
    169         * @param value  the value.
    170         *
    171         * @see #getSmallestValue()
    172         */
    173        public void setSmallestValue(double value) {
    174            if (value <= 0.0) {
    175                throw new IllegalArgumentException("Requires 'value' > 0.0.");
    176            }
    177            this.smallestValue = value;
    178            notifyListeners(new AxisChangeEvent(this));
    179        }
    180
    181        /**
    182         * Returns the current tick unit.
    183         *
    184         * @return The current tick unit.
    185         *
    186         * @see #setTickUnit(NumberTickUnit)
    187         */
    188        public NumberTickUnit getTickUnit() {
    189            return this.tickUnit;
    190        }
    191
    192        /**
    193         * Sets the tick unit for the axis and sends an {@link AxisChangeEvent} to
    194         * all registered listeners.  A side effect of calling this method is that
    195         * the "auto-select" feature for tick units is switched off (you can
    196         * restore it using the {@link ValueAxis#setAutoTickUnitSelection(boolean)}
    197         * method).
    198         *
    199         * @param unit  the new tick unit (<code>null</code> not permitted).
    200         *
    201         * @see #getTickUnit()
    202         */
    203        public void setTickUnit(NumberTickUnit unit) {
    204            // defer argument checking...
    205            setTickUnit(unit, true, true);
    206        }
    207
    208        /**
    209         * Sets the tick unit for the axis and, if requested, sends an
    210         * {@link AxisChangeEvent} to all registered listeners.  In addition, an
    211         * option is provided to turn off the "auto-select" feature for tick units
    212         * (you can restore it using the
    213         * {@link ValueAxis#setAutoTickUnitSelection(boolean)} method).
    214         *
    215         * @param unit  the new tick unit (<code>null</code> not permitted).
    216         * @param notify  notify listeners?
    217         * @param turnOffAutoSelect  turn off the auto-tick selection?
    218         *
    219         * @see #getTickUnit()
    220         */
    221        public void setTickUnit(NumberTickUnit unit, boolean notify,
    222                                boolean turnOffAutoSelect) {
    223
    224            if (unit == null) {
    225                throw new IllegalArgumentException("Null 'unit' argument.");
    226            }
    227            this.tickUnit = unit;
    228            if (turnOffAutoSelect) {
    229                setAutoTickUnitSelection(false, false);
    230            }
    231            if (notify) {
    232                notifyListeners(new AxisChangeEvent(this));
    233            }
    234
    235        }
    236
    237        /**
    238         * Returns the number format override.  If this is non-null, then it will
    239         * be used to format the numbers on the axis.
    240         *
    241         * @return The number formatter (possibly <code>null</code>).
    242         *
    243         * @see #setNumberFormatOverride(NumberFormat)
    244         */
    245        public NumberFormat getNumberFormatOverride() {
    246            return this.numberFormatOverride;
    247        }
    248
    249        /**
    250         * Sets the number format override.  If this is non-null, then it will be
    251         * used to format the numbers on the axis.
    252         *
    253         * @param formatter  the number formatter (<code>null</code> permitted).
    254         *
    255         * @see #getNumberFormatOverride()
    256         */
    257        public void setNumberFormatOverride(NumberFormat formatter) {
    258            this.numberFormatOverride = formatter;
    259            notifyListeners(new AxisChangeEvent(this));
    260        }
    261
    262        /**
    263         * Calculates the log of the given value, using the current base.
    264         *
    265         * @param value  the value.
    266         *
    267         * @return The log of the given value.
    268         *
    269         * @see #calculateValue(double)
    270         * @see #getBase()
    271         */
    272        public double calculateLog(double value) {
    273            return Math.log(value) / this.baseLog;
    274        }
    275
    276        /**
    277         * Calculates the value from a given log.
    278         *
    279         * @param log  the log value (must be > 0.0).
    280         *
    281         * @return The value with the given log.
    282         *
    283         * @see #calculateLog(double)
    284         * @see #getBase()
    285         */
    286        public double calculateValue(double log) {
    287            return Math.pow(this.base, log);
    288        }
    289
    290        /**
    291         * Converts a Java2D coordinate to an axis value, assuming that the
    292         * axis covers the specified <code>edge</code> of the <code>area</code>.
    293         *
    294         * @param java2DValue  the Java2D coordinate.
    295         * @param area  the area.
    296         * @param edge  the edge that the axis belongs to.
    297         *
    298         * @return A value along the axis scale.
    299         */
    300        public double java2DToValue(double java2DValue, Rectangle2D area,
    301                RectangleEdge edge) {
    302
    303            Range range = getRange();
    304            double axisMin = calculateLog(range.getLowerBound());
    305            double axisMax = calculateLog(range.getUpperBound());
    306
    307            double min = 0.0;
    308            double max = 0.0;
    309            if (RectangleEdge.isTopOrBottom(edge)) {
    310                min = area.getX();
    311                max = area.getMaxX();
    312            }
    313            else if (RectangleEdge.isLeftOrRight(edge)) {
    314                min = area.getMaxY();
    315                max = area.getY();
    316            }
    317            double log = 0.0;
    318            if (isInverted()) {
    319                log = axisMax - (java2DValue - min) / (max - min)
    320                        * (axisMax - axisMin);
    321            }
    322            else {
    323                log = axisMin + (java2DValue - min) / (max - min)
    324                        * (axisMax - axisMin);
    325            }
    326            return calculateValue(log);
    327        }
    328
    329        /**
    330         * Converts a value on the axis scale to a Java2D coordinate relative to
    331         * the given <code>area</code>, based on the axis running along the
    332         * specified <code>edge</code>.
    333         *
    334         * @param value  the data value.
    335         * @param area  the area.
    336         * @param edge  the edge.
    337         *
    338         * @return The Java2D coordinate corresponding to <code>value</code>.
    339         */
    340        public double valueToJava2D(double value, Rectangle2D area,
    341                RectangleEdge edge) {
    342
    343            Range range = getRange();
    344            double axisMin = calculateLog(range.getLowerBound());
    345            double axisMax = calculateLog(range.getUpperBound());
    346            value = calculateLog(value);
    347
    348            double min = 0.0;
    349            double max = 0.0;
    350            if (RectangleEdge.isTopOrBottom(edge)) {
    351                min = area.getX();
    352                max = area.getMaxX();
    353            }
    354            else if (RectangleEdge.isLeftOrRight(edge)) {
    355                max = area.getMinY();
    356                min = area.getMaxY();
    357            }
    358            if (isInverted()) {
    359                return max
    360                       - ((value - axisMin) / (axisMax - axisMin)) * (max - min);
    361            }
    362            else {
    363                return min
    364                       + ((value - axisMin) / (axisMax - axisMin)) * (max - min);
    365            }
    366        }
    367
    368        /**
    369         * Configures the axis.  This method is typically called when an axis
    370         * is assigned to a new plot.
    371         */
    372        public void configure() {
    373            if (isAutoRange()) {
    374                autoAdjustRange();
    375            }
    376        }
    377
    378        /**
    379         * Adjusts the axis range to match the data range that the axis is
    380         * required to display.
    381         */
    382        protected void autoAdjustRange() {
    383            Plot plot = getPlot();
    384            if (plot == null) {
    385                return;  // no plot, no data
    386            }
    387
    388            if (plot instanceof ValueAxisPlot) {
    389                ValueAxisPlot vap = (ValueAxisPlot) plot;
    390
    391                Range r = vap.getDataRange(this);
    392                if (r == null) {
    393                    r = getDefaultAutoRange();
    394                }
    395
    396                double upper = r.getUpperBound();
    397                double lower = Math.max(r.getLowerBound(), this.smallestValue);
    398                double range = upper - lower;
    399
    400                // if fixed auto range, then derive lower bound...
    401                double fixedAutoRange = getFixedAutoRange();
    402                if (fixedAutoRange > 0.0) {
    403                    lower = Math.max(upper - fixedAutoRange, this.smallestValue);
    404                }
    405                else {
    406                    // ensure the autorange is at least <minRange> in size...
    407                    double minRange = getAutoRangeMinimumSize();
    408                    if (range < minRange) {
    409                        double expand = (minRange - range) / 2;
    410                        upper = upper + expand;
    411                        lower = lower - expand;
    412                    }
    413
    414                    // apply the margins - these should apply to the exponent range
    415                    double logUpper = calculateLog(upper);
    416                    double logLower = calculateLog(lower);
    417                    double logRange = logUpper - logLower;
    418                    logUpper = logUpper + getUpperMargin() * logRange;
    419                    logLower = logLower - getLowerMargin() * logRange;
    420                    upper = calculateValue(logUpper);
    421                    lower = calculateValue(logLower);
    422                }
    423
    424                setRange(new Range(lower, upper), false, false);
    425            }
    426
    427        }
    428
    429        /**
    430         * Draws the axis on a Java 2D graphics device (such as the screen or a
    431         * printer).
    432         *
    433         * @param g2  the graphics device (<code>null</code> not permitted).
    434         * @param cursor  the cursor location (determines where to draw the axis).
    435         * @param plotArea  the area within which the axes and plot should be drawn.
    436         * @param dataArea  the area within which the data should be drawn.
    437         * @param edge  the axis location (<code>null</code> not permitted).
    438         * @param plotState  collects information about the plot
    439         *                   (<code>null</code> permitted).
    440         *
    441         * @return The axis state (never <code>null</code>).
    442         */
    443        public AxisState draw(Graphics2D g2, double cursor, Rectangle2D plotArea,
    444                Rectangle2D dataArea, RectangleEdge edge,
    445                PlotRenderingInfo plotState) {
    446
    447            AxisState state = null;
    448            // if the axis is not visible, don't draw it...
    449            if (!isVisible()) {
    450                state = new AxisState(cursor);
    451                // even though the axis is not visible, we need ticks for the
    452                // gridlines...
    453                List ticks = refreshTicks(g2, state, dataArea, edge);
    454                state.setTicks(ticks);
    455                return state;
    456            }
    457            state = drawTickMarksAndLabels(g2, cursor, plotArea, dataArea, edge);
    458            state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state);
    459            createAndAddEntity(cursor, state, dataArea, edge, plotState);
    460            return state;
    461        }
    462
    463        /**
    464         * Calculates the positions of the tick labels for the axis, storing the
    465         * results in the tick label list (ready for drawing).
    466         *
    467         * @param g2  the graphics device.
    468         * @param state  the axis state.
    469         * @param dataArea  the area in which the plot should be drawn.
    470         * @param edge  the location of the axis.
    471         *
    472         * @return A list of ticks.
    473         *
    474         */
    475        public List refreshTicks(Graphics2D g2, AxisState state,
    476                Rectangle2D dataArea, RectangleEdge edge) {
    477
    478            List result = new java.util.ArrayList();
    479            if (RectangleEdge.isTopOrBottom(edge)) {
    480                result = refreshTicksHorizontal(g2, dataArea, edge);
    481            }
    482            else if (RectangleEdge.isLeftOrRight(edge)) {
    483                result = refreshTicksVertical(g2, dataArea, edge);
    484            }
    485            return result;
    486
    487        }
    488
    489        /**
    490         * Returns a list of ticks for an axis at the top or bottom of the chart.
    491         *
    492         * @param g2  the graphics device.
    493         * @param dataArea  the data area.
    494         * @param edge  the edge.
    495         *
    496         * @return A list of ticks.
    497         */
    498        protected List refreshTicksHorizontal(Graphics2D g2, Rectangle2D dataArea,
    499                RectangleEdge edge) {
    500
    501            Range range = getRange();
    502            List ticks = new ArrayList();
    503            Font tickLabelFont = getTickLabelFont();
    504            g2.setFont(tickLabelFont);
    505            TextAnchor textAnchor;
    506            if (edge == RectangleEdge.TOP) {
    507                textAnchor = TextAnchor.BOTTOM_CENTER;
    508            }
    509            else {
    510                textAnchor = TextAnchor.TOP_CENTER;
    511            }
    512
    513            if (isAutoTickUnitSelection()) {
    514                selectAutoTickUnit(g2, dataArea, edge);
    515            }
    516            int minorTickCount = this.tickUnit.getMinorTickCount();
    517            double start = Math.floor(calculateLog(getLowerBound()));
    518            double end = Math.ceil(calculateLog(getUpperBound()));
    519            double current = start;
    520            boolean hasTicks = (this.tickUnit.getSize() > 0.0)
    521                               && !Double.isInfinite(start);
    522            while (hasTicks && current <= end) {
    523                double v = calculateValue(current);
    524                if (range.contains(v)) {
    525                    ticks.add(new NumberTick(TickType.MAJOR, v, createTickLabel(v),
    526                            textAnchor, TextAnchor.CENTER, 0.0));
    527                }
    528                // add minor ticks (for gridlines)
    529                double next = Math.pow(this.base, current
    530                        + this.tickUnit.getSize());
    531                for (int i = 1; i < minorTickCount; i++) {
    532                    double minorV = v + i * ((next - v) / minorTickCount);
    533                    if (range.contains(minorV)) {
    534                        ticks.add(new NumberTick(TickType.MINOR, minorV, "",
    535                                textAnchor, TextAnchor.CENTER, 0.0));
    536                    }
    537                }
    538                current = current + this.tickUnit.getSize();
    539            }
    540            return ticks;
    541        }
    542
    543        /**
    544         * Returns a list of ticks for an axis at the left or right of the chart.
    545         *
    546         * @param g2  the graphics device.
    547         * @param dataArea  the data area.
    548         * @param edge  the edge.
    549         *
    550         * @return A list of ticks.
    551         */
    552        protected List refreshTicksVertical(Graphics2D g2, Rectangle2D dataArea,
    553                RectangleEdge edge) {
    554
    555            Range range = getRange();
    556            List ticks = new ArrayList();
    557            Font tickLabelFont = getTickLabelFont();
    558            g2.setFont(tickLabelFont);
    559            TextAnchor textAnchor;
    560            if (edge == RectangleEdge.RIGHT) {
    561                textAnchor = TextAnchor.CENTER_LEFT;
    562            }
    563            else {
    564                textAnchor = TextAnchor.CENTER_RIGHT;
    565            }
    566
    567            if (isAutoTickUnitSelection()) {
    568                selectAutoTickUnit(g2, dataArea, edge);
    569            }
    570            int minorTickCount = this.tickUnit.getMinorTickCount();
    571            double start = Math.floor(calculateLog(getLowerBound()));
    572            double end = Math.ceil(calculateLog(getUpperBound()));
    573            double current = start;
    574            boolean hasTicks = (this.tickUnit.getSize() > 0.0)
    575                               && !Double.isInfinite(start);
    576            while (hasTicks && current <= end) {
    577                double v = calculateValue(current);
    578                if (range.contains(v)) {
    579                    ticks.add(new NumberTick(TickType.MAJOR, v, createTickLabel(v),
    580                            textAnchor, TextAnchor.CENTER, 0.0));
    581                }
    582                // add minor ticks (for gridlines)
    583                double next = Math.pow(this.base, current
    584                        + this.tickUnit.getSize());
    585                for (int i = 1; i < minorTickCount; i++) {
    586                    double minorV = v + i * ((next - v) / minorTickCount);
    587                    if (range.contains(minorV)) {
    588                        ticks.add(new NumberTick(TickType.MINOR, minorV, "",
    589                                textAnchor, TextAnchor.CENTER, 0.0));
    590                    }
    591                }
    592                current = current + this.tickUnit.getSize();
    593            }
    594            return ticks;
    595        }
    596
    597        /**
    598         * Selects an appropriate tick value for the axis.  The strategy is to
    599         * display as many ticks as possible (selected from an array of 'standard'
    600         * tick units) without the labels overlapping.
    601         *
    602         * @param g2  the graphics device.
    603         * @param dataArea  the area defined by the axes.
    604         * @param edge  the axis location.
    605         *
    606         * @since 1.0.7
    607         */
    608        protected void selectAutoTickUnit(Graphics2D g2, Rectangle2D dataArea,
    609                RectangleEdge edge) {
    610
    611            if (RectangleEdge.isTopOrBottom(edge)) {
    612                selectHorizontalAutoTickUnit(g2, dataArea, edge);
    613            }
    614            else if (RectangleEdge.isLeftOrRight(edge)) {
    615                selectVerticalAutoTickUnit(g2, dataArea, edge);
    616            }
    617
    618        }
    619
    620        /**
    621         * Selects an appropriate tick value for the axis.  The strategy is to
    622         * display as many ticks as possible (selected from an array of 'standard'
    623         * tick units) without the labels overlapping.
    624         *
    625         * @param g2  the graphics device.
    626         * @param dataArea  the area defined by the axes.
    627         * @param edge  the axis location.
    628         *
    629         * @since 1.0.7
    630         */
    631       protected void selectHorizontalAutoTickUnit(Graphics2D g2,
    632               Rectangle2D dataArea, RectangleEdge edge) {
    633
    634            double tickLabelWidth = estimateMaximumTickLabelWidth(g2,
    635                    getTickUnit());
    636
    637            // start with the current tick unit...
    638            TickUnitSource tickUnits = getStandardTickUnits();
    639            TickUnit unit1 = tickUnits.getCeilingTickUnit(getTickUnit());
    640            double unit1Width = exponentLengthToJava2D(unit1.getSize(), dataArea,
    641                    edge);
    642
    643            // then extrapolate...
    644            double guess = (tickLabelWidth / unit1Width) * unit1.getSize();
    645
    646            NumberTickUnit unit2 = (NumberTickUnit)
    647                    tickUnits.getCeilingTickUnit(guess);
    648            double unit2Width = exponentLengthToJava2D(unit2.getSize(), dataArea,
    649                    edge);
    650
    651            tickLabelWidth = estimateMaximumTickLabelWidth(g2, unit2);
    652            if (tickLabelWidth > unit2Width) {
    653                unit2 = (NumberTickUnit) tickUnits.getLargerTickUnit(unit2);
    654            }
    655
    656            setTickUnit(unit2, false, false);
    657
    658        }
    659
    660        /**
    661         * Converts a length in data coordinates into the corresponding length in
    662         * Java2D coordinates.
    663         *
    664         * @param length  the length.
    665         * @param area  the plot area.
    666         * @param edge  the edge along which the axis lies.
    667         *
    668         * @return The length in Java2D coordinates.
    669         *
    670         * @since 1.0.7
    671         */
    672        public double exponentLengthToJava2D(double length, Rectangle2D area,
    673                                    RectangleEdge edge) {
    674            double one = valueToJava2D(calculateValue(1.0), area, edge);
    675            double l = valueToJava2D(calculateValue(length + 1.0), area, edge);
    676            return Math.abs(l - one);
    677        }
    678
    679        /**
    680         * Selects an appropriate tick value for the axis.  The strategy is to
    681         * display as many ticks as possible (selected from an array of 'standard'
    682         * tick units) without the labels overlapping.
    683         *
    684         * @param g2  the graphics device.
    685         * @param dataArea  the area in which the plot should be drawn.
    686         * @param edge  the axis location.
    687         *
    688         * @since 1.0.7
    689         */
    690        protected void selectVerticalAutoTickUnit(Graphics2D g2,
    691                                                  Rectangle2D dataArea,
    692                                                  RectangleEdge edge) {
    693
    694            double tickLabelHeight = estimateMaximumTickLabelHeight(g2);
    695
    696            // start with the current tick unit...
    697            TickUnitSource tickUnits = getStandardTickUnits();
    698            TickUnit unit1 = tickUnits.getCeilingTickUnit(getTickUnit());
    699            double unitHeight = exponentLengthToJava2D(unit1.getSize(), dataArea,
    700                    edge);
    701
    702            // then extrapolate...
    703            double guess = (tickLabelHeight / unitHeight) * unit1.getSize();
    704
    705            NumberTickUnit unit2 = (NumberTickUnit)
    706                    tickUnits.getCeilingTickUnit(guess);
    707            double unit2Height = exponentLengthToJava2D(unit2.getSize(), dataArea,
    708                    edge);
    709
    710            tickLabelHeight = estimateMaximumTickLabelHeight(g2);
    711            if (tickLabelHeight > unit2Height) {
    712                unit2 = (NumberTickUnit) tickUnits.getLargerTickUnit(unit2);
    713            }
    714
    715            setTickUnit(unit2, false, false);
    716
    717        }
    718
    719        /**
    720         * Estimates the maximum tick label height.
    721         *
    722         * @param g2  the graphics device.
    723         *
    724         * @return The maximum height.
    725         *
    726         * @since 1.0.7
    727         */
    728        protected double estimateMaximumTickLabelHeight(Graphics2D g2) {
    729
    730            RectangleInsets tickLabelInsets = getTickLabelInsets();
    731            double result = tickLabelInsets.getTop() + tickLabelInsets.getBottom();
    732
    733            Font tickLabelFont = getTickLabelFont();
    734            FontRenderContext frc = g2.getFontRenderContext();
    735            result += tickLabelFont.getLineMetrics("123", frc).getHeight();
    736            return result;
    737
    738        }
    739
    740        /**
    741         * Estimates the maximum width of the tick labels, assuming the specified
    742         * tick unit is used.
    743         * <P>
    744         * Rather than computing the string bounds of every tick on the axis, we
    745         * just look at two values: the lower bound and the upper bound for the
    746         * axis.  These two values will usually be representative.
    747         *
    748         * @param g2  the graphics device.
    749         * @param unit  the tick unit to use for calculation.
    750         *
    751         * @return The estimated maximum width of the tick labels.
    752         *
    753         * @since 1.0.7
    754         */
    755        protected double estimateMaximumTickLabelWidth(Graphics2D g2,
    756                                                       TickUnit unit) {
    757
    758            RectangleInsets tickLabelInsets = getTickLabelInsets();
    759            double result = tickLabelInsets.getLeft() + tickLabelInsets.getRight();
    760
    761            if (isVerticalTickLabels()) {
    762                // all tick labels have the same width (equal to the height of the
    763                // font)...
    764                FontRenderContext frc = g2.getFontRenderContext();
    765                LineMetrics lm = getTickLabelFont().getLineMetrics("0", frc);
    766                result += lm.getHeight();
    767            }
    768            else {
    769                // look at lower and upper bounds...
    770                FontMetrics fm = g2.getFontMetrics(getTickLabelFont());
    771                Range range = getRange();
    772                double lower = range.getLowerBound();
    773                double upper = range.getUpperBound();
    774                String lowerStr = "";
    775                String upperStr = "";
    776                NumberFormat formatter = getNumberFormatOverride();
    777                if (formatter != null) {
    778                    lowerStr = formatter.format(lower);
    779                    upperStr = formatter.format(upper);
    780                }
    781                else {
    782                    lowerStr = unit.valueToString(lower);
    783                    upperStr = unit.valueToString(upper);
    784                }
    785                double w1 = fm.stringWidth(lowerStr);
    786                double w2 = fm.stringWidth(upperStr);
    787                result += Math.max(w1, w2);
    788            }
    789
    790            return result;
    791
    792        }
    793
    794        /**
    795         * Zooms in on the current range.
    796         *
    797         * @param lowerPercent  the new lower bound.
    798         * @param upperPercent  the new upper bound.
    799         */
    800        public void zoomRange(double lowerPercent, double upperPercent) {
    801            Range range = getRange();
    802            double start = range.getLowerBound();
    803            double end = range.getUpperBound();
    804            double log1 = calculateLog(start);
    805            double log2 = calculateLog(end);
    806            double length = log2 - log1;
    807            Range adjusted = null;
    808            if (isInverted()) {
    809                double logA = log1 + length * (1 - upperPercent);
    810                double logB = log1 + length * (1 - lowerPercent);
    811                adjusted = new Range(calculateValue(logA), calculateValue(logB));
    812            }
    813            else {
    814                double logA = log1 + length * lowerPercent;
    815                double logB = log1 + length * upperPercent;
    816                adjusted = new Range(calculateValue(logA), calculateValue(logB));
    817            }
    818            setRange(adjusted);
    819        }
    820
    821        /**
    822         * Slides the axis range by the specified percentage.
    823         *
    824         * @param percent  the percentage.
    825         *
    826         * @since 1.0.13
    827         */
    828        public void pan(double percent) {
    829            Range range = getRange();
    830            double lower = range.getLowerBound();
    831            double upper = range.getUpperBound();
    832            double log1 = calculateLog(lower);
    833            double log2 = calculateLog(upper);
    834            double length = log2 - log1;
    835            double adj = length * percent;
    836            log1 = log1 + adj;
    837            log2 = log2 + adj;
    838            setRange(calculateValue(log1), calculateValue(log2));
    839        }
    840
    841        /**
    842         * Creates a tick label for the specified value.  Note that this method
    843         * was 'private' prior to version 1.0.10.
    844         *
    845         * @param value  the value.
    846         *
    847         * @return The label.
    848         *
    849         * @since 1.0.10
    850         */
    851        protected String createTickLabel(double value) {
    852            if (this.numberFormatOverride != null) {
    853                return this.numberFormatOverride.format(value);
    854            }
    855            else {
    856                return this.tickUnit.valueToString(value);
    857            }
    858        }
    859
    860        /**
    861         * Tests this axis for equality with an arbitrary object.
    862         *
    863         * @param obj  the object (<code>null</code> permitted).
    864         *
    865         * @return A boolean.
    866         */
    867        public boolean equals(Object obj) {
    868            if (obj == this) {
    869                return true;
    870            }
    871            if (!(obj instanceof LogAxis)) {
    872                return false;
    873            }
    874            LogAxis that = (LogAxis) obj;
    875            if (this.base != that.base) {
    876                return false;
    877            }
    878            if (this.smallestValue != that.smallestValue) {
    879                return false;
    880            }
    881            return super.equals(obj);
    882        }
    883
    884        /**
    885         * Returns a hash code for this instance.
    886         *
    887         * @return A hash code.
    888         */
    889        public int hashCode() {
    890            int result = 193;
    891            long temp = Double.doubleToLongBits(this.base);
    892            result = 37 * result + (int) (temp ^ (temp >>> 32));
    893            temp = Double.doubleToLongBits(this.smallestValue);
    894            result = 37 * result + (int) (temp ^ (temp >>> 32));
    895            if (this.numberFormatOverride != null) {
    896                result = 37 * result + this.numberFormatOverride.hashCode();
    897            }
    898            result = 37 * result + this.tickUnit.hashCode();
    899            return result;
    900        }
    901
    902        /**
    903         * Returns a collection of tick units for log (base 10) values.
    904         * Uses a given Locale to create the DecimalFormats.
    905         *
    906         * @param locale the locale to use to represent Numbers.
    907         *
    908         * @return A collection of tick units for integer values.
    909         *
    910         * @since 1.0.7
    911         */
    912        public static TickUnitSource createLogTickUnits(Locale locale) {
    913            TickUnits units = new TickUnits();
    914            NumberFormat numberFormat = new LogFormat();
    915            units.add(new NumberTickUnit(0.05, numberFormat, 2));
    916            units.add(new NumberTickUnit(0.1, numberFormat, 10));
    917            units.add(new NumberTickUnit(0.2, numberFormat, 2));
    918            units.add(new NumberTickUnit(0.5, numberFormat, 5));
    919            units.add(new NumberTickUnit(1, numberFormat, 10));
    920            units.add(new NumberTickUnit(2, numberFormat, 10));
    921            units.add(new NumberTickUnit(3, numberFormat, 15));
    922            units.add(new NumberTickUnit(4, numberFormat, 20));
    923            units.add(new NumberTickUnit(5, numberFormat, 25));
    924            units.add(new NumberTickUnit(6, numberFormat));
    925            units.add(new NumberTickUnit(7, numberFormat));
    926            units.add(new NumberTickUnit(8, numberFormat));
    927            units.add(new NumberTickUnit(9, numberFormat));
    928            units.add(new NumberTickUnit(10, numberFormat));
    929            return units;
    930        }
    931
    932    }