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