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