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
043package org.jfree.chart.axis;
044
045import java.awt.Font;
046import java.awt.FontMetrics;
047import java.awt.Graphics2D;
048import java.awt.font.FontRenderContext;
049import java.awt.font.LineMetrics;
050import java.awt.geom.Rectangle2D;
051import java.text.DecimalFormat;
052import java.text.NumberFormat;
053import java.util.ArrayList;
054import java.util.List;
055import java.util.Locale;
056
057import org.jfree.chart.event.AxisChangeEvent;
058import org.jfree.chart.plot.Plot;
059import org.jfree.chart.plot.PlotRenderingInfo;
060import org.jfree.chart.plot.ValueAxisPlot;
061import org.jfree.data.Range;
062import org.jfree.ui.RectangleEdge;
063import org.jfree.ui.RectangleInsets;
064import 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 */
072public 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}