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