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