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