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