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    }