001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2013, 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 * DateAxis.java 029 * ------------- 030 * (C) Copyright 2000-2013, by Object Refinery Limited and Contributors. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): Jonathan Nash; 034 * David Li; 035 * Michael Rauch; 036 * Bill Kelemen; 037 * Pawel Pabis; 038 * Chris Boek; 039 * Peter Kolb (patches 1934255 and 2603321); 040 * Andrew Mickish (patch 1870189); 041 * Fawad Halim (bug 2201869); 042 * 043 * Changes (from 23-Jun-2001) 044 * -------------------------- 045 * 23-Jun-2001 : Modified to work with null data source (DG); 046 * 18-Sep-2001 : Updated header (DG); 047 * 27-Nov-2001 : Changed constructors from public to protected, updated Javadoc 048 * comments (DG); 049 * 16-Jan-2002 : Added an optional crosshair, based on the implementation by 050 * Jonathan Nash (DG); 051 * 26-Feb-2002 : Updated import statements (DG); 052 * 22-Apr-2002 : Added a setRange() method (DG); 053 * 25-Jun-2002 : Removed redundant local variable (DG); 054 * 25-Jul-2002 : Changed order of parameters in ValueAxis constructor (DG); 055 * 21-Aug-2002 : The setTickUnit() method now turns off auto-tick unit 056 * selection (fix for bug id 528885) (DG); 057 * 05-Sep-2002 : Updated the constructors to reflect changes in the Axis 058 * class (DG); 059 * 18-Sep-2002 : Fixed errors reported by Checkstyle (DG); 060 * 25-Sep-2002 : Added new setRange() methods, and deprecated 061 * setAxisRange() (DG); 062 * 04-Oct-2002 : Changed auto tick selection to parallel number axis 063 * classes (DG); 064 * 24-Oct-2002 : Added a date format override (DG); 065 * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG); 066 * 14-Jan-2003 : Changed autoRangeMinimumSize from Number --> double, moved 067 * crosshair settings to the plot (DG); 068 * 15-Jan-2003 : Removed anchor date (DG); 069 * 20-Jan-2003 : Removed unnecessary constructors (DG); 070 * 26-Mar-2003 : Implemented Serializable (DG); 071 * 02-May-2003 : Added additional units to createStandardDateTickUnits() 072 * method, as suggested by mhilpert in bug report 723187 (DG); 073 * 13-May-2003 : Merged HorizontalDateAxis and VerticalDateAxis (DG); 074 * 24-May-2003 : Added support for underlying timeline for 075 * SegmentedTimeline (BK); 076 * 16-Jul-2003 : Applied patch from Pawel Pabis to fix overlapping dates (DG); 077 * 22-Jul-2003 : Applied patch from Pawel Pabis for monthly ticks (DG); 078 * 25-Jul-2003 : Fixed bug 777561 and 777586 (DG); 079 * 13-Aug-2003 : Implemented Cloneable and added equals() method (DG); 080 * 02-Sep-2003 : Fixes for bug report 790506 (DG); 081 * 04-Sep-2003 : Fixed tick label alignment when axis appears at the top (DG); 082 * 10-Sep-2003 : Fixes for segmented timeline (DG); 083 * 17-Sep-2003 : Fixed a layout bug when multiple domain axes are used (DG); 084 * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG); 085 * 07-Nov-2003 : Modified to use new tick classes (DG); 086 * 12-Nov-2003 : Modified tick labelling to use roll unit from DateTickUnit 087 * when a calculated tick value is hidden (which can occur in 088 * segmented date axes) (DG); 089 * 24-Nov-2003 : Fixed some problems with the auto tick unit selection, and 090 * fixed bug 846277 (labels missing for inverted axis) (DG); 091 * 30-Dec-2003 : Fixed bug in refreshTicksHorizontal() when start of time unit 092 * (ex. 1st of month) was hidden, causing infinite loop (BK); 093 * 13-Jan-2004 : Fixed bug in previousStandardDate() method (fix by Richard 094 * Wardle) (DG); 095 * 21-Jan-2004 : Renamed translateJava2DToValue --> java2DToValue, and 096 * translateValueToJava2D --> valueToJava2D (DG); 097 * 12-Mar-2004 : Fixed bug where date format override is ignored for vertical 098 * axis (DG); 099 * 16-Mar-2004 : Added plotState to draw() method (DG); 100 * 07-Apr-2004 : Changed string width calculation (DG); 101 * 21-Apr-2004 : Fixed bug in estimateMaximumTickLabelWidth() method (bug id 102 * 939148) (DG); 103 * 11-Jan-2005 : Removed deprecated methods in preparation for 1.0.0 104 * release (DG); 105 * 13-Jan-2005 : Fixed bug (see 106 * http://www.jfree.org/forum/viewtopic.php?t=11330) (DG); 107 * 21-Apr-2005 : Replaced Insets with RectangleInsets, removed redundant 108 * argument from selectAutoTickUnit() (DG); 109 * ------------- JFREECHART 1.0.x --------------------------------------------- 110 * 10-Feb-2006 : Added some API doc comments in respect of bug 821046 (DG); 111 * 19-Apr-2006 : Fixed bug 1472942 in equals() method (DG); 112 * 25-Sep-2006 : Fixed bug 1564977 missing tick labels (DG); 113 * 15-Jan-2007 : Added get/setTimeZone() suggested by 'skunk' (DG); 114 * 18-Jan-2007 : Fixed bug 1638678, time zone for calendar in 115 * previousStandardDate() (DG); 116 * 04-Apr-2007 : Use time zone in date calculations (CB); 117 * 19-Apr-2007 : Fix exceptions in setMinimum/MaximumDate() (DG); 118 * 03-May-2007 : Fixed minor bugs in previousStandardDate(), with new JUnit 119 * tests (DG); 120 * 21-Nov-2007 : Fixed warnings from FindBugs (DG); 121 * 01-Sep-2008 : Use new methods from DateRange, added fix for bug 122 * 2078057 (DG); 123 * 18-Sep-2008 : Added locale to go with timezone (DG); 124 * 25-Sep-2008 : Added minor tick support, see patch 1934255 by Peter Kolb (DG); 125 * 25-Nov-2008 : Added bug fix 2201869 by Fawad Halim (DG); 126 * 21-Jan-2009 : Check tickUnit for minor tick count (DG); 127 * 19-Mar-2009 : Added entity support - see patch 2603321 by Peter Kolb (DG); 128 * 08-Feb-2012 : Bugfix for endless-loop, bug 3484403 by rbrabe (MH); 129 * 25-Jul-2013 : Update event notification to use fireChangeEvent() (DG); 130 * 01-Aug-2013 : Added attributedLabel override to support superscripts, 131 * subscripts and more (DG); 132 * 12-Sep-2013 : Prevent exception when zooming in below 1 millisecond (DG); 133 * 23-Nov-2013 : Deprecated DEFAULT_DATE_TICK_UNIT to fix bug #977 (DG); 134 * 135 */ 136 137package org.jfree.chart.axis; 138 139import java.awt.Font; 140import java.awt.FontMetrics; 141import java.awt.Graphics2D; 142import java.awt.font.FontRenderContext; 143import java.awt.font.LineMetrics; 144import java.awt.geom.Rectangle2D; 145import java.io.Serializable; 146import java.text.DateFormat; 147import java.text.SimpleDateFormat; 148import java.util.Calendar; 149import java.util.Date; 150import java.util.List; 151import java.util.Locale; 152import java.util.TimeZone; 153 154import org.jfree.chart.event.AxisChangeEvent; 155import org.jfree.chart.plot.Plot; 156import org.jfree.chart.plot.PlotRenderingInfo; 157import org.jfree.chart.plot.ValueAxisPlot; 158import org.jfree.chart.util.ParamChecks; 159import org.jfree.data.Range; 160import org.jfree.data.time.DateRange; 161import org.jfree.data.time.Month; 162import org.jfree.data.time.RegularTimePeriod; 163import org.jfree.data.time.Year; 164import org.jfree.ui.RectangleEdge; 165import org.jfree.ui.RectangleInsets; 166import org.jfree.ui.TextAnchor; 167import org.jfree.util.ObjectUtilities; 168 169/** 170 * The base class for axes that display dates. You will find it easier to 171 * understand how this axis works if you bear in mind that it really 172 * displays/measures integer (or long) data, where the integers are 173 * milliseconds since midnight, 1-Jan-1970. When displaying tick labels, the 174 * millisecond values are converted back to dates using a 175 * <code>DateFormat</code> instance. 176 * <P> 177 * You can also create a {@link org.jfree.chart.axis.Timeline} and supply in 178 * the constructor to create an axis that only contains certain domain values. 179 * For example, this allows you to create a date axis that only contains 180 * working days. 181 */ 182public class DateAxis extends ValueAxis implements Cloneable, Serializable { 183 184 /** For serialization. */ 185 private static final long serialVersionUID = -1013460999649007604L; 186 187 /** The default axis range. */ 188 public static final DateRange DEFAULT_DATE_RANGE = new DateRange(); 189 190 /** The default minimum auto range size. */ 191 public static final double 192 DEFAULT_AUTO_RANGE_MINIMUM_SIZE_IN_MILLISECONDS = 2.0; 193 194 /** 195 * The default date tick unit. 196 * 197 * @deprecated As pointed out in bug #977, the SimpleDateFormat in this 198 * object uses Calendar which is not thread safe...so you should 199 * avoid reusing this instance and create a new instance as required. 200 */ 201 public static final DateTickUnit DEFAULT_DATE_TICK_UNIT 202 = new DateTickUnit(DateTickUnitType.DAY, 1, new SimpleDateFormat()); 203 204 /** The default anchor date. */ 205 public static final Date DEFAULT_ANCHOR_DATE = new Date(); 206 207 /** The current tick unit. */ 208 private DateTickUnit tickUnit; 209 210 /** The override date format. */ 211 private DateFormat dateFormatOverride; 212 213 /** 214 * Tick marks can be displayed at the start or the middle of the time 215 * period. 216 */ 217 private DateTickMarkPosition tickMarkPosition = DateTickMarkPosition.START; 218 219 /** 220 * A timeline that includes all milliseconds (as defined by 221 * <code>java.util.Date</code>) in the real time line. 222 */ 223 private static class DefaultTimeline implements Timeline, Serializable { 224 225 /** 226 * Converts a millisecond into a timeline value. 227 * 228 * @param millisecond the millisecond. 229 * 230 * @return The timeline value. 231 */ 232 @Override 233 public long toTimelineValue(long millisecond) { 234 return millisecond; 235 } 236 237 /** 238 * Converts a date into a timeline value. 239 * 240 * @param date the domain value. 241 * 242 * @return The timeline value. 243 */ 244 @Override 245 public long toTimelineValue(Date date) { 246 return date.getTime(); 247 } 248 249 /** 250 * Converts a timeline value into a millisecond (as encoded by 251 * <code>java.util.Date</code>). 252 * 253 * @param value the value. 254 * 255 * @return The millisecond. 256 */ 257 @Override 258 public long toMillisecond(long value) { 259 return value; 260 } 261 262 /** 263 * Returns <code>true</code> if the timeline includes the specified 264 * domain value. 265 * 266 * @param millisecond the millisecond. 267 * 268 * @return <code>true</code>. 269 */ 270 @Override 271 public boolean containsDomainValue(long millisecond) { 272 return true; 273 } 274 275 /** 276 * Returns <code>true</code> if the timeline includes the specified 277 * domain value. 278 * 279 * @param date the date. 280 * 281 * @return <code>true</code>. 282 */ 283 @Override 284 public boolean containsDomainValue(Date date) { 285 return true; 286 } 287 288 /** 289 * Returns <code>true</code> if the timeline includes the specified 290 * domain value range. 291 * 292 * @param from the start value. 293 * @param to the end value. 294 * 295 * @return <code>true</code>. 296 */ 297 @Override 298 public boolean containsDomainRange(long from, long to) { 299 return true; 300 } 301 302 /** 303 * Returns <code>true</code> if the timeline includes the specified 304 * domain value range. 305 * 306 * @param from the start date. 307 * @param to the end date. 308 * 309 * @return <code>true</code>. 310 */ 311 @Override 312 public boolean containsDomainRange(Date from, Date to) { 313 return true; 314 } 315 316 /** 317 * Tests an object for equality with this instance. 318 * 319 * @param object the object. 320 * 321 * @return A boolean. 322 */ 323 @Override 324 public boolean equals(Object object) { 325 if (object == null) { 326 return false; 327 } 328 if (object == this) { 329 return true; 330 } 331 if (object instanceof DefaultTimeline) { 332 return true; 333 } 334 return false; 335 } 336 } 337 338 /** A static default timeline shared by all standard DateAxis */ 339 private static final Timeline DEFAULT_TIMELINE = new DefaultTimeline(); 340 341 /** The time zone for the axis. */ 342 private TimeZone timeZone; 343 344 /** 345 * The locale for the axis (<code>null</code> is not permitted). 346 * 347 * @since 1.0.11 348 */ 349 private Locale locale; 350 351 /** Our underlying timeline. */ 352 private Timeline timeline; 353 354 /** 355 * Creates a date axis with no label. 356 */ 357 public DateAxis() { 358 this(null); 359 } 360 361 /** 362 * Creates a date axis with the specified label. 363 * 364 * @param label the axis label (<code>null</code> permitted). 365 */ 366 public DateAxis(String label) { 367 this(label, TimeZone.getDefault()); 368 } 369 370 /** 371 * Creates a date axis. A timeline is specified for the axis. This allows 372 * special transformations to occur between a domain of values and the 373 * values included in the axis. 374 * 375 * @see org.jfree.chart.axis.SegmentedTimeline 376 * 377 * @param label the axis label (<code>null</code> permitted). 378 * @param zone the time zone. 379 * 380 * @deprecated From 1.0.11 onwards, use {@link #DateAxis(String, TimeZone, 381 * Locale)} instead, to explicitly set the locale. 382 */ 383 public DateAxis(String label, TimeZone zone) { 384 this(label, zone, Locale.getDefault()); 385 } 386 387 /** 388 * Creates a date axis. A timeline is specified for the axis. This allows 389 * special transformations to occur between a domain of values and the 390 * values included in the axis. 391 * 392 * @see org.jfree.chart.axis.SegmentedTimeline 393 * 394 * @param label the axis label (<code>null</code> permitted). 395 * @param zone the time zone. 396 * @param locale the locale (<code>null</code> not permitted). 397 * 398 * @since 1.0.11 399 */ 400 public DateAxis(String label, TimeZone zone, Locale locale) { 401 super(label, DateAxis.createStandardDateTickUnits(zone, locale)); 402 this.tickUnit = new DateTickUnit(DateTickUnitType.DAY, 1, 403 new SimpleDateFormat()); 404 setAutoRangeMinimumSize( 405 DEFAULT_AUTO_RANGE_MINIMUM_SIZE_IN_MILLISECONDS); 406 setRange(DEFAULT_DATE_RANGE, false, false); 407 this.dateFormatOverride = null; 408 this.timeZone = zone; 409 this.locale = locale; 410 this.timeline = DEFAULT_TIMELINE; 411 } 412 413 /** 414 * Returns the time zone for the axis. 415 * 416 * @return The time zone (never <code>null</code>). 417 * 418 * @since 1.0.4 419 * 420 * @see #setTimeZone(TimeZone) 421 */ 422 public TimeZone getTimeZone() { 423 return this.timeZone; 424 } 425 426 /** 427 * Sets the time zone for the axis and sends an {@link AxisChangeEvent} to 428 * all registered listeners. 429 * 430 * @param zone the time zone (<code>null</code> not permitted). 431 * 432 * @since 1.0.4 433 * 434 * @see #getTimeZone() 435 */ 436 public void setTimeZone(TimeZone zone) { 437 ParamChecks.nullNotPermitted(zone, "zone"); 438 if (!this.timeZone.equals(zone)) { 439 this.timeZone = zone; 440 setStandardTickUnits(createStandardDateTickUnits(zone, 441 this.locale)); 442 fireChangeEvent(); 443 } 444 } 445 446 /** 447 * Returns the underlying timeline used by this axis. 448 * 449 * @return The timeline. 450 */ 451 public Timeline getTimeline() { 452 return this.timeline; 453 } 454 455 /** 456 * Sets the underlying timeline to use for this axis. If the timeline is 457 * changed, an {@link AxisChangeEvent} is sent to all registered listeners. 458 * 459 * @param timeline the timeline. 460 */ 461 public void setTimeline(Timeline timeline) { 462 if (this.timeline != timeline) { 463 this.timeline = timeline; 464 fireChangeEvent(); 465 } 466 } 467 468 /** 469 * Returns the tick unit for the axis. 470 * <p> 471 * Note: if the <code>autoTickUnitSelection</code> flag is 472 * <code>true</code> the tick unit may be changed while the axis is being 473 * drawn, so in that case the return value from this method may be 474 * irrelevant if the method is called before the axis has been drawn. 475 * 476 * @return The tick unit (possibly <code>null</code>). 477 * 478 * @see #setTickUnit(DateTickUnit) 479 * @see ValueAxis#isAutoTickUnitSelection() 480 */ 481 public DateTickUnit getTickUnit() { 482 return this.tickUnit; 483 } 484 485 /** 486 * Sets the tick unit for the axis. The auto-tick-unit-selection flag is 487 * set to <code>false</code>, and registered listeners are notified that 488 * the axis has been changed. 489 * 490 * @param unit the tick unit. 491 * 492 * @see #getTickUnit() 493 * @see #setTickUnit(DateTickUnit, boolean, boolean) 494 */ 495 public void setTickUnit(DateTickUnit unit) { 496 setTickUnit(unit, true, true); 497 } 498 499 /** 500 * Sets the tick unit attribute and, if requested, sends an 501 * {@link AxisChangeEvent} to all registered listeners. 502 * 503 * @param unit the new tick unit. 504 * @param notify notify registered listeners? 505 * @param turnOffAutoSelection turn off auto selection? 506 * 507 * @see #getTickUnit() 508 */ 509 public void setTickUnit(DateTickUnit unit, boolean notify, 510 boolean turnOffAutoSelection) { 511 512 this.tickUnit = unit; 513 if (turnOffAutoSelection) { 514 setAutoTickUnitSelection(false, false); 515 } 516 if (notify) { 517 fireChangeEvent(); 518 } 519 520 } 521 522 /** 523 * Returns the date format override. If this is non-null, then it will be 524 * used to format the dates on the axis. 525 * 526 * @return The formatter (possibly <code>null</code>). 527 */ 528 public DateFormat getDateFormatOverride() { 529 return this.dateFormatOverride; 530 } 531 532 /** 533 * Sets the date format override and sends an {@link AxisChangeEvent} to 534 * all registered listeners. If this is non-null, then it will be 535 * used to format the dates on the axis. 536 * 537 * @param formatter the date formatter (<code>null</code> permitted). 538 */ 539 public void setDateFormatOverride(DateFormat formatter) { 540 this.dateFormatOverride = formatter; 541 fireChangeEvent(); 542 } 543 544 /** 545 * Sets the upper and lower bounds for the axis and sends an 546 * {@link AxisChangeEvent} to all registered listeners. As a side-effect, 547 * the auto-range flag is set to false. 548 * 549 * @param range the new range (<code>null</code> not permitted). 550 */ 551 @Override 552 public void setRange(Range range) { 553 setRange(range, true, true); 554 } 555 556 /** 557 * Sets the range for the axis, if requested, sends an 558 * {@link AxisChangeEvent} to all registered listeners. As a side-effect, 559 * the auto-range flag is set to <code>false</code> (optional). 560 * 561 * @param range the range (<code>null</code> not permitted). 562 * @param turnOffAutoRange a flag that controls whether or not the auto 563 * range is turned off. 564 * @param notify a flag that controls whether or not listeners are 565 * notified. 566 */ 567 @Override 568 public void setRange(Range range, boolean turnOffAutoRange, 569 boolean notify) { 570 ParamChecks.nullNotPermitted(range, "range"); 571 // usually the range will be a DateRange, but if it isn't do a 572 // conversion... 573 if (!(range instanceof DateRange)) { 574 range = new DateRange(range); 575 } 576 super.setRange(range, turnOffAutoRange, notify); 577 } 578 579 /** 580 * Sets the axis range and sends an {@link AxisChangeEvent} to all 581 * registered listeners. 582 * 583 * @param lower the lower bound for the axis. 584 * @param upper the upper bound for the axis. 585 */ 586 public void setRange(Date lower, Date upper) { 587 if (lower.getTime() >= upper.getTime()) { 588 throw new IllegalArgumentException("Requires 'lower' < 'upper'."); 589 } 590 setRange(new DateRange(lower, upper)); 591 } 592 593 /** 594 * Sets the axis range and sends an {@link AxisChangeEvent} to all 595 * registered listeners. 596 * 597 * @param lower the lower bound for the axis. 598 * @param upper the upper bound for the axis. 599 */ 600 @Override 601 public void setRange(double lower, double upper) { 602 if (lower >= upper) { 603 throw new IllegalArgumentException("Requires 'lower' < 'upper'."); 604 } 605 setRange(new DateRange(lower, upper)); 606 } 607 608 /** 609 * Returns the earliest date visible on the axis. 610 * 611 * @return The date. 612 * 613 * @see #setMinimumDate(Date) 614 * @see #getMaximumDate() 615 */ 616 public Date getMinimumDate() { 617 Date result; 618 Range range = getRange(); 619 if (range instanceof DateRange) { 620 DateRange r = (DateRange) range; 621 result = r.getLowerDate(); 622 } 623 else { 624 result = new Date((long) range.getLowerBound()); 625 } 626 return result; 627 } 628 629 /** 630 * Sets the minimum date visible on the axis and sends an 631 * {@link AxisChangeEvent} to all registered listeners. If 632 * <code>date</code> is on or after the current maximum date for 633 * the axis, the maximum date will be shifted to preserve the current 634 * length of the axis. 635 * 636 * @param date the date (<code>null</code> not permitted). 637 * 638 * @see #getMinimumDate() 639 * @see #setMaximumDate(Date) 640 */ 641 public void setMinimumDate(Date date) { 642 ParamChecks.nullNotPermitted(date, "date"); 643 // check the new minimum date relative to the current maximum date 644 Date maxDate = getMaximumDate(); 645 long maxMillis = maxDate.getTime(); 646 long newMinMillis = date.getTime(); 647 if (maxMillis <= newMinMillis) { 648 Date oldMin = getMinimumDate(); 649 long length = maxMillis - oldMin.getTime(); 650 maxDate = new Date(newMinMillis + length); 651 } 652 setRange(new DateRange(date, maxDate), true, false); 653 fireChangeEvent(); 654 } 655 656 /** 657 * Returns the latest date visible on the axis. 658 * 659 * @return The date. 660 * 661 * @see #setMaximumDate(Date) 662 * @see #getMinimumDate() 663 */ 664 public Date getMaximumDate() { 665 Date result; 666 Range range = getRange(); 667 if (range instanceof DateRange) { 668 DateRange r = (DateRange) range; 669 result = r.getUpperDate(); 670 } 671 else { 672 result = new Date((long) range.getUpperBound()); 673 } 674 return result; 675 } 676 677 /** 678 * Sets the maximum date visible on the axis and sends an 679 * {@link AxisChangeEvent} to all registered listeners. If 680 * <code>maximumDate</code> is on or before the current minimum date for 681 * the axis, the minimum date will be shifted to preserve the current 682 * length of the axis. 683 * 684 * @param maximumDate the date (<code>null</code> not permitted). 685 * 686 * @see #getMinimumDate() 687 * @see #setMinimumDate(Date) 688 */ 689 public void setMaximumDate(Date maximumDate) { 690 ParamChecks.nullNotPermitted(maximumDate, "maximumDate"); 691 // check the new maximum date relative to the current minimum date 692 Date minDate = getMinimumDate(); 693 long minMillis = minDate.getTime(); 694 long newMaxMillis = maximumDate.getTime(); 695 if (minMillis >= newMaxMillis) { 696 Date oldMax = getMaximumDate(); 697 long length = oldMax.getTime() - minMillis; 698 minDate = new Date(newMaxMillis - length); 699 } 700 setRange(new DateRange(minDate, maximumDate), true, false); 701 fireChangeEvent(); 702 } 703 704 /** 705 * Returns the tick mark position (start, middle or end of the time period). 706 * 707 * @return The position (never <code>null</code>). 708 */ 709 public DateTickMarkPosition getTickMarkPosition() { 710 return this.tickMarkPosition; 711 } 712 713 /** 714 * Sets the tick mark position (start, middle or end of the time period) 715 * and sends an {@link AxisChangeEvent} to all registered listeners. 716 * 717 * @param position the position (<code>null</code> not permitted). 718 */ 719 public void setTickMarkPosition(DateTickMarkPosition position) { 720 ParamChecks.nullNotPermitted(position, "position"); 721 this.tickMarkPosition = position; 722 fireChangeEvent(); 723 } 724 725 /** 726 * Configures the axis to work with the specified plot. If the axis has 727 * auto-scaling, then sets the maximum and minimum values. 728 */ 729 @Override 730 public void configure() { 731 if (isAutoRange()) { 732 autoAdjustRange(); 733 } 734 } 735 736 /** 737 * Returns <code>true</code> if the axis hides this value, and 738 * <code>false</code> otherwise. 739 * 740 * @param millis the data value. 741 * 742 * @return A value. 743 */ 744 public boolean isHiddenValue(long millis) { 745 return (!this.timeline.containsDomainValue(new Date(millis))); 746 } 747 748 /** 749 * Translates the data value to the display coordinates (Java 2D User Space) 750 * of the chart. 751 * 752 * @param value the date to be plotted. 753 * @param area the rectangle (in Java2D space) where the data is to be 754 * plotted. 755 * @param edge the axis location. 756 * 757 * @return The coordinate corresponding to the supplied data value. 758 */ 759 @Override 760 public double valueToJava2D(double value, Rectangle2D area, 761 RectangleEdge edge) { 762 763 value = this.timeline.toTimelineValue((long) value); 764 765 DateRange range = (DateRange) getRange(); 766 double axisMin = this.timeline.toTimelineValue(range.getLowerMillis()); 767 double axisMax = this.timeline.toTimelineValue(range.getUpperMillis()); 768 double result = 0.0; 769 if (RectangleEdge.isTopOrBottom(edge)) { 770 double minX = area.getX(); 771 double maxX = area.getMaxX(); 772 if (isInverted()) { 773 result = maxX + ((value - axisMin) / (axisMax - axisMin)) 774 * (minX - maxX); 775 } 776 else { 777 result = minX + ((value - axisMin) / (axisMax - axisMin)) 778 * (maxX - minX); 779 } 780 } 781 else if (RectangleEdge.isLeftOrRight(edge)) { 782 double minY = area.getMinY(); 783 double maxY = area.getMaxY(); 784 if (isInverted()) { 785 result = minY + (((value - axisMin) / (axisMax - axisMin)) 786 * (maxY - minY)); 787 } 788 else { 789 result = maxY - (((value - axisMin) / (axisMax - axisMin)) 790 * (maxY - minY)); 791 } 792 } 793 return result; 794 } 795 796 /** 797 * Translates a date to Java2D coordinates, based on the range displayed by 798 * this axis for the specified data area. 799 * 800 * @param date the date. 801 * @param area the rectangle (in Java2D space) where the data is to be 802 * plotted. 803 * @param edge the axis location. 804 * 805 * @return The coordinate corresponding to the supplied date. 806 */ 807 public double dateToJava2D(Date date, Rectangle2D area, 808 RectangleEdge edge) { 809 double value = date.getTime(); 810 return valueToJava2D(value, area, edge); 811 } 812 813 /** 814 * Translates a Java2D coordinate into the corresponding data value. To 815 * perform this translation, you need to know the area used for plotting 816 * data, and which edge the axis is located on. 817 * 818 * @param java2DValue the coordinate in Java2D space. 819 * @param area the rectangle (in Java2D space) where the data is to be 820 * plotted. 821 * @param edge the axis location. 822 * 823 * @return A data value. 824 */ 825 @Override 826 public double java2DToValue(double java2DValue, Rectangle2D area, 827 RectangleEdge edge) { 828 829 DateRange range = (DateRange) getRange(); 830 double axisMin = this.timeline.toTimelineValue(range.getLowerMillis()); 831 double axisMax = this.timeline.toTimelineValue(range.getUpperMillis()); 832 833 double min = 0.0; 834 double max = 0.0; 835 if (RectangleEdge.isTopOrBottom(edge)) { 836 min = area.getX(); 837 max = area.getMaxX(); 838 } 839 else if (RectangleEdge.isLeftOrRight(edge)) { 840 min = area.getMaxY(); 841 max = area.getY(); 842 } 843 844 double result; 845 if (isInverted()) { 846 result = axisMax - ((java2DValue - min) / (max - min) 847 * (axisMax - axisMin)); 848 } 849 else { 850 result = axisMin + ((java2DValue - min) / (max - min) 851 * (axisMax - axisMin)); 852 } 853 854 return this.timeline.toMillisecond((long) result); 855 } 856 857 /** 858 * Calculates the value of the lowest visible tick on the axis. 859 * 860 * @param unit date unit to use. 861 * 862 * @return The value of the lowest visible tick on the axis. 863 */ 864 public Date calculateLowestVisibleTickValue(DateTickUnit unit) { 865 return nextStandardDate(getMinimumDate(), unit); 866 } 867 868 /** 869 * Calculates the value of the highest visible tick on the axis. 870 * 871 * @param unit date unit to use. 872 * 873 * @return The value of the highest visible tick on the axis. 874 */ 875 public Date calculateHighestVisibleTickValue(DateTickUnit unit) { 876 return previousStandardDate(getMaximumDate(), unit); 877 } 878 879 /** 880 * Returns the previous "standard" date, for a given date and tick unit. 881 * 882 * @param date the reference date. 883 * @param unit the tick unit. 884 * 885 * @return The previous "standard" date. 886 */ 887 protected Date previousStandardDate(Date date, DateTickUnit unit) { 888 889 int milliseconds; 890 int seconds; 891 int minutes; 892 int hours; 893 int days; 894 int months; 895 int years; 896 897 Calendar calendar = Calendar.getInstance(this.timeZone, this.locale); 898 calendar.setTime(date); 899 int count = unit.getCount(); 900 int current = calendar.get(unit.getCalendarField()); 901 int value = count * (current / count); 902 903 switch (unit.getUnit()) { 904 905 case DateTickUnit.MILLISECOND : 906 years = calendar.get(Calendar.YEAR); 907 months = calendar.get(Calendar.MONTH); 908 days = calendar.get(Calendar.DATE); 909 hours = calendar.get(Calendar.HOUR_OF_DAY); 910 minutes = calendar.get(Calendar.MINUTE); 911 seconds = calendar.get(Calendar.SECOND); 912 calendar.set(years, months, days, hours, minutes, seconds); 913 calendar.set(Calendar.MILLISECOND, value); 914 Date mm = calendar.getTime(); 915 if (mm.getTime() >= date.getTime()) { 916 calendar.set(Calendar.MILLISECOND, value - 1); 917 mm = calendar.getTime(); 918 } 919 return mm; 920 921 case DateTickUnit.SECOND : 922 years = calendar.get(Calendar.YEAR); 923 months = calendar.get(Calendar.MONTH); 924 days = calendar.get(Calendar.DATE); 925 hours = calendar.get(Calendar.HOUR_OF_DAY); 926 minutes = calendar.get(Calendar.MINUTE); 927 if (this.tickMarkPosition == DateTickMarkPosition.START) { 928 milliseconds = 0; 929 } 930 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) { 931 milliseconds = 500; 932 } 933 else { 934 milliseconds = 999; 935 } 936 calendar.set(Calendar.MILLISECOND, milliseconds); 937 calendar.set(years, months, days, hours, minutes, value); 938 Date dd = calendar.getTime(); 939 if (dd.getTime() >= date.getTime()) { 940 calendar.set(Calendar.SECOND, value - 1); 941 dd = calendar.getTime(); 942 } 943 return dd; 944 945 case DateTickUnit.MINUTE : 946 years = calendar.get(Calendar.YEAR); 947 months = calendar.get(Calendar.MONTH); 948 days = calendar.get(Calendar.DATE); 949 hours = calendar.get(Calendar.HOUR_OF_DAY); 950 if (this.tickMarkPosition == DateTickMarkPosition.START) { 951 seconds = 0; 952 } 953 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) { 954 seconds = 30; 955 } 956 else { 957 seconds = 59; 958 } 959 calendar.clear(Calendar.MILLISECOND); 960 calendar.set(years, months, days, hours, value, seconds); 961 Date d0 = calendar.getTime(); 962 if (d0.getTime() >= date.getTime()) { 963 calendar.set(Calendar.MINUTE, value - 1); 964 d0 = calendar.getTime(); 965 } 966 return d0; 967 968 case DateTickUnit.HOUR : 969 years = calendar.get(Calendar.YEAR); 970 months = calendar.get(Calendar.MONTH); 971 days = calendar.get(Calendar.DATE); 972 if (this.tickMarkPosition == DateTickMarkPosition.START) { 973 minutes = 0; 974 seconds = 0; 975 } 976 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) { 977 minutes = 30; 978 seconds = 0; 979 } 980 else { 981 minutes = 59; 982 seconds = 59; 983 } 984 calendar.clear(Calendar.MILLISECOND); 985 calendar.set(years, months, days, value, minutes, seconds); 986 Date d1 = calendar.getTime(); 987 if (d1.getTime() >= date.getTime()) { 988 calendar.set(Calendar.HOUR_OF_DAY, value - 1); 989 d1 = calendar.getTime(); 990 } 991 return d1; 992 993 case DateTickUnit.DAY : 994 years = calendar.get(Calendar.YEAR); 995 months = calendar.get(Calendar.MONTH); 996 if (this.tickMarkPosition == DateTickMarkPosition.START) { 997 hours = 0; 998 } 999 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) { 1000 hours = 12; 1001 } 1002 else { 1003 hours = 23; 1004 } 1005 calendar.clear(Calendar.MILLISECOND); 1006 calendar.set(years, months, value, hours, 0, 0); 1007 // long result = calendar.getTimeInMillis(); 1008 // won't work with JDK 1.3 1009 Date d2 = calendar.getTime(); 1010 if (d2.getTime() >= date.getTime()) { 1011 calendar.set(Calendar.DATE, value - 1); 1012 d2 = calendar.getTime(); 1013 } 1014 return d2; 1015 1016 case DateTickUnit.MONTH : 1017 years = calendar.get(Calendar.YEAR); 1018 calendar.clear(Calendar.MILLISECOND); 1019 calendar.set(years, value, 1, 0, 0, 0); 1020 Month month = new Month(calendar.getTime(), this.timeZone, 1021 this.locale); 1022 Date standardDate = calculateDateForPosition( 1023 month, this.tickMarkPosition); 1024 long millis = standardDate.getTime(); 1025 if (millis >= date.getTime()) { 1026 month = (Month) month.previous(); 1027 // need to peg the month in case the time zone isn't the 1028 // default - see bug 2078057 1029 month.peg(Calendar.getInstance(this.timeZone)); 1030 standardDate = calculateDateForPosition( 1031 month, this.tickMarkPosition); 1032 } 1033 return standardDate; 1034 1035 case DateTickUnit.YEAR : 1036 if (this.tickMarkPosition == DateTickMarkPosition.START) { 1037 months = 0; 1038 days = 1; 1039 } 1040 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) { 1041 months = 6; 1042 days = 1; 1043 } 1044 else { 1045 months = 11; 1046 days = 31; 1047 } 1048 calendar.clear(Calendar.MILLISECOND); 1049 calendar.set(value, months, days, 0, 0, 0); 1050 Date d3 = calendar.getTime(); 1051 if (d3.getTime() >= date.getTime()) { 1052 calendar.set(Calendar.YEAR, value - 1); 1053 d3 = calendar.getTime(); 1054 } 1055 return d3; 1056 1057 default: return null; 1058 1059 } 1060 1061 } 1062 1063 /** 1064 * Returns a {@link java.util.Date} corresponding to the specified position 1065 * within a {@link RegularTimePeriod}. 1066 * 1067 * @param period the period. 1068 * @param position the position (<code>null</code> not permitted). 1069 * 1070 * @return A date. 1071 */ 1072 private Date calculateDateForPosition(RegularTimePeriod period, 1073 DateTickMarkPosition position) { 1074 ParamChecks.nullNotPermitted(period, "period"); 1075 Date result = null; 1076 if (position == DateTickMarkPosition.START) { 1077 result = new Date(period.getFirstMillisecond()); 1078 } 1079 else if (position == DateTickMarkPosition.MIDDLE) { 1080 result = new Date(period.getMiddleMillisecond()); 1081 } 1082 else if (position == DateTickMarkPosition.END) { 1083 result = new Date(period.getLastMillisecond()); 1084 } 1085 return result; 1086 1087 } 1088 1089 /** 1090 * Returns the first "standard" date (based on the specified field and 1091 * units). 1092 * 1093 * @param date the reference date. 1094 * @param unit the date tick unit. 1095 * 1096 * @return The next "standard" date. 1097 */ 1098 protected Date nextStandardDate(Date date, DateTickUnit unit) { 1099 Date previous = previousStandardDate(date, unit); 1100 Calendar calendar = Calendar.getInstance(this.timeZone, this.locale); 1101 calendar.setTime(previous); 1102 calendar.add(unit.getCalendarField(), unit.getMultiple()); 1103 return calendar.getTime(); 1104 } 1105 1106 /** 1107 * Returns a collection of standard date tick units that uses the default 1108 * time zone. This collection will be used by default, but you are free 1109 * to create your own collection if you want to (see the 1110 * {@link ValueAxis#setStandardTickUnits(TickUnitSource)} method inherited 1111 * from the {@link ValueAxis} class). 1112 * 1113 * @return A collection of standard date tick units. 1114 */ 1115 public static TickUnitSource createStandardDateTickUnits() { 1116 return createStandardDateTickUnits(TimeZone.getDefault(), 1117 Locale.getDefault()); 1118 } 1119 1120 /** 1121 * Returns a collection of standard date tick units. This collection will 1122 * be used by default, but you are free to create your own collection if 1123 * you want to (see the 1124 * {@link ValueAxis#setStandardTickUnits(TickUnitSource)} method inherited 1125 * from the {@link ValueAxis} class). 1126 * 1127 * @param zone the time zone (<code>null</code> not permitted). 1128 * @param locale the locale (<code>null</code> not permitted). 1129 * 1130 * @return A collection of standard date tick units. 1131 * 1132 * @since 1.0.11 1133 */ 1134 public static TickUnitSource createStandardDateTickUnits(TimeZone zone, 1135 Locale locale) { 1136 1137 ParamChecks.nullNotPermitted(zone, "zone"); 1138 ParamChecks.nullNotPermitted(locale, "locale"); 1139 TickUnits units = new TickUnits(); 1140 1141 // date formatters 1142 DateFormat f1 = new SimpleDateFormat("HH:mm:ss.SSS", locale); 1143 DateFormat f2 = new SimpleDateFormat("HH:mm:ss", locale); 1144 DateFormat f3 = new SimpleDateFormat("HH:mm", locale); 1145 DateFormat f4 = new SimpleDateFormat("d-MMM, HH:mm", locale); 1146 DateFormat f5 = new SimpleDateFormat("d-MMM", locale); 1147 DateFormat f6 = new SimpleDateFormat("MMM-yyyy", locale); 1148 DateFormat f7 = new SimpleDateFormat("yyyy", locale); 1149 1150 f1.setTimeZone(zone); 1151 f2.setTimeZone(zone); 1152 f3.setTimeZone(zone); 1153 f4.setTimeZone(zone); 1154 f5.setTimeZone(zone); 1155 f6.setTimeZone(zone); 1156 f7.setTimeZone(zone); 1157 1158 // milliseconds 1159 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 1, f1)); 1160 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 5, 1161 DateTickUnitType.MILLISECOND, 1, f1)); 1162 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 10, 1163 DateTickUnitType.MILLISECOND, 1, f1)); 1164 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 25, 1165 DateTickUnitType.MILLISECOND, 5, f1)); 1166 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 50, 1167 DateTickUnitType.MILLISECOND, 10, f1)); 1168 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 100, 1169 DateTickUnitType.MILLISECOND, 10, f1)); 1170 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 250, 1171 DateTickUnitType.MILLISECOND, 10, f1)); 1172 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 500, 1173 DateTickUnitType.MILLISECOND, 50, f1)); 1174 1175 // seconds 1176 units.add(new DateTickUnit(DateTickUnitType.SECOND, 1, 1177 DateTickUnitType.MILLISECOND, 50, f2)); 1178 units.add(new DateTickUnit(DateTickUnitType.SECOND, 5, 1179 DateTickUnitType.SECOND, 1, f2)); 1180 units.add(new DateTickUnit(DateTickUnitType.SECOND, 10, 1181 DateTickUnitType.SECOND, 1, f2)); 1182 units.add(new DateTickUnit(DateTickUnitType.SECOND, 30, 1183 DateTickUnitType.SECOND, 5, f2)); 1184 1185 // minutes 1186 units.add(new DateTickUnit(DateTickUnitType.MINUTE, 1, 1187 DateTickUnitType.SECOND, 5, f3)); 1188 units.add(new DateTickUnit(DateTickUnitType.MINUTE, 2, 1189 DateTickUnitType.SECOND, 10, f3)); 1190 units.add(new DateTickUnit(DateTickUnitType.MINUTE, 5, 1191 DateTickUnitType.MINUTE, 1, f3)); 1192 units.add(new DateTickUnit(DateTickUnitType.MINUTE, 10, 1193 DateTickUnitType.MINUTE, 1, f3)); 1194 units.add(new DateTickUnit(DateTickUnitType.MINUTE, 15, 1195 DateTickUnitType.MINUTE, 5, f3)); 1196 units.add(new DateTickUnit(DateTickUnitType.MINUTE, 20, 1197 DateTickUnitType.MINUTE, 5, f3)); 1198 units.add(new DateTickUnit(DateTickUnitType.MINUTE, 30, 1199 DateTickUnitType.MINUTE, 5, f3)); 1200 1201 // hours 1202 units.add(new DateTickUnit(DateTickUnitType.HOUR, 1, 1203 DateTickUnitType.MINUTE, 5, f3)); 1204 units.add(new DateTickUnit(DateTickUnitType.HOUR, 2, 1205 DateTickUnitType.MINUTE, 10, f3)); 1206 units.add(new DateTickUnit(DateTickUnitType.HOUR, 4, 1207 DateTickUnitType.MINUTE, 30, f3)); 1208 units.add(new DateTickUnit(DateTickUnitType.HOUR, 6, 1209 DateTickUnitType.HOUR, 1, f3)); 1210 units.add(new DateTickUnit(DateTickUnitType.HOUR, 12, 1211 DateTickUnitType.HOUR, 1, f4)); 1212 1213 // days 1214 units.add(new DateTickUnit(DateTickUnitType.DAY, 1, 1215 DateTickUnitType.HOUR, 1, f5)); 1216 units.add(new DateTickUnit(DateTickUnitType.DAY, 2, 1217 DateTickUnitType.HOUR, 1, f5)); 1218 units.add(new DateTickUnit(DateTickUnitType.DAY, 7, 1219 DateTickUnitType.DAY, 1, f5)); 1220 units.add(new DateTickUnit(DateTickUnitType.DAY, 15, 1221 DateTickUnitType.DAY, 1, f5)); 1222 1223 // months 1224 units.add(new DateTickUnit(DateTickUnitType.MONTH, 1, 1225 DateTickUnitType.DAY, 1, f6)); 1226 units.add(new DateTickUnit(DateTickUnitType.MONTH, 2, 1227 DateTickUnitType.DAY, 1, f6)); 1228 units.add(new DateTickUnit(DateTickUnitType.MONTH, 3, 1229 DateTickUnitType.MONTH, 1, f6)); 1230 units.add(new DateTickUnit(DateTickUnitType.MONTH, 4, 1231 DateTickUnitType.MONTH, 1, f6)); 1232 units.add(new DateTickUnit(DateTickUnitType.MONTH, 6, 1233 DateTickUnitType.MONTH, 1, f6)); 1234 1235 // years 1236 units.add(new DateTickUnit(DateTickUnitType.YEAR, 1, 1237 DateTickUnitType.MONTH, 1, f7)); 1238 units.add(new DateTickUnit(DateTickUnitType.YEAR, 2, 1239 DateTickUnitType.MONTH, 3, f7)); 1240 units.add(new DateTickUnit(DateTickUnitType.YEAR, 5, 1241 DateTickUnitType.YEAR, 1, f7)); 1242 units.add(new DateTickUnit(DateTickUnitType.YEAR, 10, 1243 DateTickUnitType.YEAR, 1, f7)); 1244 units.add(new DateTickUnit(DateTickUnitType.YEAR, 25, 1245 DateTickUnitType.YEAR, 5, f7)); 1246 units.add(new DateTickUnit(DateTickUnitType.YEAR, 50, 1247 DateTickUnitType.YEAR, 10, f7)); 1248 units.add(new DateTickUnit(DateTickUnitType.YEAR, 100, 1249 DateTickUnitType.YEAR, 20, f7)); 1250 1251 return units; 1252 1253 } 1254 1255 /** 1256 * Rescales the axis to ensure that all data is visible. 1257 */ 1258 @Override 1259 protected void autoAdjustRange() { 1260 1261 Plot plot = getPlot(); 1262 1263 if (plot == null) { 1264 return; // no plot, no data 1265 } 1266 1267 if (plot instanceof ValueAxisPlot) { 1268 ValueAxisPlot vap = (ValueAxisPlot) plot; 1269 1270 Range r = vap.getDataRange(this); 1271 if (r == null) { 1272 if (this.timeline instanceof SegmentedTimeline) { 1273 //Timeline hasn't method getStartTime() 1274 r = new DateRange(( 1275 (SegmentedTimeline) this.timeline).getStartTime(), 1276 ((SegmentedTimeline) this.timeline).getStartTime() 1277 + 1); 1278 } 1279 else { 1280 r = new DateRange(); 1281 } 1282 } 1283 1284 long upper = this.timeline.toTimelineValue( 1285 (long) r.getUpperBound()); 1286 long lower; 1287 long fixedAutoRange = (long) getFixedAutoRange(); 1288 if (fixedAutoRange > 0.0) { 1289 lower = upper - fixedAutoRange; 1290 } 1291 else { 1292 lower = this.timeline.toTimelineValue((long) r.getLowerBound()); 1293 double range = upper - lower; 1294 long minRange = (long) getAutoRangeMinimumSize(); 1295 if (range < minRange) { 1296 long expand = (long) (minRange - range) / 2; 1297 upper = upper + expand; 1298 lower = lower - expand; 1299 } 1300 upper = upper + (long) (range * getUpperMargin()); 1301 lower = lower - (long) (range * getLowerMargin()); 1302 } 1303 1304 upper = this.timeline.toMillisecond(upper); 1305 lower = this.timeline.toMillisecond(lower); 1306 DateRange dr = new DateRange(new Date(lower), new Date(upper)); 1307 setRange(dr, false, false); 1308 } 1309 1310 } 1311 1312 /** 1313 * Selects an appropriate tick value for the axis. The strategy is to 1314 * display as many ticks as possible (selected from an array of 'standard' 1315 * tick units) without the labels overlapping. 1316 * 1317 * @param g2 the graphics device. 1318 * @param dataArea the area defined by the axes. 1319 * @param edge the axis location. 1320 */ 1321 protected void selectAutoTickUnit(Graphics2D g2, Rectangle2D dataArea, 1322 RectangleEdge edge) { 1323 1324 if (RectangleEdge.isTopOrBottom(edge)) { 1325 selectHorizontalAutoTickUnit(g2, dataArea, edge); 1326 } 1327 else if (RectangleEdge.isLeftOrRight(edge)) { 1328 selectVerticalAutoTickUnit(g2, dataArea, edge); 1329 } 1330 1331 } 1332 1333 /** 1334 * Selects an appropriate tick size for the axis. The strategy is to 1335 * display as many ticks as possible (selected from a collection of 1336 * 'standard' tick units) without the labels overlapping. 1337 * 1338 * @param g2 the graphics device. 1339 * @param dataArea the area defined by the axes. 1340 * @param edge the axis location. 1341 */ 1342 protected void selectHorizontalAutoTickUnit(Graphics2D g2, 1343 Rectangle2D dataArea, RectangleEdge edge) { 1344 1345 long shift = 0; 1346 if (this.timeline instanceof SegmentedTimeline) { 1347 shift = ((SegmentedTimeline) this.timeline).getStartTime(); 1348 } 1349 double zero = valueToJava2D(shift + 0.0, dataArea, edge); 1350 double tickLabelWidth = estimateMaximumTickLabelWidth(g2, 1351 getTickUnit()); 1352 1353 // start with the current tick unit... 1354 TickUnitSource tickUnits = getStandardTickUnits(); 1355 TickUnit unit1 = tickUnits.getCeilingTickUnit(getTickUnit()); 1356 double x1 = valueToJava2D(shift + unit1.getSize(), dataArea, edge); 1357 double unit1Width = Math.abs(x1 - zero); 1358 1359 // then extrapolate... 1360 double guess = (tickLabelWidth / unit1Width) * unit1.getSize(); 1361 DateTickUnit unit2 = (DateTickUnit) tickUnits.getCeilingTickUnit(guess); 1362 double x2 = valueToJava2D(shift + unit2.getSize(), dataArea, edge); 1363 double unit2Width = Math.abs(x2 - zero); 1364 tickLabelWidth = estimateMaximumTickLabelWidth(g2, unit2); 1365 if (tickLabelWidth > unit2Width) { 1366 unit2 = (DateTickUnit) tickUnits.getLargerTickUnit(unit2); 1367 } 1368 setTickUnit(unit2, false, false); 1369 } 1370 1371 /** 1372 * Selects an appropriate tick size for the axis. The strategy is to 1373 * display as many ticks as possible (selected from a collection of 1374 * 'standard' tick units) without the labels overlapping. 1375 * 1376 * @param g2 the graphics device. 1377 * @param dataArea the area in which the plot should be drawn. 1378 * @param edge the axis location. 1379 */ 1380 protected void selectVerticalAutoTickUnit(Graphics2D g2, 1381 Rectangle2D dataArea, RectangleEdge edge) { 1382 1383 // start with the current tick unit... 1384 TickUnitSource tickUnits = getStandardTickUnits(); 1385 double zero = valueToJava2D(0.0, dataArea, edge); 1386 1387 // start with a unit that is at least 1/10th of the axis length 1388 double estimate1 = getRange().getLength() / 10.0; 1389 DateTickUnit candidate1 1390 = (DateTickUnit) tickUnits.getCeilingTickUnit(estimate1); 1391 double labelHeight1 = estimateMaximumTickLabelHeight(g2, candidate1); 1392 double y1 = valueToJava2D(candidate1.getSize(), dataArea, edge); 1393 double candidate1UnitHeight = Math.abs(y1 - zero); 1394 1395 // now extrapolate based on label height and unit height... 1396 double estimate2 1397 = (labelHeight1 / candidate1UnitHeight) * candidate1.getSize(); 1398 DateTickUnit candidate2 1399 = (DateTickUnit) tickUnits.getCeilingTickUnit(estimate2); 1400 double labelHeight2 = estimateMaximumTickLabelHeight(g2, candidate2); 1401 double y2 = valueToJava2D(candidate2.getSize(), dataArea, edge); 1402 double unit2Height = Math.abs(y2 - zero); 1403 1404 // make final selection... 1405 DateTickUnit finalUnit; 1406 if (labelHeight2 < unit2Height) { 1407 finalUnit = candidate2; 1408 } 1409 else { 1410 finalUnit = (DateTickUnit) tickUnits.getLargerTickUnit(candidate2); 1411 } 1412 setTickUnit(finalUnit, false, false); 1413 1414 } 1415 1416 /** 1417 * Estimates the maximum width of the tick labels, assuming the specified 1418 * tick unit is used. 1419 * <P> 1420 * Rather than computing the string bounds of every tick on the axis, we 1421 * just look at two values: the lower bound and the upper bound for the 1422 * axis. These two values will usually be representative. 1423 * 1424 * @param g2 the graphics device. 1425 * @param unit the tick unit to use for calculation. 1426 * 1427 * @return The estimated maximum width of the tick labels. 1428 */ 1429 private double estimateMaximumTickLabelWidth(Graphics2D g2, 1430 DateTickUnit unit) { 1431 1432 RectangleInsets tickLabelInsets = getTickLabelInsets(); 1433 double result = tickLabelInsets.getLeft() + tickLabelInsets.getRight(); 1434 1435 Font tickLabelFont = getTickLabelFont(); 1436 FontRenderContext frc = g2.getFontRenderContext(); 1437 LineMetrics lm = tickLabelFont.getLineMetrics("ABCxyz", frc); 1438 if (isVerticalTickLabels()) { 1439 // all tick labels have the same width (equal to the height of 1440 // the font)... 1441 result += lm.getHeight(); 1442 } 1443 else { 1444 // look at lower and upper bounds... 1445 DateRange range = (DateRange) getRange(); 1446 Date lower = range.getLowerDate(); 1447 Date upper = range.getUpperDate(); 1448 String lowerStr, upperStr; 1449 DateFormat formatter = getDateFormatOverride(); 1450 if (formatter != null) { 1451 lowerStr = formatter.format(lower); 1452 upperStr = formatter.format(upper); 1453 } 1454 else { 1455 lowerStr = unit.dateToString(lower); 1456 upperStr = unit.dateToString(upper); 1457 } 1458 FontMetrics fm = g2.getFontMetrics(tickLabelFont); 1459 double w1 = fm.stringWidth(lowerStr); 1460 double w2 = fm.stringWidth(upperStr); 1461 result += Math.max(w1, w2); 1462 } 1463 1464 return result; 1465 1466 } 1467 1468 /** 1469 * Estimates the maximum width of the tick labels, assuming the specified 1470 * tick unit is used. 1471 * <P> 1472 * Rather than computing the string bounds of every tick on the axis, we 1473 * just look at two values: the lower bound and the upper bound for the 1474 * axis. These two values will usually be representative. 1475 * 1476 * @param g2 the graphics device. 1477 * @param unit the tick unit to use for calculation. 1478 * 1479 * @return The estimated maximum width of the tick labels. 1480 */ 1481 private double estimateMaximumTickLabelHeight(Graphics2D g2, 1482 DateTickUnit unit) { 1483 1484 RectangleInsets tickLabelInsets = getTickLabelInsets(); 1485 double result = tickLabelInsets.getTop() + tickLabelInsets.getBottom(); 1486 1487 Font tickLabelFont = getTickLabelFont(); 1488 FontRenderContext frc = g2.getFontRenderContext(); 1489 LineMetrics lm = tickLabelFont.getLineMetrics("ABCxyz", frc); 1490 if (!isVerticalTickLabels()) { 1491 // all tick labels have the same width (equal to the height of 1492 // the font)... 1493 result += lm.getHeight(); 1494 } 1495 else { 1496 // look at lower and upper bounds... 1497 DateRange range = (DateRange) getRange(); 1498 Date lower = range.getLowerDate(); 1499 Date upper = range.getUpperDate(); 1500 String lowerStr, upperStr; 1501 DateFormat formatter = getDateFormatOverride(); 1502 if (formatter != null) { 1503 lowerStr = formatter.format(lower); 1504 upperStr = formatter.format(upper); 1505 } 1506 else { 1507 lowerStr = unit.dateToString(lower); 1508 upperStr = unit.dateToString(upper); 1509 } 1510 FontMetrics fm = g2.getFontMetrics(tickLabelFont); 1511 double w1 = fm.stringWidth(lowerStr); 1512 double w2 = fm.stringWidth(upperStr); 1513 result += Math.max(w1, w2); 1514 } 1515 1516 return result; 1517 1518 } 1519 1520 /** 1521 * Calculates the positions of the tick labels for the axis, storing the 1522 * results in the tick label list (ready for drawing). 1523 * 1524 * @param g2 the graphics device. 1525 * @param state the axis state. 1526 * @param dataArea the area in which the plot should be drawn. 1527 * @param edge the location of the axis. 1528 * 1529 * @return A list of ticks. 1530 */ 1531 @Override 1532 public List refreshTicks(Graphics2D g2, AxisState state, 1533 Rectangle2D dataArea, RectangleEdge edge) { 1534 1535 List result = null; 1536 if (RectangleEdge.isTopOrBottom(edge)) { 1537 result = refreshTicksHorizontal(g2, dataArea, edge); 1538 } 1539 else if (RectangleEdge.isLeftOrRight(edge)) { 1540 result = refreshTicksVertical(g2, dataArea, edge); 1541 } 1542 return result; 1543 1544 } 1545 1546 /** 1547 * Corrects the given tick date for the position setting. 1548 * 1549 * @param time the tick date/time. 1550 * @param unit the tick unit. 1551 * @param position the tick position. 1552 * 1553 * @return The adjusted time. 1554 */ 1555 private Date correctTickDateForPosition(Date time, DateTickUnit unit, 1556 DateTickMarkPosition position) { 1557 Date result = time; 1558 switch (unit.getUnit()) { 1559 case DateTickUnit.MILLISECOND : 1560 case DateTickUnit.SECOND : 1561 case DateTickUnit.MINUTE : 1562 case DateTickUnit.HOUR : 1563 case DateTickUnit.DAY : 1564 break; 1565 case DateTickUnit.MONTH : 1566 result = calculateDateForPosition(new Month(time, 1567 this.timeZone, this.locale), position); 1568 break; 1569 case DateTickUnit.YEAR : 1570 result = calculateDateForPosition(new Year(time, 1571 this.timeZone, this.locale), position); 1572 break; 1573 1574 default: break; 1575 } 1576 return result; 1577 } 1578 1579 /** 1580 * Recalculates the ticks for the date axis. 1581 * 1582 * @param g2 the graphics device. 1583 * @param dataArea the area in which the data is to be drawn. 1584 * @param edge the location of the axis. 1585 * 1586 * @return A list of ticks. 1587 */ 1588 protected List refreshTicksHorizontal(Graphics2D g2, 1589 Rectangle2D dataArea, RectangleEdge edge) { 1590 1591 List result = new java.util.ArrayList(); 1592 1593 Font tickLabelFont = getTickLabelFont(); 1594 g2.setFont(tickLabelFont); 1595 1596 if (isAutoTickUnitSelection()) { 1597 selectAutoTickUnit(g2, dataArea, edge); 1598 } 1599 1600 DateTickUnit unit = getTickUnit(); 1601 Date tickDate = calculateLowestVisibleTickValue(unit); 1602 Date upperDate = getMaximumDate(); 1603 1604 boolean hasRolled = false; 1605 while (tickDate.before(upperDate)) { 1606 // could add a flag to make the following correction optional... 1607 if (!hasRolled) { 1608 tickDate = correctTickDateForPosition(tickDate, unit, 1609 this.tickMarkPosition); 1610 } 1611 1612 long lowestTickTime = tickDate.getTime(); 1613 long distance = unit.addToDate(tickDate, this.timeZone).getTime() 1614 - lowestTickTime; 1615 int minorTickSpaces = getMinorTickCount(); 1616 if (minorTickSpaces <= 0) { 1617 minorTickSpaces = unit.getMinorTickCount(); 1618 } 1619 for (int minorTick = 1; minorTick < minorTickSpaces; minorTick++) { 1620 long minorTickTime = lowestTickTime - distance 1621 * minorTick / minorTickSpaces; 1622 if (minorTickTime > 0 && getRange().contains(minorTickTime) 1623 && (!isHiddenValue(minorTickTime))) { 1624 result.add(new DateTick(TickType.MINOR, 1625 new Date(minorTickTime), "", TextAnchor.TOP_CENTER, 1626 TextAnchor.CENTER, 0.0)); 1627 } 1628 } 1629 1630 if (!isHiddenValue(tickDate.getTime())) { 1631 // work out the value, label and position 1632 String tickLabel; 1633 DateFormat formatter = getDateFormatOverride(); 1634 if (formatter != null) { 1635 tickLabel = formatter.format(tickDate); 1636 } 1637 else { 1638 tickLabel = this.tickUnit.dateToString(tickDate); 1639 } 1640 TextAnchor anchor, rotationAnchor; 1641 double angle = 0.0; 1642 if (isVerticalTickLabels()) { 1643 anchor = TextAnchor.CENTER_RIGHT; 1644 rotationAnchor = TextAnchor.CENTER_RIGHT; 1645 if (edge == RectangleEdge.TOP) { 1646 angle = Math.PI / 2.0; 1647 } 1648 else { 1649 angle = -Math.PI / 2.0; 1650 } 1651 } 1652 else { 1653 if (edge == RectangleEdge.TOP) { 1654 anchor = TextAnchor.BOTTOM_CENTER; 1655 rotationAnchor = TextAnchor.BOTTOM_CENTER; 1656 } 1657 else { 1658 anchor = TextAnchor.TOP_CENTER; 1659 rotationAnchor = TextAnchor.TOP_CENTER; 1660 } 1661 } 1662 1663 Tick tick = new DateTick(tickDate, tickLabel, anchor, 1664 rotationAnchor, angle); 1665 result.add(tick); 1666 hasRolled = false; 1667 1668 long currentTickTime = tickDate.getTime(); 1669 tickDate = unit.addToDate(tickDate, this.timeZone); 1670 long nextTickTime = tickDate.getTime(); 1671 for (int minorTick = 1; minorTick < minorTickSpaces; 1672 minorTick++) { 1673 long minorTickTime = currentTickTime 1674 + (nextTickTime - currentTickTime) 1675 * minorTick / minorTickSpaces; 1676 if (getRange().contains(minorTickTime) 1677 && (!isHiddenValue(minorTickTime))) { 1678 result.add(new DateTick(TickType.MINOR, 1679 new Date(minorTickTime), "", 1680 TextAnchor.TOP_CENTER, TextAnchor.CENTER, 1681 0.0)); 1682 } 1683 } 1684 1685 } 1686 else { 1687 tickDate = unit.rollDate(tickDate, this.timeZone); 1688 hasRolled = true; 1689 continue; 1690 } 1691 1692 } 1693 return result; 1694 1695 } 1696 1697 /** 1698 * Recalculates the ticks for the date axis. 1699 * 1700 * @param g2 the graphics device. 1701 * @param dataArea the area in which the plot should be drawn. 1702 * @param edge the location of the axis. 1703 * 1704 * @return A list of ticks. 1705 */ 1706 protected List refreshTicksVertical(Graphics2D g2, 1707 Rectangle2D dataArea, RectangleEdge edge) { 1708 1709 List result = new java.util.ArrayList(); 1710 1711 Font tickLabelFont = getTickLabelFont(); 1712 g2.setFont(tickLabelFont); 1713 1714 if (isAutoTickUnitSelection()) { 1715 selectAutoTickUnit(g2, dataArea, edge); 1716 } 1717 DateTickUnit unit = getTickUnit(); 1718 Date tickDate = calculateLowestVisibleTickValue(unit); 1719 Date upperDate = getMaximumDate(); 1720 1721 boolean hasRolled = false; 1722 while (tickDate.before(upperDate)) { 1723 1724 // could add a flag to make the following correction optional... 1725 if (!hasRolled) { 1726 tickDate = correctTickDateForPosition(tickDate, unit, 1727 this.tickMarkPosition); 1728 } 1729 1730 long lowestTickTime = tickDate.getTime(); 1731 long distance = unit.addToDate(tickDate, this.timeZone).getTime() 1732 - lowestTickTime; 1733 int minorTickSpaces = getMinorTickCount(); 1734 if (minorTickSpaces <= 0) { 1735 minorTickSpaces = unit.getMinorTickCount(); 1736 } 1737 for (int minorTick = 1; minorTick < minorTickSpaces; minorTick++) { 1738 long minorTickTime = lowestTickTime - distance 1739 * minorTick / minorTickSpaces; 1740 if (minorTickTime > 0 && getRange().contains(minorTickTime) 1741 && (!isHiddenValue(minorTickTime))) { 1742 result.add(new DateTick(TickType.MINOR, 1743 new Date(minorTickTime), "", TextAnchor.TOP_CENTER, 1744 TextAnchor.CENTER, 0.0)); 1745 } 1746 } 1747 if (!isHiddenValue(tickDate.getTime())) { 1748 // work out the value, label and position 1749 String tickLabel; 1750 DateFormat formatter = getDateFormatOverride(); 1751 if (formatter != null) { 1752 tickLabel = formatter.format(tickDate); 1753 } 1754 else { 1755 tickLabel = this.tickUnit.dateToString(tickDate); 1756 } 1757 TextAnchor anchor, rotationAnchor; 1758 double angle = 0.0; 1759 if (isVerticalTickLabels()) { 1760 anchor = TextAnchor.BOTTOM_CENTER; 1761 rotationAnchor = TextAnchor.BOTTOM_CENTER; 1762 if (edge == RectangleEdge.LEFT) { 1763 angle = -Math.PI / 2.0; 1764 } 1765 else { 1766 angle = Math.PI / 2.0; 1767 } 1768 } 1769 else { 1770 if (edge == RectangleEdge.LEFT) { 1771 anchor = TextAnchor.CENTER_RIGHT; 1772 rotationAnchor = TextAnchor.CENTER_RIGHT; 1773 } 1774 else { 1775 anchor = TextAnchor.CENTER_LEFT; 1776 rotationAnchor = TextAnchor.CENTER_LEFT; 1777 } 1778 } 1779 1780 Tick tick = new DateTick(tickDate, tickLabel, anchor, 1781 rotationAnchor, angle); 1782 result.add(tick); 1783 hasRolled = false; 1784 1785 long currentTickTime = tickDate.getTime(); 1786 tickDate = unit.addToDate(tickDate, this.timeZone); 1787 long nextTickTime = tickDate.getTime(); 1788 for (int minorTick = 1; minorTick < minorTickSpaces; 1789 minorTick++) { 1790 long minorTickTime = currentTickTime 1791 + (nextTickTime - currentTickTime) 1792 * minorTick / minorTickSpaces; 1793 if (getRange().contains(minorTickTime) 1794 && (!isHiddenValue(minorTickTime))) { 1795 result.add(new DateTick(TickType.MINOR, 1796 new Date(minorTickTime), "", 1797 TextAnchor.TOP_CENTER, TextAnchor.CENTER, 1798 0.0)); 1799 } 1800 } 1801 } 1802 else { 1803 tickDate = unit.rollDate(tickDate, this.timeZone); 1804 hasRolled = true; 1805 } 1806 } 1807 return result; 1808 } 1809 1810 /** 1811 * Draws the axis on a Java 2D graphics device (such as the screen or a 1812 * printer). 1813 * 1814 * @param g2 the graphics device (<code>null</code> not permitted). 1815 * @param cursor the cursor location. 1816 * @param plotArea the area within which the axes and data should be 1817 * drawn (<code>null</code> not permitted). 1818 * @param dataArea the area within which the data should be drawn 1819 * (<code>null</code> not permitted). 1820 * @param edge the location of the axis (<code>null</code> not permitted). 1821 * @param plotState collects information about the plot 1822 * (<code>null</code> permitted). 1823 * 1824 * @return The axis state (never <code>null</code>). 1825 */ 1826 @Override 1827 public AxisState draw(Graphics2D g2, double cursor, Rectangle2D plotArea, 1828 Rectangle2D dataArea, RectangleEdge edge, 1829 PlotRenderingInfo plotState) { 1830 1831 // if the axis is not visible, don't draw it... 1832 if (!isVisible()) { 1833 AxisState state = new AxisState(cursor); 1834 // even though the axis is not visible, we need to refresh ticks in 1835 // case the grid is being drawn... 1836 List ticks = refreshTicks(g2, state, dataArea, edge); 1837 state.setTicks(ticks); 1838 return state; 1839 } 1840 1841 // draw the tick marks and labels... 1842 AxisState state = drawTickMarksAndLabels(g2, cursor, plotArea, 1843 dataArea, edge); 1844 1845 // draw the axis label (note that 'state' is passed in *and* 1846 // returned)... 1847 if (getAttributedLabel() != null) { 1848 state = drawAttributedLabel(getAttributedLabel(), g2, plotArea, 1849 dataArea, edge, state); 1850 1851 } else { 1852 state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state); 1853 } 1854 createAndAddEntity(cursor, state, dataArea, edge, plotState); 1855 return state; 1856 1857 } 1858 1859 /** 1860 * Zooms in on the current range (zoom-in stops once the axis length 1861 * reaches the equivalent of one millisecond). 1862 * 1863 * @param lowerPercent the new lower bound. 1864 * @param upperPercent the new upper bound. 1865 */ 1866 @Override 1867 public void zoomRange(double lowerPercent, double upperPercent) { 1868 double start = this.timeline.toTimelineValue( 1869 (long) getRange().getLowerBound()); 1870 double end = this.timeline.toTimelineValue( 1871 (long) getRange().getUpperBound()); 1872 double length = end - start; 1873 Range adjusted; 1874 long adjStart, adjEnd; 1875 if (isInverted()) { 1876 adjStart = (long) (start + (length * (1 - upperPercent))); 1877 adjEnd = (long) (start + (length * (1 - lowerPercent))); 1878 } 1879 else { 1880 adjStart = (long) (start + length * lowerPercent); 1881 adjEnd = (long) (start + length * upperPercent); 1882 } 1883 // when zooming to sub-millisecond ranges, it can be the case that 1884 // adjEnd == adjStart...and we can't have an axis with zero length 1885 // so we apply this instead: 1886 if (adjEnd <= adjStart) { 1887 adjEnd = adjStart + 1L; 1888 } 1889 adjusted = new DateRange(this.timeline.toMillisecond(adjStart), 1890 this.timeline.toMillisecond(adjEnd)); 1891 setRange(adjusted); 1892 } 1893 1894 /** 1895 * Tests this axis for equality with an arbitrary object. 1896 * 1897 * @param obj the object (<code>null</code> permitted). 1898 * 1899 * @return A boolean. 1900 */ 1901 @Override 1902 public boolean equals(Object obj) { 1903 if (obj == this) { 1904 return true; 1905 } 1906 if (!(obj instanceof DateAxis)) { 1907 return false; 1908 } 1909 DateAxis that = (DateAxis) obj; 1910 if (!ObjectUtilities.equal(this.tickUnit, that.tickUnit)) { 1911 return false; 1912 } 1913 if (!ObjectUtilities.equal(this.dateFormatOverride, 1914 that.dateFormatOverride)) { 1915 return false; 1916 } 1917 if (!ObjectUtilities.equal(this.tickMarkPosition, 1918 that.tickMarkPosition)) { 1919 return false; 1920 } 1921 if (!ObjectUtilities.equal(this.timeline, that.timeline)) { 1922 return false; 1923 } 1924 return super.equals(obj); 1925 } 1926 1927 /** 1928 * Returns a hash code for this object. 1929 * 1930 * @return A hash code. 1931 */ 1932 @Override 1933 public int hashCode() { 1934 return super.hashCode(); 1935 } 1936 1937 /** 1938 * Returns a clone of the object. 1939 * 1940 * @return A clone. 1941 * 1942 * @throws CloneNotSupportedException if some component of the axis does 1943 * not support cloning. 1944 */ 1945 @Override 1946 public Object clone() throws CloneNotSupportedException { 1947 DateAxis clone = (DateAxis) super.clone(); 1948 // 'dateTickUnit' is immutable : no need to clone 1949 if (this.dateFormatOverride != null) { 1950 clone.dateFormatOverride 1951 = (DateFormat) this.dateFormatOverride.clone(); 1952 } 1953 // 'tickMarkPosition' is immutable : no need to clone 1954 return clone; 1955 } 1956 1957 /** 1958 * Returns a collection of standard date tick units. This collection will 1959 * be used by default, but you are free to create your own collection if 1960 * you want to (see the 1961 * {@link ValueAxis#setStandardTickUnits(TickUnitSource)} method inherited 1962 * from the {@link ValueAxis} class). 1963 * 1964 * @param zone the time zone (<code>null</code> not permitted). 1965 * 1966 * @return A collection of standard date tick units. 1967 * 1968 * @deprecated Since 1.0.11, use {@link #createStandardDateTickUnits( 1969 * TimeZone, Locale)} to explicitly set the locale as well as the 1970 * time zone. 1971 */ 1972 public static TickUnitSource createStandardDateTickUnits(TimeZone zone) { 1973 return createStandardDateTickUnits(zone, Locale.getDefault()); 1974 } 1975 1976}