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