Source for org.jfree.chart.axis.DateAxis

   1: /* ===========================================================
   2:  * JFreeChart : a free chart library for the Java(tm) platform
   3:  * ===========================================================
   4:  *
   5:  * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
   6:  *
   7:  * Project Info:  http://www.jfree.org/jfreechart/index.html
   8:  *
   9:  * This library is free software; you can redistribute it and/or modify it 
  10:  * under the terms of the GNU Lesser General Public License as published by 
  11:  * the Free Software Foundation; either version 2.1 of the License, or 
  12:  * (at your option) any later version.
  13:  *
  14:  * This library is distributed in the hope that it will be useful, but 
  15:  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
  16:  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
  17:  * License for more details.
  18:  *
  19:  * You should have received a copy of the GNU Lesser General Public
  20:  * License along with this library; if not, write to the Free Software
  21:  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
  22:  * USA.  
  23:  *
  24:  * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
  25:  * in the United States and other countries.]
  26:  *
  27:  * -------------
  28:  * DateAxis.java
  29:  * -------------
  30:  * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
  31:  *
  32:  * Original Author:  David Gilbert;
  33:  * Contributor(s):   Jonathan Nash;
  34:  *                   David Li;
  35:  *                   Michael Rauch;
  36:  *                   Bill Kelemen;
  37:  *                   Pawel Pabis;
  38:  *
  39:  * $Id: DateAxis.java,v 1.17.2.8 2007/01/18 15:20:34 mungady Exp $
  40:  *
  41:  * Changes (from 23-Jun-2001)
  42:  * --------------------------
  43:  * 23-Jun-2001 : Modified to work with null data source (DG);
  44:  * 18-Sep-2001 : Updated header (DG);
  45:  * 27-Nov-2001 : Changed constructors from public to protected, updated Javadoc 
  46:  *               comments (DG);
  47:  * 16-Jan-2002 : Added an optional crosshair, based on the implementation by 
  48:  *               Jonathan Nash (DG);
  49:  * 26-Feb-2002 : Updated import statements (DG);
  50:  * 22-Apr-2002 : Added a setRange() method (DG);
  51:  * 25-Jun-2002 : Removed redundant local variable (DG);
  52:  * 25-Jul-2002 : Changed order of parameters in ValueAxis constructor (DG);
  53:  * 21-Aug-2002 : The setTickUnit() method now turns off auto-tick unit 
  54:  *               selection (fix for bug id 528885) (DG);
  55:  * 05-Sep-2002 : Updated the constructors to reflect changes in the Axis 
  56:  *               class (DG);
  57:  * 18-Sep-2002 : Fixed errors reported by Checkstyle (DG);
  58:  * 25-Sep-2002 : Added new setRange() methods, and deprecated 
  59:  *               setAxisRange() (DG);
  60:  * 04-Oct-2002 : Changed auto tick selection to parallel number axis 
  61:  *               classes (DG);
  62:  * 24-Oct-2002 : Added a date format override (DG);
  63:  * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG);
  64:  * 14-Jan-2003 : Changed autoRangeMinimumSize from Number --> double, moved
  65:  *               crosshair settings to the plot (DG);
  66:  * 15-Jan-2003 : Removed anchor date (DG);
  67:  * 20-Jan-2003 : Removed unnecessary constructors (DG);
  68:  * 26-Mar-2003 : Implemented Serializable (DG);
  69:  * 02-May-2003 : Added additional units to createStandardDateTickUnits() 
  70:  *               method, as suggested by mhilpert in bug report 723187 (DG);
  71:  * 13-May-2003 : Merged HorizontalDateAxis and VerticalDateAxis (DG);
  72:  * 24-May-2003 : Added support for underlying timeline for 
  73:  *               SegmentedTimeline (BK);
  74:  * 16-Jul-2003 : Applied patch from Pawel Pabis to fix overlapping dates (DG);
  75:  * 22-Jul-2003 : Applied patch from Pawel Pabis for monthly ticks (DG);
  76:  * 25-Jul-2003 : Fixed bug 777561 and 777586 (DG);
  77:  * 13-Aug-2003 : Implemented Cloneable and added equals() method (DG);
  78:  * 02-Sep-2003 : Fixes for bug report 790506 (DG);
  79:  * 04-Sep-2003 : Fixed tick label alignment when axis appears at the top (DG);
  80:  * 10-Sep-2003 : Fixes for segmented timeline (DG);
  81:  * 17-Sep-2003 : Fixed a layout bug when multiple domain axes are used (DG);
  82:  * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);
  83:  * 07-Nov-2003 : Modified to use new tick classes (DG);
  84:  * 12-Nov-2003 : Modified tick labelling to use roll unit from DateTickUnit 
  85:  *               when a calculated tick value is hidden (which can occur in 
  86:  *               segmented date axes) (DG);
  87:  * 24-Nov-2003 : Fixed some problems with the auto tick unit selection, and 
  88:  *               fixed bug 846277 (labels missing for inverted axis) (DG);
  89:  * 30-Dec-2003 : Fixed bug in refreshTicksHorizontal() when start of time unit 
  90:  *               (ex. 1st of month) was hidden, causing infinite loop (BK);
  91:  * 13-Jan-2004 : Fixed bug in previousStandardDate() method (fix by Richard 
  92:  *               Wardle) (DG);
  93:  * 21-Jan-2004 : Renamed translateJava2DToValue --> java2DToValue, and 
  94:  *               translateValueToJava2D --> valueToJava2D (DG); 
  95:  * 12-Mar-2004 : Fixed bug where date format override is ignored for vertical 
  96:  *               axis (DG);
  97:  * 16-Mar-2004 : Added plotState to draw() method (DG);
  98:  * 07-Apr-2004 : Changed string width calculation (DG);
  99:  * 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: }