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