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