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