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