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