001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2007, 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     * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
025     * in the United States and other countries.]
026     *
027     * -------------
028     * DateAxis.java
029     * -------------
030     * (C) Copyright 2000-2007, 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     *
040     * $Id: DateAxis.java,v 1.17.2.11 2007/05/03 14:27:11 mungady Exp $
041     *
042     * Changes (from 23-Jun-2001)
043     * --------------------------
044     * 23-Jun-2001 : Modified to work with null data source (DG);
045     * 18-Sep-2001 : Updated header (DG);
046     * 27-Nov-2001 : Changed constructors from public to protected, updated Javadoc 
047     *               comments (DG);
048     * 16-Jan-2002 : Added an optional crosshair, based on the implementation by 
049     *               Jonathan Nash (DG);
050     * 26-Feb-2002 : Updated import statements (DG);
051     * 22-Apr-2002 : Added a setRange() method (DG);
052     * 25-Jun-2002 : Removed redundant local variable (DG);
053     * 25-Jul-2002 : Changed order of parameters in ValueAxis constructor (DG);
054     * 21-Aug-2002 : The setTickUnit() method now turns off auto-tick unit 
055     *               selection (fix for bug id 528885) (DG);
056     * 05-Sep-2002 : Updated the constructors to reflect changes in the Axis 
057     *               class (DG);
058     * 18-Sep-2002 : Fixed errors reported by Checkstyle (DG);
059     * 25-Sep-2002 : Added new setRange() methods, and deprecated 
060     *               setAxisRange() (DG);
061     * 04-Oct-2002 : Changed auto tick selection to parallel number axis 
062     *               classes (DG);
063     * 24-Oct-2002 : Added a date format override (DG);
064     * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG);
065     * 14-Jan-2003 : Changed autoRangeMinimumSize from Number --> double, moved
066     *               crosshair settings to the plot (DG);
067     * 15-Jan-2003 : Removed anchor date (DG);
068     * 20-Jan-2003 : Removed unnecessary constructors (DG);
069     * 26-Mar-2003 : Implemented Serializable (DG);
070     * 02-May-2003 : Added additional units to createStandardDateTickUnits() 
071     *               method, as suggested by mhilpert in bug report 723187 (DG);
072     * 13-May-2003 : Merged HorizontalDateAxis and VerticalDateAxis (DG);
073     * 24-May-2003 : Added support for underlying timeline for 
074     *               SegmentedTimeline (BK);
075     * 16-Jul-2003 : Applied patch from Pawel Pabis to fix overlapping dates (DG);
076     * 22-Jul-2003 : Applied patch from Pawel Pabis for monthly ticks (DG);
077     * 25-Jul-2003 : Fixed bug 777561 and 777586 (DG);
078     * 13-Aug-2003 : Implemented Cloneable and added equals() method (DG);
079     * 02-Sep-2003 : Fixes for bug report 790506 (DG);
080     * 04-Sep-2003 : Fixed tick label alignment when axis appears at the top (DG);
081     * 10-Sep-2003 : Fixes for segmented timeline (DG);
082     * 17-Sep-2003 : Fixed a layout bug when multiple domain axes are used (DG);
083     * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);
084     * 07-Nov-2003 : Modified to use new tick classes (DG);
085     * 12-Nov-2003 : Modified tick labelling to use roll unit from DateTickUnit 
086     *               when a calculated tick value is hidden (which can occur in 
087     *               segmented date axes) (DG);
088     * 24-Nov-2003 : Fixed some problems with the auto tick unit selection, and 
089     *               fixed bug 846277 (labels missing for inverted axis) (DG);
090     * 30-Dec-2003 : Fixed bug in refreshTicksHorizontal() when start of time unit 
091     *               (ex. 1st of month) was hidden, causing infinite loop (BK);
092     * 13-Jan-2004 : Fixed bug in previousStandardDate() method (fix by Richard 
093     *               Wardle) (DG);
094     * 21-Jan-2004 : Renamed translateJava2DToValue --> java2DToValue, and 
095     *               translateValueToJava2D --> valueToJava2D (DG); 
096     * 12-Mar-2004 : Fixed bug where date format override is ignored for vertical 
097     *               axis (DG);
098     * 16-Mar-2004 : Added plotState to draw() method (DG);
099     * 07-Apr-2004 : Changed string width calculation (DG);
100     * 21-Apr-2004 : Fixed bug in estimateMaximumTickLabelWidth() method (bug id 
101     *               939148) (DG);
102     * 11-Jan-2005 : Removed deprecated methods in preparation for 1.0.0 
103     *               release (DG);
104     * 13-Jan-2005 : Fixed bug (see 
105     *               http://www.jfree.org/forum/viewtopic.php?t=11330) (DG);
106     * 21-Apr-2005 : Replaced Insets with RectangleInsets, removed redundant 
107     *               argument from selectAutoTickUnit() (DG);
108     * ------------- JFREECHART 1.0.x ---------------------------------------------
109     * 10-Feb-2006 : Added some API doc comments in respect of bug 821046 (DG);
110     * 19-Apr-2006 : Fixed bug 1472942 in equals() method (DG);
111     * 25-Sep-2006 : Fixed bug 1564977 missing tick labels (DG);
112     * 15-Jan-2007 : Added get/setTimeZone() suggested by 'skunk' (DG);
113     * 18-Jan-2007 : Fixed bug 1638678, time zone for calendar in 
114     *               previousStandardDate() (DG);
115     * 04-Apr-2007 : Use time zone in date calculations (CB);
116     * 19-Apr-2007 : Fix exceptions in setMinimum/MaximumDate() (DG);
117     * 03-May-2007 : Fixed minor bugs in previousStandardDate(), with new JUnit
118     *               tests (DG);
119     * 
120     */
121    
122    package org.jfree.chart.axis;
123    
124    import java.awt.Font;
125    import java.awt.FontMetrics;
126    import java.awt.Graphics2D;
127    import java.awt.font.FontRenderContext;
128    import java.awt.font.LineMetrics;
129    import java.awt.geom.Rectangle2D;
130    import java.io.Serializable;
131    import java.text.DateFormat;
132    import java.text.SimpleDateFormat;
133    import java.util.Calendar;
134    import java.util.Date;
135    import java.util.List;
136    import java.util.TimeZone;
137    
138    import org.jfree.chart.event.AxisChangeEvent;
139    import org.jfree.chart.plot.Plot;
140    import org.jfree.chart.plot.PlotRenderingInfo;
141    import org.jfree.chart.plot.ValueAxisPlot;
142    import org.jfree.data.Range;
143    import org.jfree.data.time.DateRange;
144    import org.jfree.data.time.Month;
145    import org.jfree.data.time.RegularTimePeriod;
146    import org.jfree.data.time.Year;
147    import org.jfree.ui.RectangleEdge;
148    import org.jfree.ui.RectangleInsets;
149    import org.jfree.ui.TextAnchor;
150    import org.jfree.util.ObjectUtilities;
151    
152    /**
153     * The base class for axes that display dates.  You will find it easier to 
154     * understand how this axis works if you bear in mind that it really 
155     * displays/measures integer (or long) data, where the integers are 
156     * milliseconds since midnight, 1-Jan-1970.  When displaying tick labels, the 
157     * millisecond values are converted back to dates using a 
158     * <code>DateFormat</code> instance.
159     * <P>
160     * You can also create a {@link org.jfree.chart.axis.Timeline} and supply in 
161     * the constructor to create an axis that only contains certain domain values. 
162     * For example, this allows you to create a date axis that only contains 
163     * working days.
164     */
165    public class DateAxis extends ValueAxis implements Cloneable, Serializable {
166    
167        /** For serialization. */
168        private static final long serialVersionUID = -1013460999649007604L;
169        
170        /** The default axis range. */
171        public static final DateRange DEFAULT_DATE_RANGE = new DateRange();
172    
173        /** The default minimum auto range size. */
174        public static final double 
175                DEFAULT_AUTO_RANGE_MINIMUM_SIZE_IN_MILLISECONDS = 2.0;
176    
177        /** The default date tick unit. */
178        public static final DateTickUnit DEFAULT_DATE_TICK_UNIT
179                = new DateTickUnit(DateTickUnit.DAY, 1, new SimpleDateFormat());
180    
181        /** The default anchor date. */
182        public static final Date DEFAULT_ANCHOR_DATE = new Date();
183    
184        /** The current tick unit. */
185        private DateTickUnit tickUnit;
186    
187        /** The override date format. */
188        private DateFormat dateFormatOverride;
189    
190        /** 
191         * Tick marks can be displayed at the start or the middle of the time 
192         * period. 
193         */
194        private DateTickMarkPosition tickMarkPosition = DateTickMarkPosition.START;
195    
196        /**
197         * A timeline that includes all milliseconds (as defined by 
198         * <code>java.util.Date</code>) in the real time line.
199         */
200        private static class DefaultTimeline implements Timeline, Serializable {
201    
202            /**
203             * Converts a millisecond into a timeline value.
204             *
205             * @param millisecond  the millisecond.
206             *
207             * @return The timeline value.
208             */
209            public long toTimelineValue(long millisecond) {
210                return millisecond;
211            }
212    
213            /**
214             * Converts a date into a timeline value.
215             *
216             * @param date  the domain value.
217             *
218             * @return The timeline value.
219             */
220            public long toTimelineValue(Date date) {
221                return date.getTime();
222            }
223    
224            /**
225             * Converts a timeline value into a millisecond (as encoded by 
226             * <code>java.util.Date</code>).
227             *
228             * @param value  the value.
229             *
230             * @return The millisecond.
231             */
232            public long toMillisecond(long value) {
233                return value;
234            }
235    
236            /**
237             * Returns <code>true</code> if the timeline includes the specified 
238             * domain value.
239             *
240             * @param millisecond  the millisecond.
241             *
242             * @return <code>true</code>.
243             */
244            public boolean containsDomainValue(long millisecond) {
245                return true;
246            }
247    
248            /**
249             * Returns <code>true</code> if the timeline includes the specified 
250             * domain value.
251             *
252             * @param date  the date.
253             *
254             * @return <code>true</code>.
255             */
256            public boolean containsDomainValue(Date date) {
257                return true;
258            }
259    
260            /**
261             * Returns <code>true</code> if the timeline includes the specified 
262             * domain value range.
263             *
264             * @param from  the start value.
265             * @param to  the end value.
266             *
267             * @return <code>true</code>.
268             */
269            public boolean containsDomainRange(long from, long to) {
270                return true;
271            }
272    
273            /**
274             * Returns <code>true</code> if the timeline includes the specified 
275             * domain value range.
276             *
277             * @param from  the start date.
278             * @param to  the end date.
279             *
280             * @return <code>true</code>.
281             */
282            public boolean containsDomainRange(Date from, Date to) {
283                return true;
284            }
285    
286            /**
287             * Tests an object for equality with this instance.
288             *
289             * @param object  the object.
290             *
291             * @return A boolean.
292             */
293            public boolean equals(Object object) {
294                if (object == null) {
295                    return false;
296                }
297                if (object == this) {
298                    return true;
299                }
300                if (object instanceof DefaultTimeline) {
301                    return true;
302                }
303                return false;
304            }
305        }
306    
307        /** A static default timeline shared by all standard DateAxis */
308        private static final Timeline DEFAULT_TIMELINE = new DefaultTimeline();
309    
310        /** The time zone for the axis. */
311        private TimeZone timeZone;
312        
313        /** Our underlying timeline. */
314        private Timeline timeline;
315    
316        /**
317         * Creates a date axis with no label.
318         */
319        public DateAxis() {
320            this(null);
321        }
322    
323        /**
324         * Creates a date axis with the specified label.
325         *
326         * @param label  the axis label (<code>null</code> permitted).
327         */
328        public DateAxis(String label) {
329            this(label, TimeZone.getDefault());
330        }
331    
332        /**
333         * Creates a date axis. A timeline is specified for the axis. This allows 
334         * special transformations to occur between a domain of values and the 
335         * values included in the axis.
336         *
337         * @see org.jfree.chart.axis.SegmentedTimeline
338         *
339         * @param label  the axis label (<code>null</code> permitted).
340         * @param zone  the time zone.
341         */
342        public DateAxis(String label, TimeZone zone) {
343            super(label, DateAxis.createStandardDateTickUnits(zone));
344            setTickUnit(DateAxis.DEFAULT_DATE_TICK_UNIT, false, false);
345            setAutoRangeMinimumSize(
346                    DEFAULT_AUTO_RANGE_MINIMUM_SIZE_IN_MILLISECONDS);
347            setRange(DEFAULT_DATE_RANGE, false, false);
348            this.dateFormatOverride = null;
349            this.timeZone = zone;
350            this.timeline = DEFAULT_TIMELINE;
351        }
352    
353        /**
354         * Returns the time zone for the axis.
355         * 
356         * @return The time zone.
357         * 
358         * @since 1.0.4
359         * @see #setTimeZone(TimeZone)
360         */
361        public TimeZone getTimeZone() {
362            return this.timeZone;
363        }
364        
365        /**
366         * Sets the time zone for the axis and sends an {@link AxisChangeEvent} to
367         * all registered listeners.
368         * 
369         * @param zone  the time zone (<code>null</code> not permitted).
370         * 
371         * @since 1.0.4
372         * @see #getTimeZone()
373         */
374        public void setTimeZone(TimeZone zone) {
375            if (!this.timeZone.equals(zone)) {
376                this.timeZone = zone;
377                setStandardTickUnits(createStandardDateTickUnits(zone));
378                notifyListeners(new AxisChangeEvent(this));
379            }
380        } 
381        
382        /**
383         * Returns the underlying timeline used by this axis.
384         *
385         * @return The timeline.
386         */
387        public Timeline getTimeline() {
388            return this.timeline;
389        }
390    
391        /**
392         * Sets the underlying timeline to use for this axis.
393         * <P>
394         * If the timeline is changed, an {@link AxisChangeEvent} is sent to all
395         * registered listeners.
396         *
397         * @param timeline  the timeline.
398         */
399        public void setTimeline(Timeline timeline) {
400            if (this.timeline != timeline) {
401                this.timeline = timeline;
402                notifyListeners(new AxisChangeEvent(this));
403            }
404        }
405    
406        /**
407         * Returns the tick unit for the axis.
408         * <p>
409         * Note: if the <code>autoTickUnitSelection</code> flag is 
410         * <code>true</code> the tick unit may be changed while the axis is being 
411         * drawn, so in that case the return value from this method may be
412         * irrelevant if the method is called before the axis has been drawn.
413         *
414         * @return The tick unit (possibly <code>null</code>).
415         * 
416         * @see #setTickUnit(DateTickUnit)
417         * @see ValueAxis#isAutoTickUnitSelection()
418         */
419        public DateTickUnit getTickUnit() {
420            return this.tickUnit;
421        }
422    
423        /**
424         * Sets the tick unit for the axis.  The auto-tick-unit-selection flag is 
425         * set to <code>false</code>, and registered listeners are notified that 
426         * the axis has been changed.
427         *
428         * @param unit  the tick unit.
429         * 
430         * @see #getTickUnit()
431         * @see #setTickUnit(DateTickUnit, boolean, boolean)
432         */
433        public void setTickUnit(DateTickUnit unit) {
434            setTickUnit(unit, true, true);
435        }
436    
437        /**
438         * Sets the tick unit attribute.
439         *
440         * @param unit  the new tick unit.
441         * @param notify  notify registered listeners?
442         * @param turnOffAutoSelection  turn off auto selection?
443         * 
444         * @see #getTickUnit()
445         */
446        public void setTickUnit(DateTickUnit unit, boolean notify, 
447                                boolean turnOffAutoSelection) {
448    
449            this.tickUnit = unit;
450            if (turnOffAutoSelection) {
451                setAutoTickUnitSelection(false, false);
452            }
453            if (notify) {
454                notifyListeners(new AxisChangeEvent(this));
455            }
456    
457        }
458    
459        /**
460         * Returns the date format override.  If this is non-null, then it will be
461         * used to format the dates on the axis.
462         *
463         * @return The formatter (possibly <code>null</code>).
464         */
465        public DateFormat getDateFormatOverride() {
466            return this.dateFormatOverride;
467        }
468    
469        /**
470         * Sets the date format override.  If this is non-null, then it will be 
471         * used to format the dates on the axis.
472         *
473         * @param formatter  the date formatter (<code>null</code> permitted).
474         */
475        public void setDateFormatOverride(DateFormat formatter) {
476            this.dateFormatOverride = formatter;
477            notifyListeners(new AxisChangeEvent(this));
478        }
479    
480        /**
481         * Sets the upper and lower bounds for the axis and sends an 
482         * {@link AxisChangeEvent} to all registered listeners.  As a side-effect, 
483         * the auto-range flag is set to false.
484         *
485         * @param range  the new range (<code>null</code> not permitted).
486         */
487        public void setRange(Range range) {
488            setRange(range, true, true);
489        }
490    
491        /**
492         * Sets the range for the axis, if requested, sends an 
493         * {@link AxisChangeEvent} to all registered listeners.  As a side-effect, 
494         * the auto-range flag is set to <code>false</code> (optional).
495         *
496         * @param range  the range (<code>null</code> not permitted).
497         * @param turnOffAutoRange  a flag that controls whether or not the auto 
498         *                          range is turned off.
499         * @param notify  a flag that controls whether or not listeners are 
500         *                notified.
501         */
502        public void setRange(Range range, boolean turnOffAutoRange, 
503                             boolean notify) {
504            if (range == null) {
505                throw new IllegalArgumentException("Null 'range' argument.");
506            }
507            // usually the range will be a DateRange, but if it isn't do a 
508            // conversion...
509            if (!(range instanceof DateRange)) {
510                range = new DateRange(range);
511            }
512            super.setRange(range, turnOffAutoRange, notify);
513        }
514    
515        /**
516         * Sets the axis range and sends an {@link AxisChangeEvent} to all 
517         * registered listeners.
518         *
519         * @param lower  the lower bound for the axis.
520         * @param upper  the upper bound for the axis.
521         */
522        public void setRange(Date lower, Date upper) {
523            if (lower.getTime() >= upper.getTime()) {
524                throw new IllegalArgumentException("Requires 'lower' < 'upper'.");
525            }
526            setRange(new DateRange(lower, upper));
527        }
528    
529        /**
530         * Sets the axis range and sends an {@link AxisChangeEvent} to all 
531         * registered listeners.
532         *
533         * @param lower  the lower bound for the axis.
534         * @param upper  the upper bound for the axis.
535         */
536        public void setRange(double lower, double upper) {
537            if (lower >= upper) {
538                throw new IllegalArgumentException("Requires 'lower' < 'upper'.");
539            }
540            setRange(new DateRange(lower, upper));
541        }
542    
543        /**
544         * Returns the earliest date visible on the axis.
545         *
546         * @return The date.
547         * 
548         * @see #setMinimumDate(Date)
549         * @see #getMaximumDate()
550         */
551        public Date getMinimumDate() {
552            Date result = null;
553            Range range = getRange();
554            if (range instanceof DateRange) {
555                DateRange r = (DateRange) range;
556                result = r.getLowerDate();
557            }
558            else {
559                result = new Date((long) range.getLowerBound());
560            }
561            return result;
562        }
563    
564        /**
565         * Sets the minimum date visible on the axis and sends an 
566         * {@link AxisChangeEvent} to all registered listeners.  If 
567         * <code>date</code> is on or after the current maximum date for 
568         * the axis, the maximum date will be shifted to preserve the current
569         * length of the axis.
570         *
571         * @param date  the date (<code>null</code> not permitted).
572         * 
573         * @see #getMinimumDate()
574         * @see #setMaximumDate(Date)
575         */
576        public void setMinimumDate(Date date) {
577            if (date == null) {
578                throw new IllegalArgumentException("Null 'date' argument.");
579            }
580            // check the new minimum date relative to the current maximum date
581            Date maxDate = getMaximumDate();
582            long maxMillis = maxDate.getTime();
583            long newMinMillis = date.getTime();
584            if (maxMillis <= newMinMillis) {
585                Date oldMin = getMinimumDate();
586                long length = maxMillis - oldMin.getTime();
587                maxDate = new Date(newMinMillis + length);
588            }
589            setRange(new DateRange(date, maxDate), true, false);
590            notifyListeners(new AxisChangeEvent(this));
591        }
592    
593        /**
594         * Returns the latest date visible on the axis.
595         *
596         * @return The date.
597         * 
598         * @see #setMaximumDate(Date)
599         * @see #getMinimumDate()
600         */
601        public Date getMaximumDate() {
602            Date result = null;
603            Range range = getRange();
604            if (range instanceof DateRange) {
605                DateRange r = (DateRange) range;
606                result = r.getUpperDate();
607            }
608            else {
609                result = new Date((long) range.getUpperBound());
610            }
611            return result;
612        }
613    
614        /**
615         * Sets the maximum date visible on the axis and sends an 
616         * {@link AxisChangeEvent} to all registered listeners.  If 
617         * <code>maximumDate</code> is on or before the current minimum date for 
618         * the axis, the minimum date will be shifted to preserve the current
619         * length of the axis.
620         *
621         * @param maximumDate  the date (<code>null</code> not permitted).
622         * 
623         * @see #getMinimumDate()
624         * @see #setMinimumDate(Date)
625         */
626        public void setMaximumDate(Date maximumDate) {
627            if (maximumDate == null) {
628                throw new IllegalArgumentException("Null 'maximumDate' argument.");
629            }
630            // check the new maximum date relative to the current minimum date
631            Date minDate = getMinimumDate();
632            long minMillis = minDate.getTime();
633            long newMaxMillis = maximumDate.getTime();
634            if (minMillis >= newMaxMillis) {
635                Date oldMax = getMaximumDate();
636                long length = oldMax.getTime() - minMillis;
637                minDate = new Date(newMaxMillis - length);
638            }
639            setRange(new DateRange(minDate, maximumDate), true, false);
640            notifyListeners(new AxisChangeEvent(this));
641        }
642    
643        /**
644         * Returns the tick mark position (start, middle or end of the time period).
645         *
646         * @return The position (never <code>null</code>).
647         */
648        public DateTickMarkPosition getTickMarkPosition() {
649            return this.tickMarkPosition;
650        }
651    
652        /**
653         * Sets the tick mark position (start, middle or end of the time period) 
654         * and sends an {@link AxisChangeEvent} to all registered listeners.
655         *
656         * @param position  the position (<code>null</code> not permitted).
657         */
658        public void setTickMarkPosition(DateTickMarkPosition position) {
659            if (position == null) {
660                throw new IllegalArgumentException("Null 'position' argument.");
661            }
662            this.tickMarkPosition = position;
663            notifyListeners(new AxisChangeEvent(this));
664        }
665    
666        /**
667         * Configures the axis to work with the specified plot.  If the axis has
668         * auto-scaling, then sets the maximum and minimum values.
669         */
670        public void configure() {
671            if (isAutoRange()) {
672                autoAdjustRange();
673            }
674        }
675    
676        /**
677         * Returns <code>true</code> if the axis hides this value, and 
678         * <code>false</code> otherwise.
679         *
680         * @param millis  the data value.
681         *
682         * @return A value.
683         */
684        public boolean isHiddenValue(long millis) {
685            return (!this.timeline.containsDomainValue(new Date(millis)));
686        }
687    
688        /**
689         * Translates the data value to the display coordinates (Java 2D User Space)
690         * of the chart.
691         *
692         * @param value  the date to be plotted.
693         * @param area  the rectangle (in Java2D space) where the data is to be 
694         *              plotted.
695         * @param edge  the axis location.
696         *
697         * @return The coordinate corresponding to the supplied data value.
698         */
699        public double valueToJava2D(double value, Rectangle2D area, 
700                                    RectangleEdge edge) {
701            
702            value = this.timeline.toTimelineValue((long) value);
703    
704            DateRange range = (DateRange) getRange();
705            double axisMin = this.timeline.toTimelineValue(range.getLowerDate());
706            double axisMax = this.timeline.toTimelineValue(range.getUpperDate());
707            double result = 0.0;
708            if (RectangleEdge.isTopOrBottom(edge)) {
709                double minX = area.getX();
710                double maxX = area.getMaxX();
711                if (isInverted()) {
712                    result = maxX + ((value - axisMin) / (axisMax - axisMin)) 
713                             * (minX - maxX);
714                }
715                else {
716                    result = minX + ((value - axisMin) / (axisMax - axisMin)) 
717                             * (maxX - minX);
718                }
719            }
720            else if (RectangleEdge.isLeftOrRight(edge)) {
721                double minY = area.getMinY();
722                double maxY = area.getMaxY();
723                if (isInverted()) {
724                    result = minY + (((value - axisMin) / (axisMax - axisMin)) 
725                             * (maxY - minY));
726                }
727                else {
728                    result = maxY - (((value - axisMin) / (axisMax - axisMin)) 
729                             * (maxY - minY));
730                }
731            }
732            return result;
733    
734        }
735    
736        /**
737         * Translates a date to Java2D coordinates, based on the range displayed by
738         * this axis for the specified data area.
739         *
740         * @param date  the date.
741         * @param area  the rectangle (in Java2D space) where the data is to be
742         *              plotted.
743         * @param edge  the axis location.
744         *
745         * @return The coordinate corresponding to the supplied date.
746         */
747        public double dateToJava2D(Date date, Rectangle2D area, 
748                                   RectangleEdge edge) {  
749            double value = date.getTime();
750            return valueToJava2D(value, area, edge);
751        }
752    
753        /**
754         * Translates a Java2D coordinate into the corresponding data value.  To 
755         * perform this translation, you need to know the area used for plotting 
756         * data, and which edge the axis is located on.
757         *
758         * @param java2DValue  the coordinate in Java2D space.
759         * @param area  the rectangle (in Java2D space) where the data is to be 
760         *              plotted.
761         * @param edge  the axis location.
762         *
763         * @return A data value.
764         */
765        public double java2DToValue(double java2DValue, Rectangle2D area, 
766                                    RectangleEdge edge) {
767            
768            DateRange range = (DateRange) getRange();
769            double axisMin = this.timeline.toTimelineValue(range.getLowerDate());
770            double axisMax = this.timeline.toTimelineValue(range.getUpperDate());
771    
772            double min = 0.0;
773            double max = 0.0;
774            if (RectangleEdge.isTopOrBottom(edge)) {
775                min = area.getX();
776                max = area.getMaxX();
777            }
778            else if (RectangleEdge.isLeftOrRight(edge)) {
779                min = area.getMaxY();
780                max = area.getY();
781            }
782    
783            double result;
784            if (isInverted()) {
785                 result = axisMax - ((java2DValue - min) / (max - min) 
786                          * (axisMax - axisMin));
787            }
788            else {
789                 result = axisMin + ((java2DValue - min) / (max - min) 
790                          * (axisMax - axisMin));
791            }
792    
793            return this.timeline.toMillisecond((long) result); 
794        }
795    
796        /**
797         * Calculates the value of the lowest visible tick on the axis.
798         *
799         * @param unit  date unit to use.
800         *
801         * @return The value of the lowest visible tick on the axis.
802         */
803        public Date calculateLowestVisibleTickValue(DateTickUnit unit) {
804            return nextStandardDate(getMinimumDate(), unit);
805        }
806    
807        /**
808         * Calculates the value of the highest visible tick on the axis.
809         *
810         * @param unit  date unit to use.
811         *
812         * @return The value of the highest visible tick on the axis.
813         */
814        public Date calculateHighestVisibleTickValue(DateTickUnit unit) {
815            return previousStandardDate(getMaximumDate(), unit);
816        }
817        
818        /**
819         * Returns the previous "standard" date, for a given date and tick unit.
820         *
821         * @param date  the reference date.
822         * @param unit  the tick unit.
823         *
824         * @return The previous "standard" date.
825         */
826        protected Date previousStandardDate(Date date, DateTickUnit unit) {
827    
828            int milliseconds;
829            int seconds;
830            int minutes;
831            int hours;
832            int days;
833            int months;
834            int years;
835    
836            Calendar calendar = Calendar.getInstance(this.timeZone);
837            calendar.setTime(date);
838            int count = unit.getCount();
839            int current = calendar.get(unit.getCalendarField());
840            int value = count * (current / count);
841    
842            switch (unit.getUnit()) {
843    
844                case (DateTickUnit.MILLISECOND) :
845                    years = calendar.get(Calendar.YEAR);
846                    months = calendar.get(Calendar.MONTH);
847                    days = calendar.get(Calendar.DATE);
848                    hours = calendar.get(Calendar.HOUR_OF_DAY);
849                    minutes = calendar.get(Calendar.MINUTE);
850                    seconds = calendar.get(Calendar.SECOND);
851                    calendar.set(years, months, days, hours, minutes, seconds);
852                    calendar.set(Calendar.MILLISECOND, value);
853                    Date mm = calendar.getTime();
854                    if (mm.getTime() >= date.getTime()) {
855                        calendar.set(Calendar.MILLISECOND, value - 1);
856                        mm = calendar.getTime();
857                    }
858                    return calendar.getTime();
859    
860                case (DateTickUnit.SECOND) :
861                    years = calendar.get(Calendar.YEAR);
862                    months = calendar.get(Calendar.MONTH);
863                    days = calendar.get(Calendar.DATE);
864                    hours = calendar.get(Calendar.HOUR_OF_DAY);
865                    minutes = calendar.get(Calendar.MINUTE);
866                    if (this.tickMarkPosition == DateTickMarkPosition.START) {
867                        milliseconds = 0;
868                    }
869                    else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
870                        milliseconds = 500;
871                    }
872                    else {
873                        milliseconds = 999;
874                    }
875                    calendar.set(Calendar.MILLISECOND, milliseconds);
876                    calendar.set(years, months, days, hours, minutes, value);
877                    Date dd = calendar.getTime();
878                    if (dd.getTime() >= date.getTime()) {
879                        calendar.set(Calendar.SECOND, value - 1);
880                        dd = calendar.getTime();
881                    }
882                    return calendar.getTime();
883    
884                case (DateTickUnit.MINUTE) :
885                    years = calendar.get(Calendar.YEAR);
886                    months = calendar.get(Calendar.MONTH);
887                    days = calendar.get(Calendar.DATE);
888                    hours = calendar.get(Calendar.HOUR_OF_DAY);
889                    if (this.tickMarkPosition == DateTickMarkPosition.START) {
890                        seconds = 0;
891                    }
892                    else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
893                        seconds = 30;
894                    }
895                    else {
896                        seconds = 59;
897                    }
898                    calendar.clear(Calendar.MILLISECOND);
899                    calendar.set(years, months, days, hours, value, seconds);
900                    Date d0 = calendar.getTime();
901                    if (d0.getTime() >= date.getTime()) {
902                        calendar.set(Calendar.MINUTE, value - 1);
903                        d0 = calendar.getTime();
904                    }
905                    return d0;
906    
907                case (DateTickUnit.HOUR) :
908                    years = calendar.get(Calendar.YEAR);
909                    months = calendar.get(Calendar.MONTH);
910                    days = calendar.get(Calendar.DATE);
911                    if (this.tickMarkPosition == DateTickMarkPosition.START) {
912                        minutes = 0;
913                        seconds = 0;
914                    }
915                    else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
916                        minutes = 30;
917                        seconds = 0;
918                    }
919                    else {
920                        minutes = 59;
921                        seconds = 59;
922                    }
923                    calendar.clear(Calendar.MILLISECOND);
924                    calendar.set(years, months, days, value, minutes, seconds);
925                    Date d1 = calendar.getTime();
926                    if (d1.getTime() >= date.getTime()) {
927                        calendar.set(Calendar.HOUR_OF_DAY, value - 1);
928                        d1 = calendar.getTime();
929                    }
930                    return d1;
931    
932                case (DateTickUnit.DAY) :
933                    years = calendar.get(Calendar.YEAR);
934                    months = calendar.get(Calendar.MONTH);
935                    if (this.tickMarkPosition == DateTickMarkPosition.START) {
936                        hours = 0;
937                        minutes = 0;
938                        seconds = 0;
939                    }
940                    else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
941                        hours = 12;
942                        minutes = 0;
943                        seconds = 0;
944                    }
945                    else {
946                        hours = 23;
947                        minutes = 59;
948                        seconds = 59;
949                    }
950                    calendar.clear(Calendar.MILLISECOND);
951                    calendar.set(years, months, value, hours, 0, 0);
952                    // long result = calendar.getTimeInMillis();  
953                        // won't work with JDK 1.3
954                    Date d2 = calendar.getTime();
955                    if (d2.getTime() >= date.getTime()) {
956                        calendar.set(Calendar.DATE, value - 1);
957                        d2 = calendar.getTime();
958                    }
959                    return d2;
960    
961                case (DateTickUnit.MONTH) :
962                    years = calendar.get(Calendar.YEAR);
963                    calendar.clear(Calendar.MILLISECOND);
964                    calendar.set(years, value, 1, 0, 0, 0);
965                    Month month = new Month(calendar.getTime(), this.timeZone);
966                    Date standardDate = calculateDateForPosition(
967                            month, this.tickMarkPosition);
968                    long millis = standardDate.getTime();
969                    if (millis >= date.getTime()) {
970                        month = (Month) month.previous();
971                        standardDate = calculateDateForPosition(
972                                month, this.tickMarkPosition);
973                    }
974                    return standardDate;
975    
976                case(DateTickUnit.YEAR) :
977                    if (this.tickMarkPosition == DateTickMarkPosition.START) {
978                        months = 0;
979                        days = 1;
980                    }
981                    else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) {
982                        months = 6;
983                        days = 1;
984                    }
985                    else {
986                        months = 11;
987                        days = 31;
988                    }
989                    calendar.clear(Calendar.MILLISECOND);
990                    calendar.set(value, months, days, 0, 0, 0);
991                    Date d3 = calendar.getTime();
992                    if (d3.getTime() >= date.getTime()) {
993                        calendar.set(Calendar.YEAR, value - 1);
994                        d3 = calendar.getTime();
995                    }
996                    return d3;
997    
998                default: return null;
999    
1000            }
1001    
1002        }
1003    
1004        /**
1005         * Returns a {@link java.util.Date} corresponding to the specified position
1006         * within a {@link RegularTimePeriod}.
1007         *
1008         * @param period  the period.
1009         * @param position  the position (<code>null</code> not permitted).
1010         *
1011         * @return A date.
1012         */
1013        private Date calculateDateForPosition(RegularTimePeriod period, 
1014                                              DateTickMarkPosition position) {
1015            
1016            if (position == null) {
1017                throw new IllegalArgumentException("Null 'position' argument.");   
1018            }
1019            Date result = null;
1020            if (position == DateTickMarkPosition.START) {
1021                result = new Date(period.getFirstMillisecond());
1022            }
1023            else if (position == DateTickMarkPosition.MIDDLE) {
1024                result = new Date(period.getMiddleMillisecond());
1025            }
1026            else if (position == DateTickMarkPosition.END) {
1027                result = new Date(period.getLastMillisecond());
1028            }
1029            return result;
1030    
1031        }
1032    
1033        /**
1034         * Returns the first "standard" date (based on the specified field and 
1035         * units).
1036         *
1037         * @param date  the reference date.
1038         * @param unit  the date tick unit.
1039         *
1040         * @return The next "standard" date.
1041         */
1042        protected Date nextStandardDate(Date date, DateTickUnit unit) {
1043            Date previous = previousStandardDate(date, unit);
1044            Calendar calendar = Calendar.getInstance(this.timeZone);
1045            calendar.setTime(previous);
1046            calendar.add(unit.getCalendarField(), unit.getCount());
1047            return calendar.getTime();
1048        }
1049    
1050        /**
1051         * Returns a collection of standard date tick units that uses the default 
1052         * time zone.  This collection will be used by default, but you are free 
1053         * to create your own collection if you want to (see the 
1054         * {@link ValueAxis#setStandardTickUnits(TickUnitSource)} method inherited 
1055         * from the {@link ValueAxis} class).
1056         *
1057         * @return A collection of standard date tick units.
1058         */
1059        public static TickUnitSource createStandardDateTickUnits() {
1060            return createStandardDateTickUnits(TimeZone.getDefault());
1061        }
1062    
1063        /**
1064         * Returns a collection of standard date tick units.  This collection will 
1065         * be used by default, but you are free to create your own collection if 
1066         * you want to (see the 
1067         * {@link ValueAxis#setStandardTickUnits(TickUnitSource)} method inherited 
1068         * from the {@link ValueAxis} class).
1069         *
1070         * @param zone  the time zone (<code>null</code> not permitted).
1071         * 
1072         * @return A collection of standard date tick units.
1073         */
1074        public static TickUnitSource createStandardDateTickUnits(TimeZone zone) {
1075    
1076            if (zone == null) {
1077                throw new IllegalArgumentException("Null 'zone' argument.");
1078            }
1079            TickUnits units = new TickUnits();
1080    
1081            // date formatters
1082            DateFormat f1 = new SimpleDateFormat("HH:mm:ss.SSS");
1083            DateFormat f2 = new SimpleDateFormat("HH:mm:ss");
1084            DateFormat f3 = new SimpleDateFormat("HH:mm");
1085            DateFormat f4 = new SimpleDateFormat("d-MMM, HH:mm");
1086            DateFormat f5 = new SimpleDateFormat("d-MMM");
1087            DateFormat f6 = new SimpleDateFormat("MMM-yyyy");
1088            DateFormat f7 = new SimpleDateFormat("yyyy");
1089            
1090            f1.setTimeZone(zone);
1091            f2.setTimeZone(zone);
1092            f3.setTimeZone(zone);
1093            f4.setTimeZone(zone);
1094            f5.setTimeZone(zone);
1095            f6.setTimeZone(zone);
1096            f7.setTimeZone(zone);
1097            
1098            // milliseconds
1099            units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 1, f1));
1100            units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 5, 
1101                    DateTickUnit.MILLISECOND, 1, f1));
1102            units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 10, 
1103                    DateTickUnit.MILLISECOND, 1, f1));
1104            units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 25, 
1105                    DateTickUnit.MILLISECOND, 5, f1));
1106            units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 50, 
1107                    DateTickUnit.MILLISECOND, 10, f1));
1108            units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 100, 
1109                    DateTickUnit.MILLISECOND, 10, f1));
1110            units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 250, 
1111                    DateTickUnit.MILLISECOND, 10, f1));
1112            units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 500, 
1113                    DateTickUnit.MILLISECOND, 50, f1));
1114    
1115            // seconds
1116            units.add(new DateTickUnit(DateTickUnit.SECOND, 1, 
1117                    DateTickUnit.MILLISECOND, 50, f2));
1118            units.add(new DateTickUnit(DateTickUnit.SECOND, 5, 
1119                    DateTickUnit.SECOND, 1, f2));
1120            units.add(new DateTickUnit(DateTickUnit.SECOND, 10, 
1121                    DateTickUnit.SECOND, 1, f2));
1122            units.add(new DateTickUnit(DateTickUnit.SECOND, 30, 
1123                    DateTickUnit.SECOND, 5, f2));
1124    
1125            // minutes
1126            units.add(new DateTickUnit(DateTickUnit.MINUTE, 1, 
1127                    DateTickUnit.SECOND, 5, f3));
1128            units.add(new DateTickUnit(DateTickUnit.MINUTE, 2, 
1129                    DateTickUnit.SECOND, 10, f3));
1130            units.add(new DateTickUnit(DateTickUnit.MINUTE, 5, 
1131                    DateTickUnit.MINUTE, 1, f3));
1132            units.add(new DateTickUnit(DateTickUnit.MINUTE, 10, 
1133                    DateTickUnit.MINUTE, 1, f3));
1134            units.add(new DateTickUnit(DateTickUnit.MINUTE, 15, 
1135                    DateTickUnit.MINUTE, 5, f3));
1136            units.add(new DateTickUnit(DateTickUnit.MINUTE, 20, 
1137                    DateTickUnit.MINUTE, 5, f3));
1138            units.add(new DateTickUnit(DateTickUnit.MINUTE, 30, 
1139                    DateTickUnit.MINUTE, 5, f3));
1140    
1141            // hours
1142            units.add(new DateTickUnit(DateTickUnit.HOUR, 1, 
1143                    DateTickUnit.MINUTE, 5, f3));
1144            units.add(new DateTickUnit(DateTickUnit.HOUR, 2, 
1145                    DateTickUnit.MINUTE, 10, f3));
1146            units.add(new DateTickUnit(DateTickUnit.HOUR, 4, 
1147                    DateTickUnit.MINUTE, 30, f3));
1148            units.add(new DateTickUnit(DateTickUnit.HOUR, 6, 
1149                    DateTickUnit.HOUR, 1, f3));
1150            units.add(new DateTickUnit(DateTickUnit.HOUR, 12, 
1151                    DateTickUnit.HOUR, 1, f4));
1152    
1153            // days
1154            units.add(new DateTickUnit(DateTickUnit.DAY, 1, 
1155                    DateTickUnit.HOUR, 1, f5));
1156            units.add(new DateTickUnit(DateTickUnit.DAY, 2, 
1157                    DateTickUnit.HOUR, 1, f5));
1158            units.add(new DateTickUnit(DateTickUnit.DAY, 7, 
1159                    DateTickUnit.DAY, 1, f5));
1160            units.add(new DateTickUnit(DateTickUnit.DAY, 15, 
1161                    DateTickUnit.DAY, 1, f5));
1162    
1163            // months
1164            units.add(new DateTickUnit(DateTickUnit.MONTH, 1, 
1165                    DateTickUnit.DAY, 1, f6));
1166            units.add(new DateTickUnit(DateTickUnit.MONTH, 2, 
1167                    DateTickUnit.DAY, 1, f6));
1168            units.add(new DateTickUnit(DateTickUnit.MONTH, 3, 
1169                    DateTickUnit.MONTH, 1, f6));
1170            units.add(new DateTickUnit(DateTickUnit.MONTH, 4,  
1171                    DateTickUnit.MONTH, 1, f6));
1172            units.add(new DateTickUnit(DateTickUnit.MONTH, 6,  
1173                    DateTickUnit.MONTH, 1, f6));
1174    
1175            // years
1176            units.add(new DateTickUnit(DateTickUnit.YEAR, 1,  
1177                    DateTickUnit.MONTH, 1, f7));
1178            units.add(new DateTickUnit(DateTickUnit.YEAR, 2,  
1179                    DateTickUnit.MONTH, 3, f7));
1180            units.add(new DateTickUnit(DateTickUnit.YEAR, 5,  
1181                    DateTickUnit.YEAR, 1, f7));
1182            units.add(new DateTickUnit(DateTickUnit.YEAR, 10,  
1183                    DateTickUnit.YEAR, 1, f7));
1184            units.add(new DateTickUnit(DateTickUnit.YEAR, 25, 
1185                    DateTickUnit.YEAR, 5, f7));
1186            units.add(new DateTickUnit(DateTickUnit.YEAR, 50, 
1187                    DateTickUnit.YEAR, 10, f7));
1188            units.add(new DateTickUnit(DateTickUnit.YEAR, 100, 
1189                    DateTickUnit.YEAR, 20, f7));
1190    
1191            return units;
1192    
1193        }
1194    
1195        /**
1196         * Rescales the axis to ensure that all data is visible.
1197         */
1198        protected void autoAdjustRange() {
1199    
1200            Plot plot = getPlot();
1201    
1202            if (plot == null) {
1203                return;  // no plot, no data
1204            }
1205    
1206            if (plot instanceof ValueAxisPlot) {
1207                ValueAxisPlot vap = (ValueAxisPlot) plot;
1208    
1209                Range r = vap.getDataRange(this);
1210                if (r == null) {
1211                    if (this.timeline instanceof SegmentedTimeline) { 
1212                        //Timeline hasn't method getStartTime()
1213                        r = new DateRange((
1214                                (SegmentedTimeline) this.timeline).getStartTime(),
1215                                ((SegmentedTimeline) this.timeline).getStartTime() 
1216                                + 1);
1217                    } 
1218                    else {
1219                        r = new DateRange();
1220                    }
1221                }
1222    
1223                long upper = this.timeline.toTimelineValue(
1224                        (long) r.getUpperBound());
1225                long lower;
1226                long fixedAutoRange = (long) getFixedAutoRange();
1227                if (fixedAutoRange > 0.0) {
1228                    lower = upper - fixedAutoRange;
1229                }
1230                else {
1231                    lower = this.timeline.toTimelineValue((long) r.getLowerBound());
1232                    double range = upper - lower;
1233                    long minRange = (long) getAutoRangeMinimumSize();
1234                    if (range < minRange) {
1235                        long expand = (long) (minRange - range) / 2;
1236                        upper = upper + expand;
1237                        lower = lower - expand;
1238                    }
1239                    upper = upper + (long) (range * getUpperMargin());
1240                    lower = lower - (long) (range * getLowerMargin());
1241                }
1242    
1243                upper = this.timeline.toMillisecond(upper);
1244                lower = this.timeline.toMillisecond(lower);
1245                DateRange dr = new DateRange(new Date(lower), new Date(upper));
1246                setRange(dr, false, false);
1247            }
1248    
1249        }
1250    
1251        /**
1252         * Selects an appropriate tick value for the axis.  The strategy is to
1253         * display as many ticks as possible (selected from an array of 'standard'
1254         * tick units) without the labels overlapping.
1255         *
1256         * @param g2  the graphics device.
1257         * @param dataArea  the area defined by the axes.
1258         * @param edge  the axis location.
1259         */
1260        protected void selectAutoTickUnit(Graphics2D g2, 
1261                                          Rectangle2D dataArea,
1262                                          RectangleEdge edge) {
1263    
1264            if (RectangleEdge.isTopOrBottom(edge)) {
1265                selectHorizontalAutoTickUnit(g2, dataArea, edge);
1266            }
1267            else if (RectangleEdge.isLeftOrRight(edge)) {
1268                selectVerticalAutoTickUnit(g2, dataArea, edge);
1269            }
1270    
1271        }
1272    
1273        /**
1274         * Selects an appropriate tick size for the axis.  The strategy is to
1275         * display as many ticks as possible (selected from a collection of 
1276         * 'standard' tick units) without the labels overlapping.
1277         *
1278         * @param g2  the graphics device.
1279         * @param dataArea  the area defined by the axes.
1280         * @param edge  the axis location.
1281         */
1282        protected void selectHorizontalAutoTickUnit(Graphics2D g2, 
1283                                                    Rectangle2D dataArea, 
1284                                                    RectangleEdge edge) {
1285    
1286            long shift = 0;
1287            if (this.timeline instanceof SegmentedTimeline) {
1288                shift = ((SegmentedTimeline) this.timeline).getStartTime();
1289            }
1290            double zero = valueToJava2D(shift + 0.0, dataArea, edge);
1291            double tickLabelWidth 
1292                = estimateMaximumTickLabelWidth(g2, getTickUnit());
1293    
1294            // start with the current tick unit...
1295            TickUnitSource tickUnits = getStandardTickUnits();
1296            TickUnit unit1 = tickUnits.getCeilingTickUnit(getTickUnit());
1297            double x1 = valueToJava2D(shift + unit1.getSize(), dataArea, edge);
1298            double unit1Width = Math.abs(x1 - zero);
1299    
1300            // then extrapolate...
1301            double guess = (tickLabelWidth / unit1Width) * unit1.getSize();
1302            DateTickUnit unit2 = (DateTickUnit) tickUnits.getCeilingTickUnit(guess);
1303            double x2 = valueToJava2D(shift + unit2.getSize(), dataArea, edge);
1304            double unit2Width = Math.abs(x2 - zero);
1305            tickLabelWidth = estimateMaximumTickLabelWidth(g2, unit2);
1306            if (tickLabelWidth > unit2Width) {
1307                unit2 = (DateTickUnit) tickUnits.getLargerTickUnit(unit2);
1308            }
1309            setTickUnit(unit2, false, false);
1310        }
1311        
1312        /**
1313         * Selects an appropriate tick size for the axis.  The strategy is to
1314         * display as many ticks as possible (selected from a collection of 
1315         * 'standard' tick units) without the labels overlapping.
1316         *
1317         * @param g2  the graphics device.
1318         * @param dataArea  the area in which the plot should be drawn.
1319         * @param edge  the axis location.
1320         */
1321        protected void selectVerticalAutoTickUnit(Graphics2D g2,
1322                                                  Rectangle2D dataArea,
1323                                                  RectangleEdge edge) {
1324    
1325            // start with the current tick unit...
1326            TickUnitSource tickUnits = getStandardTickUnits();
1327            double zero = valueToJava2D(0.0, dataArea, edge);
1328    
1329            // start with a unit that is at least 1/10th of the axis length
1330            double estimate1 = getRange().getLength() / 10.0;
1331            DateTickUnit candidate1 
1332                = (DateTickUnit) tickUnits.getCeilingTickUnit(estimate1);
1333            double labelHeight1 = estimateMaximumTickLabelHeight(g2, candidate1);
1334            double y1 = valueToJava2D(candidate1.getSize(), dataArea, edge);
1335            double candidate1UnitHeight = Math.abs(y1 - zero);
1336    
1337            // now extrapolate based on label height and unit height...
1338            double estimate2 
1339                = (labelHeight1 / candidate1UnitHeight) * candidate1.getSize();
1340            DateTickUnit candidate2 
1341                = (DateTickUnit) tickUnits.getCeilingTickUnit(estimate2);
1342            double labelHeight2 = estimateMaximumTickLabelHeight(g2, candidate2);
1343            double y2 = valueToJava2D(candidate2.getSize(), dataArea, edge);
1344            double unit2Height = Math.abs(y2 - zero);
1345    
1346           // make final selection...
1347           DateTickUnit finalUnit;
1348           if (labelHeight2 < unit2Height) {
1349               finalUnit = candidate2;
1350           }
1351           else {
1352               finalUnit = (DateTickUnit) tickUnits.getLargerTickUnit(candidate2);
1353           }
1354           setTickUnit(finalUnit, false, false);
1355    
1356        }
1357    
1358        /**
1359         * Estimates the maximum width of the tick labels, assuming the specified 
1360         * tick unit is used.
1361         * <P>
1362         * Rather than computing the string bounds of every tick on the axis, we
1363         * just look at two values: the lower bound and the upper bound for the 
1364         * axis.  These two values will usually be representative.
1365         *
1366         * @param g2  the graphics device.
1367         * @param unit  the tick unit to use for calculation.
1368         *
1369         * @return The estimated maximum width of the tick labels.
1370         */
1371        private double estimateMaximumTickLabelWidth(Graphics2D g2, 
1372                                                     DateTickUnit unit) {
1373    
1374            RectangleInsets tickLabelInsets = getTickLabelInsets();
1375            double result = tickLabelInsets.getLeft() + tickLabelInsets.getRight();
1376    
1377            Font tickLabelFont = getTickLabelFont();
1378            FontRenderContext frc = g2.getFontRenderContext();
1379            LineMetrics lm = tic