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