Source for org.jfree.chart.axis.LogAxis

   1: /* ===========================================================
   2:  * JFreeChart : a free chart library for the Java(tm) platform
   3:  * ===========================================================
   4:  *
   5:  * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
   6:  *
   7:  * Project Info:  http://www.jfree.org/jfreechart/index.html
   8:  *
   9:  * This library is free software; you can redistribute it and/or modify it
  10:  * under the terms of the GNU Lesser General Public License as published by
  11:  * the Free Software Foundation; either version 2.1 of the License, or
  12:  * (at your option) any later version.
  13:  *
  14:  * This library is distributed in the hope that it will be useful, but
  15:  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
  16:  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
  17:  * License for more details.
  18:  *
  19:  * You should have received a copy of the GNU Lesser General Public
  20:  * License along with this library; if not, write to the Free Software
  21:  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
  22:  * USA.  
  23:  *
  24:  * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
  25:  * in the United States and other countries.]
  26:  *
  27:  * ------------
  28:  * LogAxis.java
  29:  * ------------
  30:  * (C) Copyright 2006, 2007, by Object Refinery Limited and Contributors.
  31:  *
  32:  * Original Author:  David Gilbert (for Object Refinery Limited);
  33:  * Contributor(s):   -;
  34:  *
  35:  * Changes
  36:  * -------
  37:  * 24-Aug-2006 : Version 1 (DG);
  38:  * 22-Mar-2007 : Use defaultAutoArrange attribute (DG);
  39:  * 02-Aug-2007 : Fixed zooming bug, added support for margins (DG);
  40:  * 
  41:  */
  42: 
  43: package org.jfree.chart.axis;
  44: 
  45: import java.awt.Font;
  46: import java.awt.FontMetrics;
  47: import java.awt.Graphics2D;
  48: import java.awt.font.FontRenderContext;
  49: import java.awt.font.LineMetrics;
  50: import java.awt.geom.Rectangle2D;
  51: import java.text.DecimalFormat;
  52: import java.text.NumberFormat;
  53: import java.util.ArrayList;
  54: import java.util.List;
  55: import java.util.Locale;
  56: 
  57: import org.jfree.chart.event.AxisChangeEvent;
  58: import org.jfree.chart.plot.Plot;
  59: import org.jfree.chart.plot.PlotRenderingInfo;
  60: import org.jfree.chart.plot.ValueAxisPlot;
  61: import org.jfree.data.Range;
  62: import org.jfree.ui.RectangleEdge;
  63: import org.jfree.ui.RectangleInsets;
  64: import org.jfree.ui.TextAnchor;
  65: 
  66: /**
  67:  * A numerical axis that uses a logarithmic scale.  The class is an 
  68:  * alternative to the {@link LogarithmicAxis} class.
  69:  * 
  70:  * @since 1.0.7
  71:  */
  72: public class LogAxis extends ValueAxis {
  73: 
  74:     /** The logarithm base. */
  75:     private double base = 10.0;
  76:     
  77:     /** The logarithm of the base value - cached for performance. */
  78:     private double baseLog = Math.log(10.0);
  79:     
  80:     /**  The smallest value permitted on the axis. */
  81:     private double smallestValue = 1E-100;
  82:     
  83:     /** The current tick unit. */
  84:     private NumberTickUnit tickUnit;
  85:     
  86:     /** The override number format. */
  87:     private NumberFormat numberFormatOverride;
  88: 
  89:     /** The number of minor ticks per major tick unit. */
  90:     private int minorTickCount; 
  91:     
  92:     /**
  93:      * Creates a new <code>LogAxis</code> with no label.
  94:      */
  95:     public LogAxis() {
  96:         this(null);    
  97:     }
  98:     
  99:     /**
 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: }