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    }