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