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