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