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