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