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