001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-present, 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-present, 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.Calendar; 057import java.util.Date; 058import java.util.List; 059import java.util.Locale; 060import java.util.Objects; 061import java.util.TimeZone; 062 063import org.jfree.chart.event.AxisChangeEvent; 064import org.jfree.chart.plot.Plot; 065import org.jfree.chart.plot.PlotRenderingInfo; 066import org.jfree.chart.plot.ValueAxisPlot; 067import org.jfree.chart.ui.RectangleEdge; 068import org.jfree.chart.ui.RectangleInsets; 069import org.jfree.chart.ui.TextAnchor; 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 private Locale locale; 246 247 /** Our underlying timeline. */ 248 private Timeline timeline; 249 250 /** 251 * Creates a date axis with no label. 252 */ 253 public DateAxis() { 254 this(null); 255 } 256 257 /** 258 * Creates a date axis with the specified label. 259 * 260 * @param label the axis label ({@code null} permitted). 261 */ 262 public DateAxis(String label) { 263 this(label, TimeZone.getDefault(), Locale.getDefault()); 264 } 265 266 /** 267 * Creates a date axis. 268 * 269 * @param label the axis label ({@code null} permitted). 270 * @param zone the time zone. 271 * @param locale the locale ({@code null} not permitted). 272 */ 273 public DateAxis(String label, TimeZone zone, Locale locale) { 274 super(label, DateAxis.createStandardDateTickUnits(zone, locale)); 275 this.tickUnit = new DateTickUnit(DateTickUnitType.DAY, 1, 276 new SimpleDateFormat()); 277 setAutoRangeMinimumSize( 278 DEFAULT_AUTO_RANGE_MINIMUM_SIZE_IN_MILLISECONDS); 279 setRange(DEFAULT_DATE_RANGE, false, false); 280 this.dateFormatOverride = null; 281 this.timeZone = zone; 282 this.locale = locale; 283 this.timeline = DEFAULT_TIMELINE; 284 } 285 286 /** 287 * Returns the time zone for the axis. 288 * 289 * @return The time zone (never {@code null}). 290 * 291 * @see #setTimeZone(TimeZone) 292 */ 293 public TimeZone getTimeZone() { 294 return this.timeZone; 295 } 296 297 /** 298 * Sets the time zone for the axis and sends an {@link AxisChangeEvent} to 299 * all registered listeners. 300 * 301 * @param zone the time zone ({@code null} not permitted). 302 * 303 * @see #getTimeZone() 304 */ 305 public void setTimeZone(TimeZone zone) { 306 Args.nullNotPermitted(zone, "zone"); 307 this.timeZone = zone; 308 setStandardTickUnits(createStandardDateTickUnits(zone, this.locale)); 309 fireChangeEvent(); 310 } 311 312 /** 313 * Returns the locale for this axis. 314 * 315 * @return The locale (never {@code null}). 316 */ 317 public Locale getLocale() { 318 return this.locale; 319 } 320 321 /** 322 * Sets the locale for the axis and sends a change event to all registered 323 * listeners. 324 * 325 * @param locale the new locale ({@code null} not permitted). 326 */ 327 public void setLocale(Locale locale) { 328 Args.nullNotPermitted(locale, "locale"); 329 this.locale = locale; 330 setStandardTickUnits(createStandardDateTickUnits(this.timeZone, 331 this.locale)); 332 fireChangeEvent(); 333 } 334 335 /** 336 * Returns the underlying timeline used by this axis. 337 * 338 * @return The timeline. 339 */ 340 public Timeline getTimeline() { 341 return this.timeline; 342 } 343 344 /** 345 * Sets the underlying timeline to use for this axis. If the timeline is 346 * changed, an {@link AxisChangeEvent} is sent to all registered listeners. 347 * 348 * @param timeline the timeline. 349 */ 350 public void setTimeline(Timeline timeline) { 351 if (this.timeline != timeline) { 352 this.timeline = timeline; 353 fireChangeEvent(); 354 } 355 } 356 357 /** 358 * Returns the tick unit for the axis. 359 * <p> 360 * Note: if the {@code autoTickUnitSelection} flag is 361 * {@code true} the tick unit may be changed while the axis is being 362 * drawn, so in that case the return value from this method may be 363 * irrelevant if the method is called before the axis has been drawn. 364 * 365 * @return The tick unit (possibly {@code null}). 366 * 367 * @see #setTickUnit(DateTickUnit) 368 * @see ValueAxis#isAutoTickUnitSelection() 369 */ 370 public DateTickUnit getTickUnit() { 371 return this.tickUnit; 372 } 373 374 /** 375 * Sets the tick unit for the axis. The auto-tick-unit-selection flag is 376 * set to {@code false}, and registered listeners are notified that 377 * the axis has been changed. 378 * 379 * @param unit the tick unit. 380 * 381 * @see #getTickUnit() 382 * @see #setTickUnit(DateTickUnit, boolean, boolean) 383 */ 384 public void setTickUnit(DateTickUnit unit) { 385 setTickUnit(unit, true, true); 386 } 387 388 /** 389 * Sets the tick unit attribute and, if requested, sends an 390 * {@link AxisChangeEvent} to all registered listeners. 391 * 392 * @param unit the new tick unit. 393 * @param notify notify registered listeners? 394 * @param turnOffAutoSelection turn off auto selection? 395 * 396 * @see #getTickUnit() 397 */ 398 public void setTickUnit(DateTickUnit unit, boolean notify, 399 boolean turnOffAutoSelection) { 400 401 this.tickUnit = unit; 402 if (turnOffAutoSelection) { 403 setAutoTickUnitSelection(false, false); 404 } 405 if (notify) { 406 fireChangeEvent(); 407 } 408 409 } 410 411 /** 412 * Returns the date format override. If this is non-null, then it will be 413 * used to format the dates on the axis. 414 * 415 * @return The formatter (possibly {@code null}). 416 */ 417 public DateFormat getDateFormatOverride() { 418 return this.dateFormatOverride; 419 } 420 421 /** 422 * Sets the date format override and sends an {@link AxisChangeEvent} to 423 * all registered listeners. If this is non-null, then it will be 424 * used to format the dates on the axis. 425 * 426 * @param formatter the date formatter ({@code null} permitted). 427 */ 428 public void setDateFormatOverride(DateFormat formatter) { 429 this.dateFormatOverride = formatter; 430 fireChangeEvent(); 431 } 432 433 /** 434 * Sets the upper and lower bounds for the axis and sends an 435 * {@link AxisChangeEvent} to all registered listeners. As a side-effect, 436 * the auto-range flag is set to false. 437 * 438 * @param range the new range ({@code null} not permitted). 439 */ 440 @Override 441 public void setRange(Range range) { 442 setRange(range, true, true); 443 } 444 445 /** 446 * Sets the range for the axis, if requested, sends an 447 * {@link AxisChangeEvent} to all registered listeners. As a side-effect, 448 * the auto-range flag is set to {@code false} (optional). 449 * 450 * @param range the range ({@code null} not permitted). 451 * @param turnOffAutoRange a flag that controls whether or not the auto 452 * range is turned off. 453 * @param notify a flag that controls whether or not listeners are 454 * notified. 455 */ 456 @Override 457 public void setRange(Range range, boolean turnOffAutoRange, 458 boolean notify) { 459 Args.nullNotPermitted(range, "range"); 460 // usually the range will be a DateRange, but if it isn't do a 461 // conversion... 462 if (!(range instanceof DateRange)) { 463 range = new DateRange(range); 464 } 465 super.setRange(range, turnOffAutoRange, notify); 466 } 467 468 /** 469 * Sets the axis range and sends an {@link AxisChangeEvent} to all 470 * registered listeners. 471 * 472 * @param lower the lower bound for the axis. 473 * @param upper the upper bound for the axis. 474 */ 475 public void setRange(Date lower, Date upper) { 476 if (lower.getTime() >= upper.getTime()) { 477 throw new IllegalArgumentException("Requires 'lower' < 'upper'."); 478 } 479 setRange(new DateRange(lower, upper)); 480 } 481 482 /** 483 * Sets the axis range and sends an {@link AxisChangeEvent} to all 484 * registered listeners. 485 * 486 * @param lower the lower bound for the axis. 487 * @param upper the upper bound for the axis. 488 */ 489 @Override 490 public void setRange(double lower, double upper) { 491 if (lower >= upper) { 492 throw new IllegalArgumentException("Requires 'lower' < 'upper'."); 493 } 494 setRange(new DateRange(lower, upper)); 495 } 496 497 /** 498 * Returns the earliest date visible on the axis. 499 * 500 * @return The date. 501 * 502 * @see #setMinimumDate(Date) 503 * @see #getMaximumDate() 504 */ 505 public Date getMinimumDate() { 506 Date result; 507 Range range = getRange(); 508 if (range instanceof DateRange) { 509 DateRange r = (DateRange) range; 510 result = r.getLowerDate(); 511 } 512 else { 513 result = new Date((long) range.getLowerBound()); 514 } 515 return result; 516 } 517 518 /** 519 * Sets the minimum date visible on the axis and sends an 520 * {@link AxisChangeEvent} to all registered listeners. If 521 * {@code date} is on or after the current maximum date for 522 * the axis, the maximum date will be shifted to preserve the current 523 * length of the axis. 524 * 525 * @param date the date ({@code null} not permitted). 526 * 527 * @see #getMinimumDate() 528 * @see #setMaximumDate(Date) 529 */ 530 public void setMinimumDate(Date date) { 531 Args.nullNotPermitted(date, "date"); 532 // check the new minimum date relative to the current maximum date 533 Date maxDate = getMaximumDate(); 534 long maxMillis = maxDate.getTime(); 535 long newMinMillis = date.getTime(); 536 if (maxMillis <= newMinMillis) { 537 Date oldMin = getMinimumDate(); 538 long length = maxMillis - oldMin.getTime(); 539 maxDate = new Date(newMinMillis + length); 540 } 541 setRange(new DateRange(date, maxDate), true, false); 542 fireChangeEvent(); 543 } 544 545 /** 546 * Returns the latest date visible on the axis. 547 * 548 * @return The date. 549 * 550 * @see #setMaximumDate(Date) 551 * @see #getMinimumDate() 552 */ 553 public Date getMaximumDate() { 554 Date result; 555 Range range = getRange(); 556 if (range instanceof DateRange) { 557 DateRange r = (DateRange) range; 558 result = r.getUpperDate(); 559 } 560 else { 561 result = new Date((long) range.getUpperBound()); 562 } 563 return result; 564 } 565 566 /** 567 * Sets the maximum date visible on the axis and sends an 568 * {@link AxisChangeEvent} to all registered listeners. If 569 * {@code maximumDate} is on or before the current minimum date for 570 * the axis, the minimum date will be shifted to preserve the current 571 * length of the axis. 572 * 573 * @param maximumDate the date ({@code null} not permitted). 574 * 575 * @see #getMinimumDate() 576 * @see #setMinimumDate(Date) 577 */ 578 public void setMaximumDate(Date maximumDate) { 579 Args.nullNotPermitted(maximumDate, "maximumDate"); 580 // check the new maximum date relative to the current minimum date 581 Date minDate = getMinimumDate(); 582 long minMillis = minDate.getTime(); 583 long newMaxMillis = maximumDate.getTime(); 584 if (minMillis >= newMaxMillis) { 585 Date oldMax = getMaximumDate(); 586 long length = oldMax.getTime() - minMillis; 587 minDate = new Date(newMaxMillis - length); 588 } 589 setRange(new DateRange(minDate, maximumDate), true, false); 590 fireChangeEvent(); 591 } 592 593 /** 594 * Returns the tick mark position (start, middle or end of the time period). 595 * 596 * @return The position (never {@code null}). 597 */ 598 public DateTickMarkPosition getTickMarkPosition() { 599 return this.tickMarkPosition; 600 } 601 602 /** 603 * Sets the tick mark position (start, middle or end of the time period) 604 * and sends an {@link AxisChangeEvent} to all registered listeners. 605 * 606 * @param position the position ({@code null} not permitted). 607 */ 608 public void setTickMarkPosition(DateTickMarkPosition position) { 609 Args.nullNotPermitted(position, "position"); 610 this.tickMarkPosition = position; 611 fireChangeEvent(); 612 } 613 614 /** 615 * Configures the axis to work with the specified plot. If the axis has 616 * auto-scaling, then sets the maximum and minimum values. 617 */ 618 @Override 619 public void configure() { 620 if (isAutoRange()) { 621 autoAdjustRange(); 622 } 623 } 624 625 /** 626 * Returns {@code true} if the axis hides this value, and 627 * {@code false} otherwise. 628 * 629 * @param millis the data value. 630 * 631 * @return A value. 632 */ 633 public boolean isHiddenValue(long millis) { 634 return (!this.timeline.containsDomainValue(new Date(millis))); 635 } 636 637 /** 638 * Translates the data value to the display coordinates (Java 2D User Space) 639 * of the chart. 640 * 641 * @param value the date to be plotted. 642 * @param area the rectangle (in Java2D space) where the data is to be 643 * plotted. 644 * @param edge the axis location. 645 * 646 * @return The coordinate corresponding to the supplied data value. 647 */ 648 @Override 649 public double valueToJava2D(double value, Rectangle2D area, 650 RectangleEdge edge) { 651 652 value = this.timeline.toTimelineValue((long) value); 653 654 DateRange range = (DateRange) getRange(); 655 double axisMin = this.timeline.toTimelineValue(range.getLowerMillis()); 656 double axisMax = this.timeline.toTimelineValue(range.getUpperMillis()); 657 double result = 0.0; 658 if (RectangleEdge.isTopOrBottom(edge)) { 659 double minX = area.getX(); 660 double maxX = area.getMaxX(); 661 if (isInverted()) { 662 result = maxX + ((value - axisMin) / (axisMax - axisMin)) 663 * (minX - maxX); 664 } 665 else { 666 result = minX + ((value - axisMin) / (axisMax - axisMin)) 667 * (maxX - minX); 668 } 669 } 670 else if (RectangleEdge.isLeftOrRight(edge)) { 671 double minY = area.getMinY(); 672 double maxY = area.getMaxY(); 673 if (isInverted()) { 674 result = minY + (((value - axisMin) / (axisMax - axisMin)) 675 * (maxY - minY)); 676 } 677 else { 678 result = maxY - (((value - axisMin) / (axisMax - axisMin)) 679 * (maxY - minY)); 680 } 681 } 682 return result; 683 } 684 685 /** 686 * Translates a date to Java2D coordinates, based on the range displayed by 687 * this axis for the specified data area. 688 * 689 * @param date the date. 690 * @param area the rectangle (in Java2D space) where the data is to be 691 * plotted. 692 * @param edge the axis location. 693 * 694 * @return The coordinate corresponding to the supplied date. 695 */ 696 public double dateToJava2D(Date date, Rectangle2D area, 697 RectangleEdge edge) { 698 double value = date.getTime(); 699 return valueToJava2D(value, area, edge); 700 } 701 702 /** 703 * Translates a Java2D coordinate into the corresponding data value. To 704 * perform this translation, you need to know the area used for plotting 705 * data, and which edge the axis is located on. 706 * 707 * @param java2DValue the coordinate in Java2D space. 708 * @param area the rectangle (in Java2D space) where the data is to be 709 * plotted. 710 * @param edge the axis location. 711 * 712 * @return A data value. 713 */ 714 @Override 715 public double java2DToValue(double java2DValue, Rectangle2D area, 716 RectangleEdge edge) { 717 718 DateRange range = (DateRange) getRange(); 719 double axisMin = this.timeline.toTimelineValue(range.getLowerMillis()); 720 double axisMax = this.timeline.toTimelineValue(range.getUpperMillis()); 721 722 double min = 0.0; 723 double max = 0.0; 724 if (RectangleEdge.isTopOrBottom(edge)) { 725 min = area.getX(); 726 max = area.getMaxX(); 727 } 728 else if (RectangleEdge.isLeftOrRight(edge)) { 729 min = area.getMaxY(); 730 max = area.getY(); 731 } 732 733 double result; 734 if (isInverted()) { 735 result = axisMax - ((java2DValue - min) / (max - min) 736 * (axisMax - axisMin)); 737 } 738 else { 739 result = axisMin + ((java2DValue - min) / (max - min) 740 * (axisMax - axisMin)); 741 } 742 743 return this.timeline.toMillisecond((long) result); 744 } 745 746 /** 747 * Calculates the value of the lowest visible tick on the axis. 748 * 749 * @param unit date unit to use. 750 * 751 * @return The value of the lowest visible tick on the axis. 752 */ 753 public Date calculateLowestVisibleTickValue(DateTickUnit unit) { 754 return nextStandardDate(getMinimumDate(), unit); 755 } 756 757 /** 758 * Calculates the value of the highest visible tick on the axis. 759 * 760 * @param unit date unit to use. 761 * 762 * @return The value of the highest visible tick on the axis. 763 */ 764 public Date calculateHighestVisibleTickValue(DateTickUnit unit) { 765 return previousStandardDate(getMaximumDate(), unit); 766 } 767 768 /** 769 * Returns the previous "standard" date, for a given date and tick unit. 770 * 771 * @param date the reference date. 772 * @param unit the tick unit. 773 * 774 * @return The previous "standard" date. 775 */ 776 protected Date previousStandardDate(Date date, DateTickUnit unit) { 777 778 int milliseconds; 779 int seconds; 780 int minutes; 781 int hours; 782 int days; 783 int months; 784 int years; 785 786 Calendar calendar = Calendar.getInstance(this.timeZone, this.locale); 787 calendar.setTime(date); 788 int count = unit.getMultiple(); 789 int current = calendar.get(unit.getCalendarField()); 790 int value = count * (current / count); 791 792 if (DateTickUnitType.MILLISECOND.equals(unit.getUnitType())) { 793 years = calendar.get(Calendar.YEAR); 794 months = calendar.get(Calendar.MONTH); 795 days = calendar.get(Calendar.DATE); 796 hours = calendar.get(Calendar.HOUR_OF_DAY); 797 minutes = calendar.get(Calendar.MINUTE); 798 seconds = calendar.get(Calendar.SECOND); 799 calendar.set(years, months, days, hours, minutes, seconds); 800 calendar.set(Calendar.MILLISECOND, value); 801 Date mm = calendar.getTime(); 802 if (mm.getTime() >= date.getTime()) { 803 calendar.set(Calendar.MILLISECOND, value - count); 804 mm = calendar.getTime(); 805 } 806 return mm; 807 } 808 else if (DateTickUnitType.SECOND.equals(unit.getUnitType())) { 809 years = calendar.get(Calendar.YEAR); 810 months = calendar.get(Calendar.MONTH); 811 days = calendar.get(Calendar.DATE); 812 hours = calendar.get(Calendar.HOUR_OF_DAY); 813 minutes = calendar.get(Calendar.MINUTE); 814 if (this.tickMarkPosition == DateTickMarkPosition.START) { 815 milliseconds = 0; 816 } 817 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) { 818 milliseconds = 500; 819 } 820 else { 821 milliseconds = 999; 822 } 823 calendar.set(Calendar.MILLISECOND, milliseconds); 824 calendar.set(years, months, days, hours, minutes, value); 825 Date dd = calendar.getTime(); 826 if (dd.getTime() >= date.getTime()) { 827 calendar.set(Calendar.SECOND, value - count); 828 dd = calendar.getTime(); 829 } 830 return dd; 831 } 832 else if (DateTickUnitType.MINUTE.equals(unit.getUnitType())) { 833 years = calendar.get(Calendar.YEAR); 834 months = calendar.get(Calendar.MONTH); 835 days = calendar.get(Calendar.DATE); 836 hours = calendar.get(Calendar.HOUR_OF_DAY); 837 if (this.tickMarkPosition == DateTickMarkPosition.START) { 838 seconds = 0; 839 } 840 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) { 841 seconds = 30; 842 } 843 else { 844 seconds = 59; 845 } 846 calendar.clear(Calendar.MILLISECOND); 847 calendar.set(years, months, days, hours, value, seconds); 848 Date d0 = calendar.getTime(); 849 if (d0.getTime() >= date.getTime()) { 850 calendar.set(Calendar.MINUTE, value - count); 851 d0 = calendar.getTime(); 852 } 853 return d0; 854 } 855 else if (DateTickUnitType.HOUR.equals(unit.getUnitType())) { 856 years = calendar.get(Calendar.YEAR); 857 months = calendar.get(Calendar.MONTH); 858 days = calendar.get(Calendar.DATE); 859 if (this.tickMarkPosition == DateTickMarkPosition.START) { 860 minutes = 0; 861 seconds = 0; 862 } 863 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) { 864 minutes = 30; 865 seconds = 0; 866 } 867 else { 868 minutes = 59; 869 seconds = 59; 870 } 871 calendar.clear(Calendar.MILLISECOND); 872 calendar.set(years, months, days, value, minutes, seconds); 873 Date d1 = calendar.getTime(); 874 if (d1.getTime() >= date.getTime()) { 875 calendar.set(Calendar.HOUR_OF_DAY, value - count); 876 d1 = calendar.getTime(); 877 } 878 return d1; 879 } 880 else if (DateTickUnitType.DAY.equals(unit.getUnitType())) { 881 years = calendar.get(Calendar.YEAR); 882 months = calendar.get(Calendar.MONTH); 883 if (this.tickMarkPosition == DateTickMarkPosition.START) { 884 hours = 0; 885 } 886 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) { 887 hours = 12; 888 } 889 else { 890 hours = 23; 891 } 892 calendar.clear(Calendar.MILLISECOND); 893 calendar.set(years, months, value, hours, 0, 0); 894 // long result = calendar.getTimeInMillis(); 895 // won't work with JDK 1.3 896 Date d2 = calendar.getTime(); 897 if (d2.getTime() >= date.getTime()) { 898 calendar.set(Calendar.DATE, value - count); 899 d2 = calendar.getTime(); 900 } 901 return d2; 902 } 903 else if (DateTickUnitType.MONTH.equals(unit.getUnitType())) { 904 value = count * ((current + 1) / count) - 1; 905 years = calendar.get(Calendar.YEAR); 906 calendar.clear(Calendar.MILLISECOND); 907 calendar.set(years, value, 1, 0, 0, 0); 908 Month month = new Month(calendar.getTime(), this.timeZone, 909 this.locale); 910 Date standardDate = calculateDateForPosition( 911 month, this.tickMarkPosition); 912 long millis = standardDate.getTime(); 913 if (millis >= date.getTime()) { 914 for (int i = 0; i < count; i++) { 915 month = (Month) month.previous(); 916 } 917 // need to peg the month in case the time zone isn't the 918 // default - see bug 2078057 919 month.peg(Calendar.getInstance(this.timeZone)); 920 standardDate = calculateDateForPosition( 921 month, this.tickMarkPosition); 922 } 923 return standardDate; 924 } 925 else if (DateTickUnitType.YEAR.equals(unit.getUnitType())) { 926 if (this.tickMarkPosition == DateTickMarkPosition.START) { 927 months = 0; 928 days = 1; 929 } 930 else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) { 931 months = 6; 932 days = 1; 933 } 934 else { 935 months = 11; 936 days = 31; 937 } 938 calendar.clear(Calendar.MILLISECOND); 939 calendar.set(value, months, days, 0, 0, 0); 940 Date d3 = calendar.getTime(); 941 if (d3.getTime() >= date.getTime()) { 942 calendar.set(Calendar.YEAR, value - count); 943 d3 = calendar.getTime(); 944 } 945 return d3; 946 } 947 return null; 948 } 949 950 /** 951 * Returns a {@link java.util.Date} corresponding to the specified position 952 * within a {@link RegularTimePeriod}. 953 * 954 * @param period the period. 955 * @param position the position ({@code null} not permitted). 956 * 957 * @return A date. 958 */ 959 private Date calculateDateForPosition(RegularTimePeriod period, 960 DateTickMarkPosition position) { 961 Args.nullNotPermitted(period, "period"); 962 Date result = null; 963 if (position == DateTickMarkPosition.START) { 964 result = new Date(period.getFirstMillisecond()); 965 } 966 else if (position == DateTickMarkPosition.MIDDLE) { 967 result = new Date(period.getMiddleMillisecond()); 968 } 969 else if (position == DateTickMarkPosition.END) { 970 result = new Date(period.getLastMillisecond()); 971 } 972 return result; 973 974 } 975 976 /** 977 * Returns the first "standard" date (based on the specified field and 978 * units). 979 * 980 * @param date the reference date. 981 * @param unit the date tick unit. 982 * 983 * @return The next "standard" date. 984 */ 985 protected Date nextStandardDate(Date date, DateTickUnit unit) { 986 Date previous = previousStandardDate(date, unit); 987 Calendar calendar = Calendar.getInstance(this.timeZone, this.locale); 988 calendar.setTime(previous); 989 calendar.add(unit.getCalendarField(), unit.getMultiple()); 990 return calendar.getTime(); 991 } 992 993 /** 994 * Returns a collection of standard date tick units that uses the default 995 * time zone. This collection will be used by default, but you are free 996 * to create your own collection if you want to (see the 997 * {@link ValueAxis#setStandardTickUnits(TickUnitSource)} method inherited 998 * from the {@link ValueAxis} class). 999 * 1000 * @return A collection of standard date tick units. 1001 */ 1002 public static TickUnitSource createStandardDateTickUnits() { 1003 return createStandardDateTickUnits(TimeZone.getDefault(), 1004 Locale.getDefault()); 1005 } 1006 1007 /** 1008 * Returns a collection of standard date tick units. This collection will 1009 * be used by default, but you are free to create your own collection if 1010 * you want to (see the 1011 * {@link ValueAxis#setStandardTickUnits(TickUnitSource)} method inherited 1012 * from the {@link ValueAxis} class). 1013 * 1014 * @param zone the time zone ({@code null} not permitted). 1015 * @param locale the locale ({@code null} not permitted). 1016 * 1017 * @return A collection of standard date tick units. 1018 */ 1019 public static TickUnitSource createStandardDateTickUnits(TimeZone zone, 1020 Locale locale) { 1021 1022 Args.nullNotPermitted(zone, "zone"); 1023 Args.nullNotPermitted(locale, "locale"); 1024 TickUnits units = new TickUnits(); 1025 1026 // date formatters 1027 DateFormat f1 = new SimpleDateFormat("HH:mm:ss.SSS", locale); 1028 DateFormat f2 = new SimpleDateFormat("HH:mm:ss", locale); 1029 DateFormat f3 = new SimpleDateFormat("HH:mm", locale); 1030 DateFormat f4 = new SimpleDateFormat("d-MMM, HH:mm", locale); 1031 DateFormat f5 = new SimpleDateFormat("d-MMM", locale); 1032 DateFormat f6 = new SimpleDateFormat("MMM-yyyy", locale); 1033 DateFormat f7 = new SimpleDateFormat("yyyy", locale); 1034 1035 f1.setTimeZone(zone); 1036 f2.setTimeZone(zone); 1037 f3.setTimeZone(zone); 1038 f4.setTimeZone(zone); 1039 f5.setTimeZone(zone); 1040 f6.setTimeZone(zone); 1041 f7.setTimeZone(zone); 1042 1043 // milliseconds 1044 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 1, f1)); 1045 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 5, 1046 DateTickUnitType.MILLISECOND, 1, f1)); 1047 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 10, 1048 DateTickUnitType.MILLISECOND, 1, f1)); 1049 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 25, 1050 DateTickUnitType.MILLISECOND, 5, f1)); 1051 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 50, 1052 DateTickUnitType.MILLISECOND, 10, f1)); 1053 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 100, 1054 DateTickUnitType.MILLISECOND, 10, f1)); 1055 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 250, 1056 DateTickUnitType.MILLISECOND, 10, f1)); 1057 units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 500, 1058 DateTickUnitType.MILLISECOND, 50, f1)); 1059 1060 // seconds 1061 units.add(new DateTickUnit(DateTickUnitType.SECOND, 1, 1062 DateTickUnitType.MILLISECOND, 50, f2)); 1063 units.add(new DateTickUnit(DateTickUnitType.SECOND, 5, 1064 DateTickUnitType.SECOND, 1, f2)); 1065 units.add(new DateTickUnit(DateTickUnitType.SECOND, 10, 1066 DateTickUnitType.SECOND, 1, f2)); 1067 units.add(new DateTickUnit(DateTickUnitType.SECOND, 30, 1068 DateTickUnitType.SECOND, 5, f2)); 1069 1070 // minutes 1071 units.add(new DateTickUnit(DateTickUnitType.MINUTE, 1, 1072 DateTickUnitType.SECOND, 5, f3)); 1073 units.add(new DateTickUnit(DateTickUnitType.MINUTE, 2, 1074 DateTickUnitType.SECOND, 10, f3)); 1075 units.add(new DateTickUnit(DateTickUnitType.MINUTE, 5, 1076 DateTickUnitType.MINUTE, 1, f3)); 1077 units.add(new DateTickUnit(DateTickUnitType.MINUTE, 10, 1078 DateTickUnitType.MINUTE, 1, f3)); 1079 units.add(new DateTickUnit(DateTickUnitType.MINUTE, 15, 1080 DateTickUnitType.MINUTE, 5, f3)); 1081 units.add(new DateTickUnit(DateTickUnitType.MINUTE, 20, 1082 DateTickUnitType.MINUTE, 5, f3)); 1083 units.add(new DateTickUnit(DateTickUnitType.MINUTE, 30, 1084 DateTickUnitType.MINUTE, 5, f3)); 1085 1086 // hours 1087 units.add(new DateTickUnit(DateTickUnitType.HOUR, 1, 1088 DateTickUnitType.MINUTE, 5, f3)); 1089 units.add(new DateTickUnit(DateTickUnitType.HOUR, 2, 1090 DateTickUnitType.MINUTE, 10, f3)); 1091 units.add(new DateTickUnit(DateTickUnitType.HOUR, 4, 1092 DateTickUnitType.MINUTE, 30, f3)); 1093 units.add(new DateTickUnit(DateTickUnitType.HOUR, 6, 1094 DateTickUnitType.HOUR, 1, f3)); 1095 units.add(new DateTickUnit(DateTickUnitType.HOUR, 12, 1096 DateTickUnitType.HOUR, 1, f4)); 1097 1098 // days 1099 units.add(new DateTickUnit(DateTickUnitType.DAY, 1, 1100 DateTickUnitType.HOUR, 1, f5)); 1101 units.add(new DateTickUnit(DateTickUnitType.DAY, 2, 1102 DateTickUnitType.HOUR, 1, f5)); 1103 units.add(new DateTickUnit(DateTickUnitType.DAY, 7, 1104 DateTickUnitType.DAY, 1, f5)); 1105 units.add(new DateTickUnit(DateTickUnitType.DAY, 15, 1106 DateTickUnitType.DAY, 1, f5)); 1107 1108 // months 1109 units.add(new DateTickUnit(DateTickUnitType.MONTH, 1, 1110 DateTickUnitType.DAY, 1, f6)); 1111 units.add(new DateTickUnit(DateTickUnitType.MONTH, 2, 1112 DateTickUnitType.DAY, 1, f6)); 1113 units.add(new DateTickUnit(DateTickUnitType.MONTH, 3, 1114 DateTickUnitType.MONTH, 1, f6)); 1115 units.add(new DateTickUnit(DateTickUnitType.MONTH, 4, 1116 DateTickUnitType.MONTH, 1, f6)); 1117 units.add(new DateTickUnit(DateTickUnitType.MONTH, 6, 1118 DateTickUnitType.MONTH, 1, f6)); 1119 1120 // years 1121 units.add(new DateTickUnit(DateTickUnitType.YEAR, 1, 1122 DateTickUnitType.MONTH, 1, f7)); 1123 units.add(new DateTickUnit(DateTickUnitType.YEAR, 2, 1124 DateTickUnitType.MONTH, 3, f7)); 1125 units.add(new DateTickUnit(DateTickUnitType.YEAR, 5, 1126 DateTickUnitType.YEAR, 1, f7)); 1127 units.add(new DateTickUnit(DateTickUnitType.YEAR, 10, 1128 DateTickUnitType.YEAR, 1, f7)); 1129 units.add(new DateTickUnit(DateTickUnitType.YEAR, 25, 1130 DateTickUnitType.YEAR, 5, f7)); 1131 units.add(new DateTickUnit(DateTickUnitType.YEAR, 50, 1132 DateTickUnitType.YEAR, 10, f7)); 1133 units.add(new DateTickUnit(DateTickUnitType.YEAR, 100, 1134 DateTickUnitType.YEAR, 20, f7)); 1135 1136 return units; 1137 1138 } 1139 1140 /** 1141 * Rescales the axis to ensure that all data is visible. 1142 */ 1143 @Override 1144 protected void autoAdjustRange() { 1145 1146 Plot plot = getPlot(); 1147 1148 if (plot == null) { 1149 return; // no plot, no data 1150 } 1151 1152 if (plot instanceof ValueAxisPlot) { 1153 ValueAxisPlot vap = (ValueAxisPlot) plot; 1154 1155 Range r = vap.getDataRange(this); 1156 if (r == null) { 1157 r = new DateRange(); 1158 } 1159 1160 long upper = this.timeline.toTimelineValue( 1161 (long) r.getUpperBound()); 1162 long lower; 1163 long fixedAutoRange = (long) getFixedAutoRange(); 1164 if (fixedAutoRange > 0.0) { 1165 lower = upper - fixedAutoRange; 1166 } 1167 else { 1168 lower = this.timeline.toTimelineValue((long) r.getLowerBound()); 1169 double range = upper - lower; 1170 long minRange = (long) getAutoRangeMinimumSize(); 1171 if (range < minRange) { 1172 long expand = (long) (minRange - range) / 2; 1173 upper = upper + expand; 1174 lower = lower - expand; 1175 } 1176 upper = upper + (long) (range * getUpperMargin()); 1177 lower = lower - (long) (range * getLowerMargin()); 1178 } 1179 1180 upper = this.timeline.toMillisecond(upper); 1181 lower = this.timeline.toMillisecond(lower); 1182 DateRange dr = new DateRange(new Date(lower), new Date(upper)); 1183 setRange(dr, false, false); 1184 } 1185 1186 } 1187 1188 /** 1189 * Selects an appropriate tick value for the axis. The strategy is to 1190 * display as many ticks as possible (selected from an array of 'standard' 1191 * tick units) without the labels overlapping. 1192 * 1193 * @param g2 the graphics device. 1194 * @param dataArea the area defined by the axes. 1195 * @param edge the axis location. 1196 */ 1197 protected void selectAutoTickUnit(Graphics2D g2, Rectangle2D dataArea, 1198 RectangleEdge edge) { 1199 1200 if (RectangleEdge.isTopOrBottom(edge)) { 1201 selectHorizontalAutoTickUnit(g2, dataArea, edge); 1202 } 1203 else if (RectangleEdge.isLeftOrRight(edge)) { 1204 selectVerticalAutoTickUnit(g2, dataArea, edge); 1205 } 1206 1207 } 1208 1209 /** 1210 * Selects an appropriate tick size for the axis. The strategy is to 1211 * display as many ticks as possible (selected from a collection of 1212 * 'standard' tick units) without the labels overlapping. 1213 * 1214 * @param g2 the graphics device. 1215 * @param dataArea the area defined by the axes. 1216 * @param edge the axis location. 1217 */ 1218 protected void selectHorizontalAutoTickUnit(Graphics2D g2, 1219 Rectangle2D dataArea, RectangleEdge edge) { 1220 1221 double zero = valueToJava2D(0.0, dataArea, edge); 1222 double tickLabelWidth = estimateMaximumTickLabelWidth(g2, 1223 getTickUnit()); 1224 1225 // start with the current tick unit... 1226 TickUnitSource tickUnits = getStandardTickUnits(); 1227 TickUnit unit1 = tickUnits.getCeilingTickUnit(getTickUnit()); 1228 double x1 = valueToJava2D(unit1.getSize(), dataArea, edge); 1229 double unit1Width = Math.abs(x1 - zero); 1230 1231 // then extrapolate... 1232 double guess = (tickLabelWidth / unit1Width) * unit1.getSize(); 1233 DateTickUnit unit2 = (DateTickUnit) tickUnits.getCeilingTickUnit(guess); 1234 double x2 = valueToJava2D(unit2.getSize(), dataArea, edge); 1235 double unit2Width = Math.abs(x2 - zero); 1236 tickLabelWidth = estimateMaximumTickLabelWidth(g2, unit2); 1237 if (tickLabelWidth > unit2Width) { 1238 unit2 = (DateTickUnit) tickUnits.getLargerTickUnit(unit2); 1239 } 1240 setTickUnit(unit2, false, false); 1241 } 1242 1243 /** 1244 * Selects an appropriate tick size for the axis. The strategy is to 1245 * display as many ticks as possible (selected from a collection of 1246 * 'standard' tick units) without the labels overlapping. 1247 * 1248 * @param g2 the graphics device. 1249 * @param dataArea the area in which the plot should be drawn. 1250 * @param edge the axis location. 1251 */ 1252 protected void selectVerticalAutoTickUnit(Graphics2D g2, 1253 Rectangle2D dataArea, RectangleEdge edge) { 1254 1255 // start with the current tick unit... 1256 TickUnitSource tickUnits = getStandardTickUnits(); 1257 double zero = valueToJava2D(0.0, dataArea, edge); 1258 1259 // start with a unit that is at least 1/10th of the axis length 1260 double estimate1 = getRange().getLength() / 10.0; 1261 DateTickUnit candidate1 1262 = (DateTickUnit) tickUnits.getCeilingTickUnit(estimate1); 1263 double labelHeight1 = estimateMaximumTickLabelHeight(g2, candidate1); 1264 double y1 = valueToJava2D(candidate1.getSize(), dataArea, edge); 1265 double candidate1UnitHeight = Math.abs(y1 - zero); 1266 1267 // now extrapolate based on label height and unit height... 1268 double estimate2 1269 = (labelHeight1 / candidate1UnitHeight) * candidate1.getSize(); 1270 DateTickUnit candidate2 1271 = (DateTickUnit) tickUnits.getCeilingTickUnit(estimate2); 1272 double labelHeight2 = estimateMaximumTickLabelHeight(g2, candidate2); 1273 double y2 = valueToJava2D(candidate2.getSize(), dataArea, edge); 1274 double unit2Height = Math.abs(y2 - zero); 1275 1276 // make final selection... 1277 DateTickUnit finalUnit; 1278 if (labelHeight2 < unit2Height) { 1279 finalUnit = candidate2; 1280 } 1281 else { 1282 finalUnit = (DateTickUnit) tickUnits.getLargerTickUnit(candidate2); 1283 } 1284 setTickUnit(finalUnit, false, false); 1285 1286 } 1287 1288 /** 1289 * Estimates the maximum width of the tick labels, assuming the specified 1290 * tick unit is used. 1291 * <P> 1292 * Rather than computing the string bounds of every tick on the axis, we 1293 * just look at two values: the lower bound and the upper bound for the 1294 * axis. These two values will usually be representative. 1295 * 1296 * @param g2 the graphics device. 1297 * @param unit the tick unit to use for calculation. 1298 * 1299 * @return The estimated maximum width of the tick labels. 1300 */ 1301 private double estimateMaximumTickLabelWidth(Graphics2D g2, 1302 DateTickUnit unit) { 1303 1304 RectangleInsets tickLabelInsets = getTickLabelInsets(); 1305 double result = tickLabelInsets.getLeft() + tickLabelInsets.getRight(); 1306 1307 Font tickLabelFont = getTickLabelFont(); 1308 FontRenderContext frc = g2.getFontRenderContext(); 1309 LineMetrics lm = tickLabelFont.getLineMetrics("ABCxyz", frc); 1310 if (isVerticalTickLabels()) { 1311 // all tick labels have the same width (equal to the height of 1312 // the font)... 1313 result += lm.getHeight(); 1314 } 1315 else { 1316 // look at lower and upper bounds... 1317 DateRange range = (DateRange) getRange(); 1318 Date lower = range.getLowerDate(); 1319 Date upper = range.getUpperDate(); 1320 String lowerStr, upperStr; 1321 DateFormat formatter = getDateFormatOverride(); 1322 if (formatter != null) { 1323 lowerStr = formatter.format(lower); 1324 upperStr = formatter.format(upper); 1325 } 1326 else { 1327 lowerStr = unit.dateToString(lower); 1328 upperStr = unit.dateToString(upper); 1329 } 1330 FontMetrics fm = g2.getFontMetrics(tickLabelFont); 1331 double w1 = fm.stringWidth(lowerStr); 1332 double w2 = fm.stringWidth(upperStr); 1333 result += Math.max(w1, w2); 1334 } 1335 1336 return result; 1337 1338 } 1339 1340 /** 1341 * Estimates the maximum width of the tick labels, assuming the specified 1342 * tick unit is used. 1343 * <P> 1344 * Rather than computing the string bounds of every tick on the axis, we 1345 * just look at two values: the lower bound and the upper bound for the 1346 * axis. These two values will usually be representative. 1347 * 1348 * @param g2 the graphics device. 1349 * @param unit the tick unit to use for calculation. 1350 * 1351 * @return The estimated maximum width of the tick labels. 1352 */ 1353 private double estimateMaximumTickLabelHeight(Graphics2D g2, 1354 DateTickUnit unit) { 1355 1356 RectangleInsets tickLabelInsets = getTickLabelInsets(); 1357 double result = tickLabelInsets.getTop() + tickLabelInsets.getBottom(); 1358 1359 Font tickLabelFont = getTickLabelFont(); 1360 FontRenderContext frc = g2.getFontRenderContext(); 1361 LineMetrics lm = tickLabelFont.getLineMetrics("ABCxyz", frc); 1362 if (!isVerticalTickLabels()) { 1363 // all tick labels have the same width (equal to the height of 1364 // the font)... 1365 result += lm.getHeight(); 1366 } 1367 else { 1368 // look at lower and upper bounds... 1369 DateRange range = (DateRange) getRange(); 1370 Date lower = range.getLowerDate(); 1371 Date upper = range.getUpperDate(); 1372 String lowerStr, upperStr; 1373 DateFormat formatter = getDateFormatOverride(); 1374 if (formatter != null) { 1375 lowerStr = formatter.format(lower); 1376 upperStr = formatter.format(upper); 1377 } 1378 else { 1379 lowerStr = unit.dateToString(lower); 1380 upperStr = unit.dateToString(upper); 1381 } 1382 FontMetrics fm = g2.getFontMetrics(tickLabelFont); 1383 double w1 = fm.stringWidth(lowerStr); 1384 double w2 = fm.stringWidth(upperStr); 1385 result += Math.max(w1, w2); 1386 } 1387 1388 return result; 1389 1390 } 1391 1392 /** 1393 * Calculates the positions of the tick labels for the axis, storing the 1394 * results in the tick label list (ready for drawing). 1395 * 1396 * @param g2 the graphics device. 1397 * @param state the axis state. 1398 * @param dataArea the area in which the plot should be drawn. 1399 * @param edge the location of the axis. 1400 * 1401 * @return A list of ticks. 1402 */ 1403 @Override 1404 public List refreshTicks(Graphics2D g2, AxisState state, 1405 Rectangle2D dataArea, RectangleEdge edge) { 1406 1407 List result = null; 1408 if (RectangleEdge.isTopOrBottom(edge)) { 1409 result = refreshTicksHorizontal(g2, dataArea, edge); 1410 } 1411 else if (RectangleEdge.isLeftOrRight(edge)) { 1412 result = refreshTicksVertical(g2, dataArea, edge); 1413 } 1414 return result; 1415 1416 } 1417 1418 /** 1419 * Corrects the given tick date for the position setting. 1420 * 1421 * @param time the tick date/time. 1422 * @param unit the tick unit. 1423 * @param position the tick position. 1424 * 1425 * @return The adjusted time. 1426 */ 1427 private Date correctTickDateForPosition(Date time, DateTickUnit unit, 1428 DateTickMarkPosition position) { 1429 Date result = time; 1430 if (unit.getUnitType().equals(DateTickUnitType.MONTH)) { 1431 result = calculateDateForPosition(new Month(time, this.timeZone, 1432 this.locale), position); 1433 } else if (unit.getUnitType().equals(DateTickUnitType.YEAR)) { 1434 result = calculateDateForPosition(new Year(time, this.timeZone, 1435 this.locale), position); 1436 } 1437 return result; 1438 } 1439 1440 /** 1441 * Recalculates the ticks for the date axis. 1442 * 1443 * @param g2 the graphics device. 1444 * @param dataArea the area in which the data is to be drawn. 1445 * @param edge the location of the axis. 1446 * 1447 * @return A list of ticks. 1448 */ 1449 protected List refreshTicksHorizontal(Graphics2D g2, 1450 Rectangle2D dataArea, RectangleEdge edge) { 1451 1452 List result = new java.util.ArrayList(); 1453 1454 Font tickLabelFont = getTickLabelFont(); 1455 g2.setFont(tickLabelFont); 1456 1457 if (isAutoTickUnitSelection()) { 1458 selectAutoTickUnit(g2, dataArea, edge); 1459 } 1460 1461 DateTickUnit unit = getTickUnit(); 1462 Date tickDate = calculateLowestVisibleTickValue(unit); 1463 Date upperDate = getMaximumDate(); 1464 1465 boolean hasRolled = false; 1466 while (tickDate.before(upperDate)) { 1467 // could add a flag to make the following correction optional... 1468 if (!hasRolled) { 1469 tickDate = correctTickDateForPosition(tickDate, unit, 1470 this.tickMarkPosition); 1471 } 1472 1473 long lowestTickTime = tickDate.getTime(); 1474 long distance = unit.addToDate(tickDate, this.timeZone).getTime() 1475 - lowestTickTime; 1476 int minorTickSpaces = getMinorTickCount(); 1477 if (minorTickSpaces <= 0) { 1478 minorTickSpaces = unit.getMinorTickCount(); 1479 } 1480 for (int minorTick = 1; minorTick < minorTickSpaces; minorTick++) { 1481 long minorTickTime = lowestTickTime - distance 1482 * minorTick / minorTickSpaces; 1483 if (minorTickTime > 0 && getRange().contains(minorTickTime) 1484 && (!isHiddenValue(minorTickTime))) { 1485 result.add(new DateTick(TickType.MINOR, 1486 new Date(minorTickTime), "", TextAnchor.TOP_CENTER, 1487 TextAnchor.CENTER, 0.0)); 1488 } 1489 } 1490 1491 if (!isHiddenValue(tickDate.getTime())) { 1492 // work out the value, label and position 1493 String tickLabel; 1494 DateFormat formatter = getDateFormatOverride(); 1495 if (formatter != null) { 1496 tickLabel = formatter.format(tickDate); 1497 } 1498 else { 1499 tickLabel = this.tickUnit.dateToString(tickDate); 1500 } 1501 TextAnchor anchor, rotationAnchor; 1502 double angle = 0.0; 1503 if (isVerticalTickLabels()) { 1504 anchor = TextAnchor.CENTER_RIGHT; 1505 rotationAnchor = TextAnchor.CENTER_RIGHT; 1506 if (edge == RectangleEdge.TOP) { 1507 angle = Math.PI / 2.0; 1508 } 1509 else { 1510 angle = -Math.PI / 2.0; 1511 } 1512 } 1513 else { 1514 if (edge == RectangleEdge.TOP) { 1515 anchor = TextAnchor.BOTTOM_CENTER; 1516 rotationAnchor = TextAnchor.BOTTOM_CENTER; 1517 } 1518 else { 1519 anchor = TextAnchor.TOP_CENTER; 1520 rotationAnchor = TextAnchor.TOP_CENTER; 1521 } 1522 } 1523 1524 Tick tick = new DateTick(tickDate, tickLabel, anchor, 1525 rotationAnchor, angle); 1526 result.add(tick); 1527 hasRolled = false; 1528 1529 long currentTickTime = tickDate.getTime(); 1530 tickDate = unit.addToDate(tickDate, this.timeZone); 1531 long nextTickTime = tickDate.getTime(); 1532 for (int minorTick = 1; minorTick < minorTickSpaces; 1533 minorTick++) { 1534 long minorTickTime = currentTickTime 1535 + (nextTickTime - currentTickTime) 1536 * minorTick / minorTickSpaces; 1537 if (getRange().contains(minorTickTime) 1538 && (!isHiddenValue(minorTickTime))) { 1539 result.add(new DateTick(TickType.MINOR, 1540 new Date(minorTickTime), "", 1541 TextAnchor.TOP_CENTER, TextAnchor.CENTER, 1542 0.0)); 1543 } 1544 } 1545 1546 } 1547 else { 1548 tickDate = unit.rollDate(tickDate, this.timeZone); 1549 hasRolled = true; 1550 } 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 refreshTicksVertical(Graphics2D g2, 1567 Rectangle2D dataArea, RectangleEdge edge) { 1568 1569 List result = new java.util.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 Tick 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, 1780 that.dateFormatOverride)) { 1781 return false; 1782 } 1783 if (!Objects.equals(this.tickMarkPosition, that.tickMarkPosition)) { 1784 return false; 1785 } 1786 if (!Objects.equals(this.timeline, that.timeline)) { 1787 return false; 1788 } 1789 return super.equals(obj); 1790 } 1791 1792 /** 1793 * Returns a hash code for this object. 1794 * 1795 * @return A hash code. 1796 */ 1797 @Override 1798 public int hashCode() { 1799 return super.hashCode(); 1800 } 1801 1802 /** 1803 * Returns a clone of the object. 1804 * 1805 * @return A clone. 1806 * 1807 * @throws CloneNotSupportedException if some component of the axis does 1808 * not support cloning. 1809 */ 1810 @Override 1811 public Object clone() throws CloneNotSupportedException { 1812 DateAxis clone = (DateAxis) super.clone(); 1813 // 'dateTickUnit' is immutable : no need to clone 1814 if (this.dateFormatOverride != null) { 1815 clone.dateFormatOverride 1816 = (DateFormat) this.dateFormatOverride.clone(); 1817 } 1818 // 'tickMarkPosition' is immutable : no need to clone 1819 return clone; 1820 } 1821 1822}