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