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