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