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