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