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