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