    001/* ===========================================================
    002 * JFreeChart : a free chart library for the Java(tm) platform
    003 * ===========================================================
    004 *
    005 * (C) Copyright 2000-2011, by Object Refinery Limited and Contributors.
    006 *
    007 * Project Info:  http://www.jfree.org/jfreechart/index.html
    008 *
    009 * This library is free software; you can redistribute it and/or modify it
    010 * under the terms of the GNU Lesser General Public License as published by
    011 * the Free Software Foundation; either version 2.1 of the License, or
    012 * (at your option) any later version.
    013 *
    014 * This library is distributed in the hope that it will be useful, but
    015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
    016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
    017 * License for more details.
    018 *
    019 * You should have received a copy of the GNU Lesser General Public
    020 * License along with this library; if not, write to the Free Software
    021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
    022 * USA.
    023 *
    024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates.
    025 * Other names may be trademarks of their respective owners.]
    026 *
    027 * -------------
    028 * DateAxis.java
    029 * -------------
    030 * (C) Copyright 2000-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
    131package org.jfree.chart.axis;
    132
    133import java.awt.Font;
    134import java.awt.FontMetrics;
    135import java.awt.Graphics2D;
    136import java.awt.font.FontRenderContext;
    137import java.awt.font.LineMetrics;
    138import java.awt.geom.Rectangle2D;
    139import java.io.Serializable;
    140import java.text.DateFormat;
    141import java.text.SimpleDateFormat;
    142import java.util.Calendar;
    143import java.util.Date;
    144import java.util.List;
    145import java.util.Locale;
    146import java.util.TimeZone;
    147
    148import org.jfree.chart.event.AxisChangeEvent;
    149import org.jfree.chart.plot.Plot;
    150import org.jfree.chart.plot.PlotRenderingInfo;
    151import org.jfree.chart.plot.ValueAxisPlot;
    152import org.jfree.data.Range;
    153import org.jfree.data.time.DateRange;
    154import org.jfree.data.time.Month;
    155import org.jfree.data.time.RegularTimePeriod;
    156import org.jfree.data.time.Year;
    157import org.jfree.ui.RectangleEdge;
    158import org.jfree.ui.RectangleInsets;
    159import org.jfree.ui.TextAnchor;
    160import 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 */
    175public 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}