001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-present, by David Gilbert 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-present, by David Gilbert and Contributors. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): Andrew Mickish (patch 1868745); 034 * Peter Kolb (patches 1934255 and 2603321); 035 */ 036 037package org.jfree.chart.axis; 038 039import java.awt.Font; 040import java.awt.Graphics2D; 041import java.awt.font.FontRenderContext; 042import java.awt.font.LineMetrics; 043import java.awt.font.TextAttribute; 044import java.awt.geom.Rectangle2D; 045import java.text.AttributedString; 046import java.text.DecimalFormat; 047import java.text.Format; 048import java.text.NumberFormat; 049import java.util.ArrayList; 050import java.util.List; 051import java.util.Objects; 052 053import org.jfree.chart.event.AxisChangeEvent; 054import org.jfree.chart.plot.Plot; 055import org.jfree.chart.plot.PlotRenderingInfo; 056import org.jfree.chart.plot.ValueAxisPlot; 057import org.jfree.chart.ui.RectangleEdge; 058import org.jfree.chart.ui.RectangleInsets; 059import org.jfree.chart.ui.TextAnchor; 060import org.jfree.chart.util.AttrStringUtils; 061import org.jfree.chart.util.Args; 062import org.jfree.data.Range; 063 064/** 065 * A numerical axis that uses a logarithmic scale. The class is an 066 * alternative to the {@link LogarithmicAxis} class. 067 */ 068public class LogAxis extends ValueAxis { 069 070 /** The logarithm base. */ 071 private double base = 10.0; 072 073 /** The logarithm of the base value - cached for performance. */ 074 private double baseLog = Math.log(10.0); 075 076 /** 077 * The base symbol to display (if {@code null} then the numerical 078 * value of the base is displayed). 079 */ 080 private String baseSymbol = null; 081 082 /** 083 * The formatter to use for the base value when the base is displayed 084 * as a numerical value. 085 */ 086 private Format baseFormatter = new DecimalFormat("0"); 087 088 /** The smallest value permitted on the axis. */ 089 private double smallestValue = 1E-100; 090 091 /** The current tick unit. */ 092 private NumberTickUnit tickUnit; 093 094 /** The override number format. */ 095 private NumberFormat numberFormatOverride; 096 097 /** 098 * Creates a new {@code LogAxis} with no label. 099 */ 100 public LogAxis() { 101 this(null); 102 } 103 104 /** 105 * Creates a new {@code LogAxis} with the given label. 106 * 107 * @param label the axis label ({@code null} permitted). 108 */ 109 public LogAxis(String label) { 110 super(label, new NumberTickUnitSource()); 111 setDefaultAutoRange(new Range(0.01, 1.0)); 112 this.tickUnit = new NumberTickUnit(1.0, new DecimalFormat("0.#"), 10); 113 } 114 115 /** 116 * Returns the base for the logarithm calculation. The default value is 117 * {@code 10.0}. 118 * 119 * @return The base for the logarithm calculation. 120 * 121 * @see #setBase(double) 122 */ 123 public double getBase() { 124 return this.base; 125 } 126 127 /** 128 * Sets the base for the logarithm calculation and sends a change event to 129 * all registered listeners. 130 * 131 * @param base the base value (must be > 1.0). 132 * 133 * @see #getBase() 134 */ 135 public void setBase(double base) { 136 if (base <= 1.0) { 137 throw new IllegalArgumentException("Requires 'base' > 1.0."); 138 } 139 this.base = base; 140 this.baseLog = Math.log(base); 141 fireChangeEvent(); 142 } 143 144 /** 145 * Returns the symbol used to represent the base of the logarithmic scale 146 * for the axis. If this is {@code null} (the default) then the 147 * numerical value of the base is displayed. 148 * 149 * @return The base symbol (possibly {@code null}). 150 */ 151 public String getBaseSymbol() { 152 return this.baseSymbol; 153 } 154 155 /** 156 * Sets the symbol used to represent the base value of the logarithmic 157 * scale and sends a change event to all registered listeners. 158 * 159 * @param symbol the symbol ({@code null} permitted). 160 */ 161 public void setBaseSymbol(String symbol) { 162 this.baseSymbol = symbol; 163 fireChangeEvent(); 164 } 165 166 /** 167 * Returns the formatter used to format the base value of the logarithmic 168 * scale when it is displayed numerically. The default value is 169 * {@code new DecimalFormat("0")}. 170 * 171 * @return The base formatter (never {@code null}). 172 */ 173 public Format getBaseFormatter() { 174 return this.baseFormatter; 175 } 176 177 /** 178 * Sets the formatter used to format the base value of the logarithmic 179 * scale when it is displayed numerically and sends a change event to all 180 * registered listeners. 181 * 182 * @param formatter the formatter ({@code null} not permitted). 183 */ 184 public void setBaseFormatter(Format formatter) { 185 Args.nullNotPermitted(formatter, "formatter"); 186 this.baseFormatter = formatter; 187 fireChangeEvent(); 188 } 189 190 /** 191 * Returns the smallest value represented by the axis. 192 * 193 * @return The smallest value represented by the axis. 194 * 195 * @see #setSmallestValue(double) 196 */ 197 public double getSmallestValue() { 198 return this.smallestValue; 199 } 200 201 /** 202 * Sets the smallest value represented by the axis and sends a change event 203 * to all registered listeners. 204 * 205 * @param value the value. 206 * 207 * @see #getSmallestValue() 208 */ 209 public void setSmallestValue(double value) { 210 if (value <= 0.0) { 211 throw new IllegalArgumentException("Requires 'value' > 0.0."); 212 } 213 this.smallestValue = value; 214 fireChangeEvent(); 215 } 216 217 /** 218 * Returns the current tick unit. 219 * 220 * @return The current tick unit. 221 * 222 * @see #setTickUnit(NumberTickUnit) 223 */ 224 public NumberTickUnit getTickUnit() { 225 return this.tickUnit; 226 } 227 228 /** 229 * Sets the tick unit for the axis and sends an {@link AxisChangeEvent} to 230 * all registered listeners. A side effect of calling this method is that 231 * the "auto-select" feature for tick units is switched off (you can 232 * restore it using the {@link ValueAxis#setAutoTickUnitSelection(boolean)} 233 * method). 234 * 235 * @param unit the new tick unit ({@code null} not permitted). 236 * 237 * @see #getTickUnit() 238 */ 239 public void setTickUnit(NumberTickUnit unit) { 240 // defer argument checking... 241 setTickUnit(unit, true, true); 242 } 243 244 /** 245 * Sets the tick unit for the axis and, if requested, sends an 246 * {@link AxisChangeEvent} to all registered listeners. In addition, an 247 * option is provided to turn off the "auto-select" feature for tick units 248 * (you can restore it using the 249 * {@link ValueAxis#setAutoTickUnitSelection(boolean)} method). 250 * 251 * @param unit the new tick unit ({@code null} not permitted). 252 * @param notify notify listeners? 253 * @param turnOffAutoSelect turn off the auto-tick selection? 254 * 255 * @see #getTickUnit() 256 */ 257 public void setTickUnit(NumberTickUnit unit, boolean notify, 258 boolean turnOffAutoSelect) { 259 Args.nullNotPermitted(unit, "unit"); 260 this.tickUnit = unit; 261 if (turnOffAutoSelect) { 262 setAutoTickUnitSelection(false, false); 263 } 264 if (notify) { 265 fireChangeEvent(); 266 } 267 } 268 269 /** 270 * Returns the number format override. If this is non-{@code null}, 271 * then it will be used to format the numbers on the axis. 272 * 273 * @return The number formatter (possibly {@code null}). 274 * 275 * @see #setNumberFormatOverride(NumberFormat) 276 */ 277 public NumberFormat getNumberFormatOverride() { 278 return this.numberFormatOverride; 279 } 280 281 /** 282 * Sets the number format override and sends a change event to all 283 * registered listeners. If this is non-{@code null}, then it will be 284 * used to format the numbers on the axis. 285 * 286 * @param formatter the number formatter ({@code null} permitted). 287 * 288 * @see #getNumberFormatOverride() 289 */ 290 public void setNumberFormatOverride(NumberFormat formatter) { 291 this.numberFormatOverride = formatter; 292 fireChangeEvent(); 293 } 294 295 /** 296 * Calculates the log of the given value, using the current base. 297 * 298 * @param value the value. 299 * 300 * @return The log of the given value. 301 * 302 * @see #calculateValue(double) 303 * @see #getBase() 304 */ 305 public double calculateLog(double value) { 306 return Math.log(value) / this.baseLog; 307 } 308 309 /** 310 * Calculates the value from a given log. 311 * 312 * @param log the log value. 313 * 314 * @return The value with the given log. 315 * 316 * @see #calculateLog(double) 317 * @see #getBase() 318 */ 319 public double calculateValue(double log) { 320 return Math.pow(this.base, log); 321 } 322 323 private double calculateValueNoINF(double log) { 324 double result = calculateValue(log); 325 if (Double.isInfinite(result)) { 326 result = Double.MAX_VALUE; 327 } 328 if (result <= 0.0) { 329 result = Double.MIN_VALUE; 330 } 331 return result; 332 } 333 334 /** 335 * Converts a Java2D coordinate to an axis value, assuming that the 336 * axis is aligned to the specified {@code edge} of the {@code area}. 337 * 338 * @param java2DValue the Java2D coordinate. 339 * @param area the area for plotting data ({@code null} not 340 * permitted). 341 * @param edge the edge that the axis is aligned to ({@code null} not 342 * permitted). 343 * 344 * @return A value along the axis scale. 345 */ 346 @Override 347 public double java2DToValue(double java2DValue, Rectangle2D area, 348 RectangleEdge edge) { 349 350 Range range = getRange(); 351 double axisMin = calculateLog(Math.max(this.smallestValue, 352 range.getLowerBound())); 353 double axisMax = calculateLog(range.getUpperBound()); 354 355 double min = 0.0; 356 double max = 0.0; 357 if (RectangleEdge.isTopOrBottom(edge)) { 358 min = area.getX(); 359 max = area.getMaxX(); 360 } else if (RectangleEdge.isLeftOrRight(edge)) { 361 min = area.getMaxY(); 362 max = area.getY(); 363 } 364 double log; 365 if (isInverted()) { 366 log = axisMax - (java2DValue - min) / (max - min) 367 * (axisMax - axisMin); 368 } else { 369 log = axisMin + (java2DValue - min) / (max - min) 370 * (axisMax - axisMin); 371 } 372 return calculateValue(log); 373 } 374 375 /** 376 * Converts a value on the axis scale to a Java2D coordinate relative to 377 * the given {@code area}, based on the axis running along the 378 * specified {@code edge}. 379 * 380 * @param value the data value. 381 * @param area the area ({@code null} not permitted). 382 * @param edge the edge ({@code null} not permitted). 383 * 384 * @return The Java2D coordinate corresponding to {@code value}. 385 */ 386 @Override 387 public double valueToJava2D(double value, Rectangle2D area, 388 RectangleEdge edge) { 389 390 Range range = getRange(); 391 double axisMin = calculateLog(range.getLowerBound()); 392 double axisMax = calculateLog(range.getUpperBound()); 393 value = calculateLog(value); 394 395 double min = 0.0; 396 double max = 0.0; 397 if (RectangleEdge.isTopOrBottom(edge)) { 398 min = area.getX(); 399 max = area.getMaxX(); 400 } else if (RectangleEdge.isLeftOrRight(edge)) { 401 max = area.getMinY(); 402 min = area.getMaxY(); 403 } 404 if (isInverted()) { 405 return max 406 - ((value - axisMin) / (axisMax - axisMin)) * (max - min); 407 } else { 408 return min 409 + ((value - axisMin) / (axisMax - axisMin)) * (max - min); 410 } 411 } 412 413 /** 414 * Configures the axis. This method is typically called when an axis 415 * is assigned to a new plot. 416 */ 417 @Override 418 public void configure() { 419 if (isAutoRange()) { 420 autoAdjustRange(); 421 } 422 } 423 424 /** 425 * Adjusts the axis range to match the data range that the axis is 426 * required to display. 427 */ 428 @Override 429 protected void autoAdjustRange() { 430 Plot plot = getPlot(); 431 if (plot == null) { 432 return; // no plot, no data 433 } 434 435 if (plot instanceof ValueAxisPlot) { 436 ValueAxisPlot vap = (ValueAxisPlot) plot; 437 438 Range r = vap.getDataRange(this); 439 if (r == null) { 440 r = getDefaultAutoRange(); 441 } 442 443 double upper = r.getUpperBound(); 444 double lower = Math.max(r.getLowerBound(), this.smallestValue); 445 double range = upper - lower; 446 447 // if fixed auto range, then derive lower bound... 448 double fixedAutoRange = getFixedAutoRange(); 449 if (fixedAutoRange > 0.0) { 450 lower = Math.max(upper - fixedAutoRange, this.smallestValue); 451 } 452 else { 453 // ensure the autorange is at least <minRange> in size... 454 double minRange = getAutoRangeMinimumSize(); 455 if (range < minRange) { 456 double expand = (minRange - range) / 2; 457 upper = upper + expand; 458 lower = lower - expand; 459 } 460 461 // apply the margins - these should apply to the exponent range 462 double logUpper = calculateLog(upper); 463 double logLower = calculateLog(lower); 464 double logRange = logUpper - logLower; 465 logUpper = logUpper + getUpperMargin() * logRange; 466 logLower = logLower - getLowerMargin() * logRange; 467 upper = calculateValueNoINF(logUpper); 468 lower = calculateValueNoINF(logLower); 469 } 470 setRange(new Range(lower, upper), false, false); 471 } 472 473 } 474 475 /** 476 * Draws the axis on a Java 2D graphics device (such as the screen or a 477 * printer). 478 * 479 * @param g2 the graphics device ({@code null} not permitted). 480 * @param cursor the cursor location (determines where to draw the axis). 481 * @param plotArea the area within which the axes and plot should be drawn. 482 * @param dataArea the area within which the data should be drawn. 483 * @param edge the axis location ({@code null} not permitted). 484 * @param plotState collects information about the plot ({@code null} 485 * permitted). 486 * 487 * @return The axis state (never {@code null}). 488 */ 489 @Override 490 public AxisState draw(Graphics2D g2, double cursor, Rectangle2D plotArea, 491 Rectangle2D dataArea, RectangleEdge edge, 492 PlotRenderingInfo plotState) { 493 494 AxisState state; 495 // if the axis is not visible, don't draw it... 496 if (!isVisible()) { 497 state = new AxisState(cursor); 498 // even though the axis is not visible, we need ticks for the 499 // gridlines... 500 List ticks = refreshTicks(g2, state, dataArea, edge); 501 state.setTicks(ticks); 502 return state; 503 } 504 state = drawTickMarksAndLabels(g2, cursor, plotArea, dataArea, edge); 505 if (getAttributedLabel() != null) { 506 state = drawAttributedLabel(getAttributedLabel(), g2, plotArea, 507 dataArea, edge, state); 508 509 } else { 510 state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state); 511 } 512 createAndAddEntity(cursor, state, dataArea, edge, plotState); 513 return state; 514 } 515 516 /** 517 * Calculates the positions of the tick labels for the axis, storing the 518 * results in the tick label list (ready for drawing). 519 * 520 * @param g2 the graphics device. 521 * @param state the axis state. 522 * @param dataArea the area in which the plot should be drawn. 523 * @param edge the location of the axis. 524 * 525 * @return A list of ticks. 526 */ 527 @Override 528 public List refreshTicks(Graphics2D g2, AxisState state, 529 Rectangle2D dataArea, RectangleEdge edge) { 530 List result = new java.util.ArrayList(); 531 if (RectangleEdge.isTopOrBottom(edge)) { 532 result = refreshTicksHorizontal(g2, dataArea, edge); 533 } 534 else if (RectangleEdge.isLeftOrRight(edge)) { 535 result = refreshTicksVertical(g2, dataArea, edge); 536 } 537 return result; 538 } 539 540 /** 541 * Returns a list of ticks for an axis at the top or bottom of the chart. 542 * 543 * @param g2 the graphics device ({@code null} not permitted). 544 * @param dataArea the data area ({@code null} not permitted). 545 * @param edge the edge ({@code null} not permitted). 546 * 547 * @return A list of ticks. 548 */ 549 protected List refreshTicksHorizontal(Graphics2D g2, Rectangle2D dataArea, 550 RectangleEdge edge) { 551 552 Range range = getRange(); 553 List ticks = new ArrayList(); 554 Font tickLabelFont = getTickLabelFont(); 555 g2.setFont(tickLabelFont); 556 TextAnchor textAnchor; 557 if (edge == RectangleEdge.TOP) { 558 textAnchor = TextAnchor.BOTTOM_CENTER; 559 } 560 else { 561 textAnchor = TextAnchor.TOP_CENTER; 562 } 563 564 if (isAutoTickUnitSelection()) { 565 selectAutoTickUnit(g2, dataArea, edge); 566 } 567 int minorTickCount = this.tickUnit.getMinorTickCount(); 568 double unit = getTickUnit().getSize(); 569 double index = Math.ceil(calculateLog(getRange().getLowerBound()) 570 / unit); 571 double start = index * unit; 572 double end = 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 = calculateValueNoINF(current); 578 if (range.contains(v)) { 579 ticks.add(new LogTick(TickType.MAJOR, v, createTickLabel(v), 580 textAnchor)); 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 LogTick(TickType.MINOR, minorV, null, 589 textAnchor)); 590 } 591 } 592 current = current + this.tickUnit.getSize(); 593 } 594 return ticks; 595 } 596 597 /** 598 * Returns a list of ticks for an axis at the left or right of the chart. 599 * 600 * @param g2 the graphics device ({@code null} not permitted). 601 * @param dataArea the data area ({@code null} not permitted). 602 * @param edge the edge that the axis is aligned to ({@code null} 603 * not permitted). 604 * 605 * @return A list of ticks. 606 */ 607 protected List refreshTicksVertical(Graphics2D g2, Rectangle2D dataArea, 608 RectangleEdge edge) { 609 610 Range range = getRange(); 611 List ticks = new ArrayList(); 612 Font tickLabelFont = getTickLabelFont(); 613 g2.setFont(tickLabelFont); 614 TextAnchor textAnchor; 615 if (edge == RectangleEdge.RIGHT) { 616 textAnchor = TextAnchor.CENTER_LEFT; 617 } 618 else { 619 textAnchor = TextAnchor.CENTER_RIGHT; 620 } 621 622 if (isAutoTickUnitSelection()) { 623 selectAutoTickUnit(g2, dataArea, edge); 624 } 625 int minorTickCount = this.tickUnit.getMinorTickCount(); 626 double unit = getTickUnit().getSize(); 627 double index = Math.ceil(calculateLog(getRange().getLowerBound()) 628 / unit); 629 double start = index * unit; 630 double end = calculateLog(getUpperBound()); 631 double current = start; 632 boolean hasTicks = (this.tickUnit.getSize() > 0.0) 633 && !Double.isInfinite(start); 634 while (hasTicks && current <= end) { 635 double v = calculateValueNoINF(current); 636 if (range.contains(v)) { 637 ticks.add(new LogTick(TickType.MAJOR, v, createTickLabel(v), 638 textAnchor)); 639 } 640 // add minor ticks (for gridlines) 641 double next = Math.pow(this.base, current 642 + this.tickUnit.getSize()); 643 for (int i = 1; i < minorTickCount; i++) { 644 double minorV = v + i * ((next - v) / minorTickCount); 645 if (range.contains(minorV)) { 646 ticks.add(new LogTick(TickType.MINOR, minorV, null, 647 textAnchor)); 648 } 649 } 650 current = current + this.tickUnit.getSize(); 651 } 652 return ticks; 653 } 654 655 /** 656 * Selects an appropriate tick value for the axis. The strategy is to 657 * display as many ticks as possible (selected from an array of 'standard' 658 * tick units) without the labels overlapping. 659 * 660 * @param g2 the graphics device ({@code null} not permitted). 661 * @param dataArea the area defined by the axes ({@code null} not 662 * permitted). 663 * @param edge the axis location ({@code null} not permitted). 664 */ 665 protected void selectAutoTickUnit(Graphics2D g2, Rectangle2D dataArea, 666 RectangleEdge edge) { 667 if (RectangleEdge.isTopOrBottom(edge)) { 668 selectHorizontalAutoTickUnit(g2, dataArea, edge); 669 } 670 else if (RectangleEdge.isLeftOrRight(edge)) { 671 selectVerticalAutoTickUnit(g2, dataArea, edge); 672 } 673 } 674 675 /** 676 * Selects an appropriate tick value for the axis. The strategy is to 677 * display as many ticks as possible (selected from an array of 'standard' 678 * tick units) without the labels overlapping. 679 * 680 * @param g2 the graphics device. 681 * @param dataArea the area defined by the axes. 682 * @param edge the axis location. 683 */ 684 protected void selectHorizontalAutoTickUnit(Graphics2D g2, 685 Rectangle2D dataArea, RectangleEdge edge) { 686 687 // select a tick unit that is the next one bigger than the current 688 // (log) range divided by 50 689 Range range = getRange(); 690 double logAxisMin = calculateLog(Math.max(this.smallestValue, 691 range.getLowerBound())); 692 double logAxisMax = calculateLog(range.getUpperBound()); 693 double size = (logAxisMax - logAxisMin) / 50; 694 TickUnitSource tickUnits = getStandardTickUnits(); 695 TickUnit candidate = tickUnits.getCeilingTickUnit(size); 696 TickUnit prevCandidate = candidate; 697 boolean found = false; 698 while (!found) { 699 // while the tick labels overlap and there are more tick sizes available, 700 // choose the next bigger label 701 this.tickUnit = (NumberTickUnit) candidate; 702 double tickLabelWidth = estimateMaximumTickLabelWidth(g2, 703 candidate); 704 // what is the available space for one unit? 705 double candidateWidth = exponentLengthToJava2D(candidate.getSize(), 706 dataArea, edge); 707 if (tickLabelWidth < candidateWidth) { 708 found = true; 709 } else if (Double.isNaN(candidateWidth)) { 710 candidate = prevCandidate; 711 found = true; 712 } else { 713 prevCandidate = candidate; 714 candidate = tickUnits.getLargerTickUnit(prevCandidate); 715 if (candidate.equals(prevCandidate)) { 716 found = true; // there are no more candidates 717 } 718 } 719 } 720 setTickUnit((NumberTickUnit) candidate, false, false); 721 } 722 723 /** 724 * Converts a length in data coordinates into the corresponding length in 725 * Java2D coordinates. 726 * 727 * @param length the length. 728 * @param area the plot area. 729 * @param edge the edge along which the axis lies. 730 * 731 * @return The length in Java2D coordinates. 732 */ 733 public double exponentLengthToJava2D(double length, Rectangle2D area, 734 RectangleEdge edge) { 735 double one = valueToJava2D(calculateValueNoINF(1.0), area, edge); 736 double l = valueToJava2D(calculateValueNoINF(length + 1.0), area, edge); 737 return Math.abs(l - one); 738 } 739 740 /** 741 * Selects an appropriate tick value for the axis. The strategy is to 742 * display as many ticks as possible (selected from an array of 'standard' 743 * tick units) without the labels overlapping. 744 * 745 * @param g2 the graphics device. 746 * @param dataArea the area in which the plot should be drawn. 747 * @param edge the axis location. 748 */ 749 protected void selectVerticalAutoTickUnit(Graphics2D g2, 750 Rectangle2D dataArea, RectangleEdge edge) { 751 // select a tick unit that is the next one bigger than the current 752 // (log) range divided by 50 753 Range range = getRange(); 754 double logAxisMin = calculateLog(Math.max(this.smallestValue, 755 range.getLowerBound())); 756 double logAxisMax = calculateLog(range.getUpperBound()); 757 double size = (logAxisMax - logAxisMin) / 50; 758 TickUnitSource tickUnits = getStandardTickUnits(); 759 TickUnit candidate = tickUnits.getCeilingTickUnit(size); 760 TickUnit prevCandidate = candidate; 761 boolean found = false; 762 while (!found) { 763 // while the tick labels overlap and there are more tick sizes available, 764 // choose the next bigger label 765 this.tickUnit = (NumberTickUnit) candidate; 766 double tickLabelHeight = estimateMaximumTickLabelHeight(g2); 767 // what is the available space for one unit? 768 double candidateHeight = exponentLengthToJava2D(candidate.getSize(), 769 dataArea, edge); 770 if (tickLabelHeight < candidateHeight) { 771 found = true; 772 } else if (Double.isNaN(candidateHeight)) { 773 candidate = prevCandidate; 774 found = true; 775 } else { 776 prevCandidate = candidate; 777 candidate = tickUnits.getLargerTickUnit(prevCandidate); 778 if (candidate.equals(prevCandidate)) { 779 found = true; // there are no more candidates 780 } 781 } 782 } 783 setTickUnit((NumberTickUnit) candidate, false, false); 784 } 785 786 /** 787 * Creates a tick label for the specified value based on the current 788 * tick unit (used for formatting the exponent). 789 * 790 * @param value the value. 791 * 792 * @return The label. 793 */ 794 protected AttributedString createTickLabel(double value) { 795 if (this.numberFormatOverride != null) { 796 String text = this.numberFormatOverride.format(value); 797 AttributedString as = new AttributedString(text); 798 as.addAttribute(TextAttribute.FONT, getTickLabelFont()); 799 return as; 800 } else { 801 String baseStr = this.baseSymbol; 802 if (baseStr == null) { 803 baseStr = this.baseFormatter.format(this.base); 804 } 805 double logy = calculateLog(value); 806 String exponentStr = getTickUnit().valueToString(logy); 807 AttributedString as = new AttributedString(baseStr + exponentStr); 808 as.addAttributes(getTickLabelFont().getAttributes(), 0, (baseStr 809 + exponentStr).length()); 810 as.addAttribute(TextAttribute.SUPERSCRIPT, 811 TextAttribute.SUPERSCRIPT_SUPER, baseStr.length(), 812 baseStr.length() + exponentStr.length()); 813 return as; 814 } 815 } 816 817 /** 818 * Estimates the maximum tick label height. 819 * 820 * @param g2 the graphics device. 821 * 822 * @return The maximum height. 823 */ 824 protected double estimateMaximumTickLabelHeight(Graphics2D g2) { 825 RectangleInsets tickLabelInsets = getTickLabelInsets(); 826 double result = tickLabelInsets.getTop() + tickLabelInsets.getBottom(); 827 828 Font tickLabelFont = getTickLabelFont(); 829 FontRenderContext frc = g2.getFontRenderContext(); 830 result += tickLabelFont.getLineMetrics("123", frc).getHeight(); 831 return result; 832 } 833 834 /** 835 * Estimates the maximum width of the tick labels, assuming the specified 836 * tick unit is used. 837 * <P> 838 * Rather than computing the string bounds of every tick on the axis, we 839 * just look at two values: the lower bound and the upper bound for the 840 * axis. These two values will usually be representative. 841 * 842 * @param g2 the graphics device. 843 * @param unit the tick unit to use for calculation. 844 * 845 * @return The estimated maximum width of the tick labels. 846 */ 847 protected double estimateMaximumTickLabelWidth(Graphics2D g2, 848 TickUnit unit) { 849 850 RectangleInsets tickLabelInsets = getTickLabelInsets(); 851 double result = tickLabelInsets.getLeft() + tickLabelInsets.getRight(); 852 853 if (isVerticalTickLabels()) { 854 // all tick labels have the same width (equal to the height of the 855 // font)... 856 FontRenderContext frc = g2.getFontRenderContext(); 857 LineMetrics lm = getTickLabelFont().getLineMetrics("0", frc); 858 result += lm.getHeight(); 859 } 860 else { 861 // look at lower and upper bounds... 862 Range range = getRange(); 863 double lower = range.getLowerBound(); 864 double upper = range.getUpperBound(); 865 AttributedString lowerStr = createTickLabel(lower); 866 AttributedString upperStr = createTickLabel(upper); 867 double w1 = AttrStringUtils.getTextBounds(lowerStr, g2).getWidth(); 868 double w2 = AttrStringUtils.getTextBounds(upperStr, g2).getWidth(); 869 result += Math.max(w1, w2); 870 } 871 return result; 872 } 873 874 /** 875 * Zooms in on the current range. 876 * 877 * @param lowerPercent the new lower bound. 878 * @param upperPercent the new upper bound. 879 */ 880 @Override 881 public void zoomRange(double lowerPercent, double upperPercent) { 882 Range range = getRange(); 883 double start = range.getLowerBound(); 884 double end = range.getUpperBound(); 885 double log1 = calculateLog(start); 886 double log2 = calculateLog(end); 887 double length = log2 - log1; 888 Range adjusted; 889 if (isInverted()) { 890 double logA = log1 + length * (1 - upperPercent); 891 double logB = log1 + length * (1 - lowerPercent); 892 adjusted = new Range(calculateValueNoINF(logA), 893 calculateValueNoINF(logB)); 894 } 895 else { 896 double logA = log1 + length * lowerPercent; 897 double logB = log1 + length * upperPercent; 898 adjusted = new Range(calculateValueNoINF(logA), 899 calculateValueNoINF(logB)); 900 } 901 setRange(adjusted); 902 } 903 904 /** 905 * Slides the axis range by the specified percentage. 906 * 907 * @param percent the percentage. 908 */ 909 @Override 910 public void pan(double percent) { 911 Range range = getRange(); 912 double lower = range.getLowerBound(); 913 double upper = range.getUpperBound(); 914 double log1 = calculateLog(lower); 915 double log2 = calculateLog(upper); 916 double length = log2 - log1; 917 double adj = length * percent; 918 log1 = log1 + adj; 919 log2 = log2 + adj; 920 setRange(calculateValueNoINF(log1), calculateValueNoINF(log2)); 921 } 922 923 /** 924 * Increases or decreases the axis range by the specified percentage about 925 * the central value and sends an {@link AxisChangeEvent} to all registered 926 * listeners. 927 * <P> 928 * To double the length of the axis range, use 200% (2.0). 929 * To halve the length of the axis range, use 50% (0.5). 930 * 931 * @param percent the resize factor. 932 * 933 * @see #resizeRange(double, double) 934 */ 935 @Override 936 public void resizeRange(double percent) { 937 Range range = getRange(); 938 double logMin = calculateLog(range.getLowerBound()); 939 double logMax = calculateLog(range.getUpperBound()); 940 double centralValue = calculateValueNoINF((logMin + logMax) / 2.0); 941 resizeRange(percent, centralValue); 942 } 943 944 @Override 945 public void resizeRange(double percent, double anchorValue) { 946 resizeRange2(percent, anchorValue); 947 } 948 949 /** 950 * Resizes the axis length to the specified percentage of the current 951 * range and sends a change event to all registered listeners. If 952 * {@code percent} is greater than 1.0 (100 percent) then the axis 953 * range is increased (which has the effect of zooming out), while if the 954 * {@code percent} is less than 1.0 the axis range is decreased 955 * (which has the effect of zooming in). The resize occurs around an 956 * anchor value (which may not be in the center of the axis). This is used 957 * to support mouse wheel zooming around an arbitrary point on the plot. 958 * <br><br> 959 * This method is overridden to perform the percentage calculations on the 960 * log values (which are linear for this axis). 961 * 962 * @param percent the percentage (must be greater than zero). 963 * @param anchorValue the anchor value. 964 */ 965 @Override 966 public void resizeRange2(double percent, double anchorValue) { 967 if (percent > 0.0) { 968 double logAnchorValue = calculateLog(anchorValue); 969 Range range = getRange(); 970 double logAxisMin = calculateLog(range.getLowerBound()); 971 double logAxisMax = calculateLog(range.getUpperBound()); 972 973 double left = percent * (logAnchorValue - logAxisMin); 974 double right = percent * (logAxisMax - logAnchorValue); 975 976 double upperBound = calculateValueNoINF(logAnchorValue + right); 977 Range adjusted = new Range(calculateValueNoINF( 978 logAnchorValue - left), upperBound); 979 setRange(adjusted); 980 } 981 else { 982 setAutoRange(true); 983 } 984 } 985 986 /** 987 * Tests this axis for equality with an arbitrary object. 988 * 989 * @param obj the object ({@code null} permitted). 990 * 991 * @return A boolean. 992 */ 993 @Override 994 public boolean equals(Object obj) { 995 if (obj == this) { 996 return true; 997 } 998 if (!(obj instanceof LogAxis)) { 999 return false; 1000 } 1001 LogAxis that = (LogAxis) obj; 1002 if (this.base != that.base) { 1003 return false; 1004 } 1005 if (!Objects.equals(this.baseSymbol, that.baseSymbol)) { 1006 return false; 1007 } 1008 if (!this.baseFormatter.equals(that.baseFormatter)) { 1009 return false; 1010 } 1011 if (this.smallestValue != that.smallestValue) { 1012 return false; 1013 } 1014 if (!Objects.equals(this.numberFormatOverride, 1015 that.numberFormatOverride)) { 1016 return false; 1017 } 1018 return super.equals(obj); 1019 } 1020 1021 /** 1022 * Returns a hash code for this instance. 1023 * 1024 * @return A hash code. 1025 */ 1026 @Override 1027 public int hashCode() { 1028 int result = 193; 1029 long temp = Double.doubleToLongBits(this.base); 1030 result = 37 * result + (int) (temp ^ (temp >>> 32)); 1031 temp = Double.doubleToLongBits(this.smallestValue); 1032 result = 37 * result + (int) (temp ^ (temp >>> 32)); 1033 if (this.numberFormatOverride != null) { 1034 result = 37 * result + this.numberFormatOverride.hashCode(); 1035 } 1036 result = 37 * result + this.tickUnit.hashCode(); 1037 return result; 1038 } 1039 1040}