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