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