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