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 * NumberAxis.java 029 * --------------- 030 * (C) Copyright 2000-present, by David Gilbert and Contributors. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): Laurence Vanhelsuwe; 034 * Peter Kolb (patches 1934255 and 2603321); 035 * 036 */ 037 038package org.jfree.chart.axis; 039 040import java.awt.Font; 041import java.awt.FontMetrics; 042import java.awt.Graphics2D; 043import java.awt.font.FontRenderContext; 044import java.awt.font.LineMetrics; 045import java.awt.geom.Rectangle2D; 046import java.io.Serializable; 047import java.text.DecimalFormat; 048import java.text.NumberFormat; 049import java.util.ArrayList; 050import java.util.List; 051import java.util.Locale; 052import java.util.Objects; 053 054import org.jfree.chart.event.AxisChangeEvent; 055import org.jfree.chart.plot.Plot; 056import org.jfree.chart.plot.PlotRenderingInfo; 057import org.jfree.chart.plot.ValueAxisPlot; 058import org.jfree.chart.ui.RectangleEdge; 059import org.jfree.chart.ui.RectangleInsets; 060import org.jfree.chart.ui.TextAnchor; 061import org.jfree.chart.util.Args; 062import org.jfree.data.Range; 063import org.jfree.data.RangeType; 064 065/** 066 * An axis for displaying numerical data. 067 * <P> 068 * If the axis is set up to automatically determine its range to fit the data, 069 * you can ensure that the range includes zero (statisticians usually prefer 070 * this) by setting the {@code autoRangeIncludesZero} flag to 071 * {@code true}. 072 * <P> 073 * The {@code NumberAxis} class has a mechanism for automatically 074 * selecting a tick unit that is appropriate for the current axis range. 075 */ 076public class NumberAxis extends ValueAxis implements Cloneable, Serializable { 077 078 /** For serialization. */ 079 private static final long serialVersionUID = 2805933088476185789L; 080 081 /** The default value for the autoRangeIncludesZero flag. */ 082 public static final boolean DEFAULT_AUTO_RANGE_INCLUDES_ZERO = true; 083 084 /** The default value for the autoRangeStickyZero flag. */ 085 public static final boolean DEFAULT_AUTO_RANGE_STICKY_ZERO = true; 086 087 /** The default tick unit. */ 088 public static final NumberTickUnit DEFAULT_TICK_UNIT = new NumberTickUnit( 089 1.0, new DecimalFormat("0")); 090 091 /** The default setting for the vertical tick labels flag. */ 092 public static final boolean DEFAULT_VERTICAL_TICK_LABELS = false; 093 094 /** 095 * The range type (can be used to force the axis to display only positive 096 * values or only negative values). 097 */ 098 private RangeType rangeType; 099 100 /** 101 * A flag that affects the axis range when the range is determined 102 * automatically. If the auto range does NOT include zero and this flag 103 * is TRUE, then the range is changed to include zero. 104 */ 105 private boolean autoRangeIncludesZero; 106 107 /** 108 * A flag that affects the size of the margins added to the axis range when 109 * the range is determined automatically. If the value 0 falls within the 110 * margin and this flag is TRUE, then the margin is truncated at zero. 111 */ 112 private boolean autoRangeStickyZero; 113 114 /** The tick unit for the axis. */ 115 private NumberTickUnit tickUnit; 116 117 /** The override number format. */ 118 private NumberFormat numberFormatOverride; 119 120 /** An optional band for marking regions on the axis. */ 121 private MarkerAxisBand markerBand; 122 123 /** 124 * Default constructor. 125 */ 126 public NumberAxis() { 127 this(null); 128 } 129 130 /** 131 * Constructs a number axis, using default values where necessary. 132 * 133 * @param label the axis label ({@code null} permitted). 134 */ 135 public NumberAxis(String label) { 136 super(label, NumberAxis.createStandardTickUnits()); 137 this.rangeType = RangeType.FULL; 138 this.autoRangeIncludesZero = DEFAULT_AUTO_RANGE_INCLUDES_ZERO; 139 this.autoRangeStickyZero = DEFAULT_AUTO_RANGE_STICKY_ZERO; 140 this.tickUnit = DEFAULT_TICK_UNIT; 141 this.numberFormatOverride = null; 142 this.markerBand = null; 143 } 144 145 /** 146 * Returns the axis range type. 147 * 148 * @return The axis range type (never {@code null}). 149 * 150 * @see #setRangeType(RangeType) 151 */ 152 public RangeType getRangeType() { 153 return this.rangeType; 154 } 155 156 /** 157 * Sets the axis range type. 158 * 159 * @param rangeType the range type ({@code null} not permitted). 160 * 161 * @see #getRangeType() 162 */ 163 public void setRangeType(RangeType rangeType) { 164 Args.nullNotPermitted(rangeType, "rangeType"); 165 this.rangeType = rangeType; 166 notifyListeners(new AxisChangeEvent(this)); 167 } 168 169 /** 170 * Returns the flag that indicates whether or not the automatic axis range 171 * (if indeed it is determined automatically) is forced to include zero. 172 * 173 * @return The flag. 174 */ 175 public boolean getAutoRangeIncludesZero() { 176 return this.autoRangeIncludesZero; 177 } 178 179 /** 180 * Sets the flag that indicates whether or not the axis range, if 181 * automatically calculated, is forced to include zero. 182 * <p> 183 * If the flag is changed to {@code true}, the axis range is 184 * recalculated. 185 * <p> 186 * Any change to the flag will trigger an {@link AxisChangeEvent}. 187 * 188 * @param flag the new value of the flag. 189 * 190 * @see #getAutoRangeIncludesZero() 191 */ 192 public void setAutoRangeIncludesZero(boolean flag) { 193 if (this.autoRangeIncludesZero != flag) { 194 this.autoRangeIncludesZero = flag; 195 if (isAutoRange()) { 196 autoAdjustRange(); 197 } 198 notifyListeners(new AxisChangeEvent(this)); 199 } 200 } 201 202 /** 203 * Returns a flag that affects the auto-range when zero falls outside the 204 * data range but inside the margins defined for the axis. 205 * 206 * @return The flag. 207 * 208 * @see #setAutoRangeStickyZero(boolean) 209 */ 210 public boolean getAutoRangeStickyZero() { 211 return this.autoRangeStickyZero; 212 } 213 214 /** 215 * Sets a flag that affects the auto-range when zero falls outside the data 216 * range but inside the margins defined for the axis. 217 * 218 * @param flag the new flag. 219 * 220 * @see #getAutoRangeStickyZero() 221 */ 222 public void setAutoRangeStickyZero(boolean flag) { 223 if (this.autoRangeStickyZero != flag) { 224 this.autoRangeStickyZero = flag; 225 if (isAutoRange()) { 226 autoAdjustRange(); 227 } 228 notifyListeners(new AxisChangeEvent(this)); 229 } 230 } 231 232 /** 233 * Returns the tick unit for the axis. 234 * <p> 235 * Note: if the {@code autoTickUnitSelection} flag is 236 * {@code true} the tick unit may be changed while the axis is being 237 * drawn, so in that case the return value from this method may be 238 * irrelevant if the method is called before the axis has been drawn. 239 * 240 * @return The tick unit for the axis. 241 * 242 * @see #setTickUnit(NumberTickUnit) 243 * @see ValueAxis#isAutoTickUnitSelection() 244 */ 245 public NumberTickUnit getTickUnit() { 246 return this.tickUnit; 247 } 248 249 /** 250 * Sets the tick unit for the axis and sends an {@link AxisChangeEvent} to 251 * all registered listeners. A side effect of calling this method is that 252 * the "auto-select" feature for tick units is switched off (you can 253 * restore it using the {@link ValueAxis#setAutoTickUnitSelection(boolean)} 254 * method). 255 * 256 * @param unit the new tick unit ({@code null} not permitted). 257 * 258 * @see #getTickUnit() 259 * @see #setTickUnit(NumberTickUnit, boolean, boolean) 260 */ 261 public void setTickUnit(NumberTickUnit unit) { 262 // defer argument checking... 263 setTickUnit(unit, true, true); 264 } 265 266 /** 267 * Sets the tick unit for the axis and, if requested, sends an 268 * {@link AxisChangeEvent} to all registered listeners. In addition, an 269 * option is provided to turn off the "auto-select" feature for tick units 270 * (you can restore it using the 271 * {@link ValueAxis#setAutoTickUnitSelection(boolean)} method). 272 * 273 * @param unit the new tick unit ({@code null} not permitted). 274 * @param notify notify listeners? 275 * @param turnOffAutoSelect turn off the auto-tick selection? 276 */ 277 public void setTickUnit(NumberTickUnit unit, boolean notify, 278 boolean turnOffAutoSelect) { 279 280 Args.nullNotPermitted(unit, "unit"); 281 this.tickUnit = unit; 282 if (turnOffAutoSelect) { 283 setAutoTickUnitSelection(false, false); 284 } 285 if (notify) { 286 notifyListeners(new AxisChangeEvent(this)); 287 } 288 289 } 290 291 /** 292 * Returns the number format override. If this is non-null, then it will 293 * be used to format the numbers on the axis. 294 * 295 * @return The number formatter (possibly {@code null}). 296 * 297 * @see #setNumberFormatOverride(NumberFormat) 298 */ 299 public NumberFormat getNumberFormatOverride() { 300 return this.numberFormatOverride; 301 } 302 303 /** 304 * Sets the number format override. If this is non-null, then it will be 305 * used to format the numbers on the axis. 306 * 307 * @param formatter the number formatter ({@code null} permitted). 308 * 309 * @see #getNumberFormatOverride() 310 */ 311 public void setNumberFormatOverride(NumberFormat formatter) { 312 this.numberFormatOverride = formatter; 313 notifyListeners(new AxisChangeEvent(this)); 314 } 315 316 /** 317 * Returns the (optional) marker band for the axis. 318 * 319 * @return The marker band (possibly {@code null}). 320 * 321 * @see #setMarkerBand(MarkerAxisBand) 322 */ 323 public MarkerAxisBand getMarkerBand() { 324 return this.markerBand; 325 } 326 327 /** 328 * Sets the marker band for the axis. 329 * <P> 330 * The marker band is optional, leave it set to {@code null} if you 331 * don't require it. 332 * 333 * @param band the new band ({@code null} permitted). 334 * 335 * @see #getMarkerBand() 336 */ 337 public void setMarkerBand(MarkerAxisBand band) { 338 this.markerBand = band; 339 notifyListeners(new AxisChangeEvent(this)); 340 } 341 342 /** 343 * Configures the axis to work with the specified plot. If the axis has 344 * auto-scaling, then sets the maximum and minimum values. 345 */ 346 @Override 347 public void configure() { 348 if (isAutoRange()) { 349 autoAdjustRange(); 350 } 351 } 352 353 /** 354 * Rescales the axis to ensure that all data is visible. 355 */ 356 @Override 357 protected void autoAdjustRange() { 358 359 Plot plot = getPlot(); 360 if (plot == null) { 361 return; // no plot, no data 362 } 363 364 if (plot instanceof ValueAxisPlot) { 365 ValueAxisPlot vap = (ValueAxisPlot) plot; 366 367 Range r = vap.getDataRange(this); 368 if (r == null) { 369 r = getDefaultAutoRange(); 370 } 371 372 double upper = r.getUpperBound(); 373 double lower = r.getLowerBound(); 374 if (this.rangeType == RangeType.POSITIVE) { 375 lower = Math.max(0.0, lower); 376 upper = Math.max(0.0, upper); 377 } 378 else if (this.rangeType == RangeType.NEGATIVE) { 379 lower = Math.min(0.0, lower); 380 upper = Math.min(0.0, upper); 381 } 382 383 if (getAutoRangeIncludesZero()) { 384 lower = Math.min(lower, 0.0); 385 upper = Math.max(upper, 0.0); 386 } 387 double range = upper - lower; 388 389 // if fixed auto range, then derive lower bound... 390 double fixedAutoRange = getFixedAutoRange(); 391 if (fixedAutoRange > 0.0) { 392 lower = upper - fixedAutoRange; 393 } 394 else { 395 // ensure the autorange is at least <minRange> in size... 396 double minRange = getAutoRangeMinimumSize(); 397 if (range < minRange) { 398 double expand = (minRange - range) / 2; 399 upper = upper + expand; 400 lower = lower - expand; 401 if (lower == upper) { // see bug report 1549218 402 double adjust = Math.abs(lower) / 10.0; 403 lower = lower - adjust; 404 upper = upper + adjust; 405 } 406 if (this.rangeType == RangeType.POSITIVE) { 407 if (lower < 0.0) { 408 upper = upper - lower; 409 lower = 0.0; 410 } 411 } 412 else if (this.rangeType == RangeType.NEGATIVE) { 413 if (upper > 0.0) { 414 lower = lower - upper; 415 upper = 0.0; 416 } 417 } 418 } 419 420 if (getAutoRangeStickyZero()) { 421 if (upper <= 0.0) { 422 upper = Math.min(0.0, upper + getUpperMargin() * range); 423 } 424 else { 425 upper = upper + getUpperMargin() * range; 426 } 427 if (lower >= 0.0) { 428 lower = Math.max(0.0, lower - getLowerMargin() * range); 429 } 430 else { 431 lower = lower - getLowerMargin() * range; 432 } 433 } 434 else { 435 upper = upper + getUpperMargin() * range; 436 lower = lower - getLowerMargin() * range; 437 } 438 } 439 440 setRange(new Range(lower, upper), false, false); 441 } 442 443 } 444 445 /** 446 * Converts a data value to a coordinate in Java2D space, assuming that the 447 * axis runs along one edge of the specified dataArea. 448 * <p> 449 * Note that it is possible for the coordinate to fall outside the plotArea. 450 * 451 * @param value the data value. 452 * @param area the area for plotting the data. 453 * @param edge the axis location. 454 * 455 * @return The Java2D coordinate. 456 * 457 * @see #java2DToValue(double, Rectangle2D, RectangleEdge) 458 */ 459 @Override 460 public double valueToJava2D(double value, Rectangle2D area, 461 RectangleEdge edge) { 462 463 Range range = getRange(); 464 double axisMin = range.getLowerBound(); 465 double axisMax = range.getUpperBound(); 466 467 double min = 0.0; 468 double max = 0.0; 469 if (RectangleEdge.isTopOrBottom(edge)) { 470 min = area.getX(); 471 max = area.getMaxX(); 472 } 473 else if (RectangleEdge.isLeftOrRight(edge)) { 474 max = area.getMinY(); 475 min = area.getMaxY(); 476 } 477 if (isInverted()) { 478 return max 479 - ((value - axisMin) / (axisMax - axisMin)) * (max - min); 480 } 481 else { 482 return min 483 + ((value - axisMin) / (axisMax - axisMin)) * (max - min); 484 } 485 486 } 487 488 /** 489 * Converts a coordinate in Java2D space to the corresponding data value, 490 * assuming that the axis runs along one edge of the specified dataArea. 491 * 492 * @param java2DValue the coordinate in Java2D space. 493 * @param area the area in which the data is plotted. 494 * @param edge the location. 495 * 496 * @return The data value. 497 * 498 * @see #valueToJava2D(double, Rectangle2D, RectangleEdge) 499 */ 500 @Override 501 public double java2DToValue(double java2DValue, Rectangle2D area, 502 RectangleEdge edge) { 503 504 Range range = getRange(); 505 double axisMin = range.getLowerBound(); 506 double axisMax = range.getUpperBound(); 507 508 double min = 0.0; 509 double max = 0.0; 510 if (RectangleEdge.isTopOrBottom(edge)) { 511 min = area.getX(); 512 max = area.getMaxX(); 513 } 514 else if (RectangleEdge.isLeftOrRight(edge)) { 515 min = area.getMaxY(); 516 max = area.getY(); 517 } 518 if (isInverted()) { 519 return axisMax 520 - (java2DValue - min) / (max - min) * (axisMax - axisMin); 521 } 522 else { 523 return axisMin 524 + (java2DValue - min) / (max - min) * (axisMax - axisMin); 525 } 526 527 } 528 529 /** 530 * Calculates the value of the lowest visible tick on the axis. 531 * 532 * @return The value of the lowest visible tick on the axis. 533 * 534 * @see #calculateHighestVisibleTickValue() 535 */ 536 protected double calculateLowestVisibleTickValue() { 537 double unit = getTickUnit().getSize(); 538 double index = Math.ceil(getRange().getLowerBound() / unit); 539 return index * unit; 540 } 541 542 /** 543 * Calculates the value of the highest visible tick on the axis. 544 * 545 * @return The value of the highest visible tick on the axis. 546 * 547 * @see #calculateLowestVisibleTickValue() 548 */ 549 protected double calculateHighestVisibleTickValue() { 550 double unit = getTickUnit().getSize(); 551 double index = Math.floor(getRange().getUpperBound() / unit); 552 return index * unit; 553 } 554 555 /** 556 * Calculates the number of visible ticks. 557 * 558 * @return The number of visible ticks on the axis. 559 */ 560 protected int calculateVisibleTickCount() { 561 double unit = getTickUnit().getSize(); 562 Range range = getRange(); 563 return (int) (Math.floor(range.getUpperBound() / unit) 564 - Math.ceil(range.getLowerBound() / unit) + 1); 565 } 566 567 /** 568 * Draws the axis on a Java 2D graphics device (such as the screen or a 569 * printer). 570 * 571 * @param g2 the graphics device ({@code null} not permitted). 572 * @param cursor the cursor location. 573 * @param plotArea the area within which the axes and data should be drawn 574 * ({@code null} not permitted). 575 * @param dataArea the area within which the data should be drawn 576 * ({@code null} not permitted). 577 * @param edge the location of the axis ({@code null} not permitted). 578 * @param plotState collects information about the plot 579 * ({@code null} permitted). 580 * 581 * @return The axis state (never {@code null}). 582 */ 583 @Override 584 public AxisState draw(Graphics2D g2, double cursor, Rectangle2D plotArea, 585 Rectangle2D dataArea, RectangleEdge edge, 586 PlotRenderingInfo plotState) { 587 588 AxisState state; 589 // if the axis is not visible, don't draw it... 590 if (!isVisible()) { 591 state = new AxisState(cursor); 592 // even though the axis is not visible, we need ticks for the 593 // gridlines... 594 List ticks = refreshTicks(g2, state, dataArea, edge); 595 state.setTicks(ticks); 596 return state; 597 } 598 599 // draw the tick marks and labels... 600 state = drawTickMarksAndLabels(g2, cursor, plotArea, dataArea, edge); 601 602 if (getAttributedLabel() != null) { 603 state = drawAttributedLabel(getAttributedLabel(), g2, plotArea, 604 dataArea, edge, state); 605 606 } else { 607 state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state); 608 } 609 createAndAddEntity(cursor, state, dataArea, edge, plotState); 610 return state; 611 612 } 613 614 /** 615 * Creates the standard tick units. 616 * <P> 617 * If you don't like these defaults, create your own instance of TickUnits 618 * and then pass it to the setStandardTickUnits() method in the 619 * NumberAxis class. 620 * 621 * @return The standard tick units. 622 * 623 * @see #setStandardTickUnits(TickUnitSource) 624 * @see #createIntegerTickUnits() 625 */ 626 public static TickUnitSource createStandardTickUnits() { 627 return new NumberTickUnitSource(); 628 } 629 630 /** 631 * Returns a collection of tick units for integer values. 632 * 633 * @return A collection of tick units for integer values. 634 * 635 * @see #setStandardTickUnits(TickUnitSource) 636 * @see #createStandardTickUnits() 637 */ 638 public static TickUnitSource createIntegerTickUnits() { 639 return new NumberTickUnitSource(true); 640 } 641 642 /** 643 * Creates a collection of standard tick units. The supplied locale is 644 * used to create the number formatter (a localised instance of 645 * {@code NumberFormat}). 646 * <P> 647 * If you don't like these defaults, create your own instance of 648 * {@link TickUnits} and then pass it to the 649 * {@code setStandardTickUnits()} method. 650 * 651 * @param locale the locale. 652 * 653 * @return A tick unit collection. 654 * 655 * @see #setStandardTickUnits(TickUnitSource) 656 */ 657 public static TickUnitSource createStandardTickUnits(Locale locale) { 658 NumberFormat numberFormat = NumberFormat.getNumberInstance(locale); 659 return new NumberTickUnitSource(false, numberFormat); 660 } 661 662 /** 663 * Returns a collection of tick units for integer values. 664 * Uses a given Locale to create the DecimalFormats. 665 * 666 * @param locale the locale to use to represent Numbers. 667 * 668 * @return A collection of tick units for integer values. 669 * 670 * @see #setStandardTickUnits(TickUnitSource) 671 */ 672 public static TickUnitSource createIntegerTickUnits(Locale locale) { 673 NumberFormat numberFormat = NumberFormat.getNumberInstance(locale); 674 return new NumberTickUnitSource(true, numberFormat); 675 } 676 677 /** 678 * Estimates the maximum tick label height. 679 * 680 * @param g2 the graphics device. 681 * 682 * @return The maximum height. 683 */ 684 protected double estimateMaximumTickLabelHeight(Graphics2D g2) { 685 RectangleInsets tickLabelInsets = getTickLabelInsets(); 686 double result = tickLabelInsets.getTop() + tickLabelInsets.getBottom(); 687 688 Font tickLabelFont = getTickLabelFont(); 689 FontRenderContext frc = g2.getFontRenderContext(); 690 result += tickLabelFont.getLineMetrics("123", frc).getHeight(); 691 return result; 692 } 693 694 /** 695 * Estimates the maximum width of the tick labels, assuming the specified 696 * tick unit is used. 697 * <P> 698 * Rather than computing the string bounds of every tick on the axis, we 699 * just look at two values: the lower bound and the upper bound for the 700 * axis. These two values will usually be representative. 701 * 702 * @param g2 the graphics device. 703 * @param unit the tick unit to use for calculation. 704 * 705 * @return The estimated maximum width of the tick labels. 706 */ 707 protected double estimateMaximumTickLabelWidth(Graphics2D g2, 708 TickUnit unit) { 709 710 RectangleInsets tickLabelInsets = getTickLabelInsets(); 711 double result = tickLabelInsets.getLeft() + tickLabelInsets.getRight(); 712 713 if (isVerticalTickLabels()) { 714 // all tick labels have the same width (equal to the height of the 715 // font)... 716 FontRenderContext frc = g2.getFontRenderContext(); 717 LineMetrics lm = getTickLabelFont().getLineMetrics("0", frc); 718 result += lm.getHeight(); 719 } 720 else { 721 // look at lower and upper bounds... 722 FontMetrics fm = g2.getFontMetrics(getTickLabelFont()); 723 Range range = getRange(); 724 double lower = range.getLowerBound(); 725 double upper = range.getUpperBound(); 726 String lowerStr, upperStr; 727 NumberFormat formatter = getNumberFormatOverride(); 728 if (formatter != null) { 729 lowerStr = formatter.format(lower); 730 upperStr = formatter.format(upper); 731 } 732 else { 733 lowerStr = unit.valueToString(lower); 734 upperStr = unit.valueToString(upper); 735 } 736 double w1 = fm.stringWidth(lowerStr); 737 double w2 = fm.stringWidth(upperStr); 738 result += Math.max(w1, w2); 739 } 740 741 return result; 742 743 } 744 745 /** 746 * Selects an appropriate tick value for the axis. The strategy is to 747 * display as many ticks as possible (selected from an array of 'standard' 748 * tick units) without the labels overlapping. 749 * 750 * @param g2 the graphics device. 751 * @param dataArea the area defined by the axes. 752 * @param edge the axis location. 753 */ 754 protected void selectAutoTickUnit(Graphics2D g2, Rectangle2D dataArea, 755 RectangleEdge edge) { 756 757 if (RectangleEdge.isTopOrBottom(edge)) { 758 selectHorizontalAutoTickUnit(g2, dataArea, edge); 759 } 760 else if (RectangleEdge.isLeftOrRight(edge)) { 761 selectVerticalAutoTickUnit(g2, dataArea, edge); 762 } 763 764 } 765 766 /** 767 * Selects an appropriate tick value for the axis. The strategy is to 768 * display as many ticks as possible (selected from an array of 'standard' 769 * tick units) without the labels overlapping. 770 * 771 * @param g2 the graphics device. 772 * @param dataArea the area defined by the axes. 773 * @param edge the axis location. 774 */ 775 protected void selectHorizontalAutoTickUnit(Graphics2D g2, 776 Rectangle2D dataArea, RectangleEdge edge) { 777 778 TickUnit unit = getTickUnit(); 779 TickUnitSource tickUnitSource = getStandardTickUnits(); 780 781 // we should start with the current tick unit if it gives a count in 782 // the range 3 to 40 otherwise estimate one that will give a count <= 10 783 double length = getRange().getLength(); 784 int count = (int) (length / unit.getSize()); 785 if (count < 3 || count > 40) { 786 unit = tickUnitSource.getCeilingTickUnit(length / 10); 787 } 788 789 // now consider the label size relative to the width of the tick unit 790 // and make a guess at the ideal size 791 TickUnit unit1 = tickUnitSource.getCeilingTickUnit(unit); 792 double tickLabelWidth = estimateMaximumTickLabelWidth(g2, unit1); 793 double unit1Width = lengthToJava2D(unit1.getSize(), dataArea, edge); 794 NumberTickUnit unit2 = (NumberTickUnit) unit1; 795 double guess = (tickLabelWidth / unit1Width) * unit1.getSize(); 796 797 // due to limitations of double precision, when you zoom very far into 798 // a chart, eventually the visible axis range will get reported as 799 // having length 0, and then 'guess' above will be infinite ... in that 800 // case we'll just stick with the tick unit we have, it's better than 801 // throwing an exception 802 // https://github.com/jfree/jfreechart/issues/64 803 if (Double.isFinite(guess)) { 804 unit2 = (NumberTickUnit) tickUnitSource.getCeilingTickUnit(guess); 805 double unit2Width = lengthToJava2D(unit2.getSize(), dataArea, edge); 806 tickLabelWidth = estimateMaximumTickLabelWidth(g2, unit2); 807 if (tickLabelWidth > unit2Width) { 808 unit2 = (NumberTickUnit) tickUnitSource.getLargerTickUnit(unit2); 809 } 810 } 811 setTickUnit(unit2, false, false); 812 } 813 814 /** 815 * Selects an appropriate tick value for the axis. The strategy is to 816 * display as many ticks as possible (selected from an array of 'standard' 817 * tick units) without the labels overlapping. 818 * 819 * @param g2 the graphics device. 820 * @param dataArea the area in which the plot should be drawn. 821 * @param edge the axis location. 822 */ 823 protected void selectVerticalAutoTickUnit(Graphics2D g2, 824 Rectangle2D dataArea, RectangleEdge edge) { 825 826 double tickLabelHeight = estimateMaximumTickLabelHeight(g2); 827 828 // start with the current tick unit... 829 TickUnitSource tickUnits = getStandardTickUnits(); 830 TickUnit unit1 = tickUnits.getCeilingTickUnit(getTickUnit()); 831 double unitHeight = lengthToJava2D(unit1.getSize(), dataArea, edge); 832 double guess; 833 if (unitHeight > 0) { // then extrapolate... 834 guess = (tickLabelHeight / unitHeight) * unit1.getSize(); 835 } else { 836 guess = getRange().getLength() / 20.0; 837 } 838 NumberTickUnit unit2 = (NumberTickUnit) tickUnits.getCeilingTickUnit( 839 guess); 840 double unit2Height = lengthToJava2D(unit2.getSize(), dataArea, edge); 841 842 tickLabelHeight = estimateMaximumTickLabelHeight(g2); 843 if (tickLabelHeight > unit2Height) { 844 unit2 = (NumberTickUnit) tickUnits.getLargerTickUnit(unit2); 845 } 846 setTickUnit(unit2, false, false); 847 848 } 849 850 /** 851 * Calculates the positions of the tick labels for the axis, storing the 852 * results in the tick label list (ready for drawing). 853 * 854 * @param g2 the graphics device. 855 * @param state the axis state. 856 * @param dataArea the area in which the plot should be drawn. 857 * @param edge the location of the axis. 858 * 859 * @return A list of ticks. 860 */ 861 @Override 862 public List refreshTicks(Graphics2D g2, AxisState state, 863 Rectangle2D dataArea, RectangleEdge edge) { 864 865 List result = new java.util.ArrayList(); 866 if (RectangleEdge.isTopOrBottom(edge)) { 867 result = refreshTicksHorizontal(g2, dataArea, edge); 868 } 869 else if (RectangleEdge.isLeftOrRight(edge)) { 870 result = refreshTicksVertical(g2, dataArea, edge); 871 } 872 return result; 873 874 } 875 876 /** 877 * Calculates the positions of the tick labels for the axis, storing the 878 * results in the tick label list (ready for drawing). 879 * 880 * @param g2 the graphics device. 881 * @param dataArea the area in which the data should be drawn. 882 * @param edge the location of the axis. 883 * 884 * @return A list of ticks. 885 */ 886 protected List refreshTicksHorizontal(Graphics2D g2, 887 Rectangle2D dataArea, RectangleEdge edge) { 888 889 List result = new java.util.ArrayList(); 890 891 Font tickLabelFont = getTickLabelFont(); 892 g2.setFont(tickLabelFont); 893 894 if (isAutoTickUnitSelection()) { 895 selectAutoTickUnit(g2, dataArea, edge); 896 } 897 898 TickUnit tu = getTickUnit(); 899 double size = tu.getSize(); 900 int count = calculateVisibleTickCount(); 901 double lowestTickValue = calculateLowestVisibleTickValue(); 902 903 if (count <= ValueAxis.MAXIMUM_TICK_COUNT) { 904 int minorTickSpaces = getMinorTickCount(); 905 if (minorTickSpaces <= 0) { 906 minorTickSpaces = tu.getMinorTickCount(); 907 } 908 for (int minorTick = 1; minorTick < minorTickSpaces; minorTick++) { 909 double minorTickValue = lowestTickValue 910 - size * minorTick / minorTickSpaces; 911 if (getRange().contains(minorTickValue)) { 912 result.add(new NumberTick(TickType.MINOR, minorTickValue, 913 "", TextAnchor.TOP_CENTER, TextAnchor.CENTER, 914 0.0)); 915 } 916 } 917 for (int i = 0; i < count; i++) { 918 double currentTickValue = lowestTickValue + (i * size); 919 String tickLabel; 920 NumberFormat formatter = getNumberFormatOverride(); 921 if (formatter != null) { 922 tickLabel = formatter.format(currentTickValue); 923 } 924 else { 925 tickLabel = getTickUnit().valueToString(currentTickValue); 926 } 927 TextAnchor anchor, rotationAnchor; 928 double angle = 0.0; 929 if (isVerticalTickLabels()) { 930 anchor = TextAnchor.CENTER_RIGHT; 931 rotationAnchor = TextAnchor.CENTER_RIGHT; 932 if (edge == RectangleEdge.TOP) { 933 angle = Math.PI / 2.0; 934 } 935 else { 936 angle = -Math.PI / 2.0; 937 } 938 } 939 else { 940 if (edge == RectangleEdge.TOP) { 941 anchor = TextAnchor.BOTTOM_CENTER; 942 rotationAnchor = TextAnchor.BOTTOM_CENTER; 943 } 944 else { 945 anchor = TextAnchor.TOP_CENTER; 946 rotationAnchor = TextAnchor.TOP_CENTER; 947 } 948 } 949 950 Tick tick = new NumberTick(currentTickValue, tickLabel, anchor, 951 rotationAnchor, angle); 952 result.add(tick); 953 double nextTickValue = lowestTickValue + ((i + 1) * size); 954 for (int minorTick = 1; minorTick < minorTickSpaces; 955 minorTick++) { 956 double minorTickValue = currentTickValue 957 + (nextTickValue - currentTickValue) 958 * minorTick / minorTickSpaces; 959 if (getRange().contains(minorTickValue)) { 960 result.add(new NumberTick(TickType.MINOR, 961 minorTickValue, "", TextAnchor.TOP_CENTER, 962 TextAnchor.CENTER, 0.0)); 963 } 964 } 965 } 966 } 967 return result; 968 969 } 970 971 /** 972 * Calculates the positions of the tick labels for the axis, storing the 973 * results in the tick label list (ready for drawing). 974 * 975 * @param g2 the graphics device. 976 * @param dataArea the area in which the plot should be drawn. 977 * @param edge the location of the axis. 978 * 979 * @return A list of ticks. 980 */ 981 protected List refreshTicksVertical(Graphics2D g2, 982 Rectangle2D dataArea, RectangleEdge edge) { 983 984 List<Tick> result = new ArrayList<>(); 985 Font tickLabelFont = getTickLabelFont(); 986 g2.setFont(tickLabelFont); 987 if (isAutoTickUnitSelection()) { 988 selectAutoTickUnit(g2, dataArea, edge); 989 } 990 991 TickUnit tu = getTickUnit(); 992 double size = tu.getSize(); 993 int count = calculateVisibleTickCount(); 994 double lowestTickValue = calculateLowestVisibleTickValue(); 995 996 if (count <= ValueAxis.MAXIMUM_TICK_COUNT) { 997 int minorTickSpaces = getMinorTickCount(); 998 if (minorTickSpaces <= 0) { 999 minorTickSpaces = tu.getMinorTickCount(); 1000 } 1001 for (int minorTick = 1; minorTick < minorTickSpaces; minorTick++) { 1002 double minorTickValue = lowestTickValue 1003 - size * minorTick / minorTickSpaces; 1004 if (getRange().contains(minorTickValue)) { 1005 result.add(new NumberTick(TickType.MINOR, minorTickValue, 1006 "", TextAnchor.TOP_CENTER, TextAnchor.CENTER, 1007 0.0)); 1008 } 1009 } 1010 1011 for (int i = 0; i < count; i++) { 1012 double currentTickValue = lowestTickValue + (i * size); 1013 String tickLabel; 1014 NumberFormat formatter = getNumberFormatOverride(); 1015 if (formatter != null) { 1016 tickLabel = formatter.format(currentTickValue); 1017 } 1018 else { 1019 tickLabel = getTickUnit().valueToString(currentTickValue); 1020 } 1021 1022 TextAnchor anchor; 1023 TextAnchor rotationAnchor; 1024 double angle = 0.0; 1025 if (isVerticalTickLabels()) { 1026 if (edge == RectangleEdge.LEFT) { 1027 anchor = TextAnchor.BOTTOM_CENTER; 1028 rotationAnchor = TextAnchor.BOTTOM_CENTER; 1029 angle = -Math.PI / 2.0; 1030 } 1031 else { 1032 anchor = TextAnchor.BOTTOM_CENTER; 1033 rotationAnchor = TextAnchor.BOTTOM_CENTER; 1034 angle = Math.PI / 2.0; 1035 } 1036 } 1037 else { 1038 if (edge == RectangleEdge.LEFT) { 1039 anchor = TextAnchor.CENTER_RIGHT; 1040 rotationAnchor = TextAnchor.CENTER_RIGHT; 1041 } 1042 else { 1043 anchor = TextAnchor.CENTER_LEFT; 1044 rotationAnchor = TextAnchor.CENTER_LEFT; 1045 } 1046 } 1047 1048 Tick tick = new NumberTick(currentTickValue, tickLabel, anchor, 1049 rotationAnchor, angle); 1050 result.add(tick); 1051 1052 double nextTickValue = lowestTickValue + ((i + 1) * size); 1053 for (int minorTick = 1; minorTick < minorTickSpaces; 1054 minorTick++) { 1055 double minorTickValue = currentTickValue 1056 + (nextTickValue - currentTickValue) 1057 * minorTick / minorTickSpaces; 1058 if (getRange().contains(minorTickValue)) { 1059 result.add(new NumberTick(TickType.MINOR, 1060 minorTickValue, "", TextAnchor.TOP_CENTER, 1061 TextAnchor.CENTER, 0.0)); 1062 } 1063 } 1064 } 1065 } 1066 return result; 1067 1068 } 1069 1070 /** 1071 * Returns a clone of the axis. 1072 * 1073 * @return A clone 1074 * 1075 * @throws CloneNotSupportedException if some component of the axis does 1076 * not support cloning. 1077 */ 1078 @Override 1079 public Object clone() throws CloneNotSupportedException { 1080 NumberAxis clone = (NumberAxis) super.clone(); 1081 if (this.numberFormatOverride != null) { 1082 clone.numberFormatOverride 1083 = (NumberFormat) this.numberFormatOverride.clone(); 1084 } 1085 return clone; 1086 } 1087 1088 /** 1089 * Tests the axis for equality with an arbitrary object. 1090 * 1091 * @param obj the object ({@code null} permitted). 1092 * 1093 * @return A boolean. 1094 */ 1095 @Override 1096 public boolean equals(Object obj) { 1097 if (obj == this) { 1098 return true; 1099 } 1100 if (!(obj instanceof NumberAxis)) { 1101 return false; 1102 } 1103 NumberAxis that = (NumberAxis) obj; 1104 if (this.autoRangeIncludesZero != that.autoRangeIncludesZero) { 1105 return false; 1106 } 1107 if (this.autoRangeStickyZero != that.autoRangeStickyZero) { 1108 return false; 1109 } 1110 if (!Objects.equals(this.tickUnit, that.tickUnit)) { 1111 return false; 1112 } 1113 if (!Objects.equals(this.numberFormatOverride, 1114 that.numberFormatOverride)) { 1115 return false; 1116 } 1117 if (!this.rangeType.equals(that.rangeType)) { 1118 return false; 1119 } 1120 return super.equals(obj); 1121 } 1122 1123 /** 1124 * Returns a hash code for this object. 1125 * 1126 * @return A hash code. 1127 */ 1128 @Override 1129 public int hashCode() { 1130 return super.hashCode(); 1131 } 1132 1133}