0001: /* ===========================================================
0002: * JFreeChart : a free chart library for the Java(tm) platform
0003: * ===========================================================
0004: *
0005: * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
0006: *
0007: * Project Info: http://www.jfree.org/jfreechart/index.html
0008: *
0009: * This library is free software; you can redistribute it and/or modify it
0010: * under the terms of the GNU Lesser General Public License as published by
0011: * the Free Software Foundation; either version 2.1 of the License, or
0012: * (at your option) any later version.
0013: *
0014: * This library is distributed in the hope that it will be useful, but
0015: * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
0016: * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
0017: * License for more details.
0018: *
0019: * You should have received a copy of the GNU Lesser General Public
0020: * License along with this library; if not, write to the Free Software
0021: * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
0022: * USA.
0023: *
0024: * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
0025: * in the United States and other countries.]
0026: *
0027: * -------------
0028: * DateAxis.java
0029: * -------------
0030: * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
0031: *
0032: * Original Author: David Gilbert;
0033: * Contributor(s): Jonathan Nash;
0034: * David Li;
0035: * Michael Rauch;
0036: * Bill Kelemen;
0037: * Pawel Pabis;
0038: * Chris Boek;
0039: *
0040: * $Id: DateAxis.java,v 1.17.2.11 2007/05/03 14:27:11 mungady Exp $
0041: *
0042: * Changes (from 23-Jun-2001)
0043: * --------------------------
0044: * 23-Jun-2001 : Modified to work with null data source (DG);
0045: * 18-Sep-2001 : Updated header (DG);
0046: * 27-Nov-2001 : Changed constructors from public to protected, updated Javadoc
0047: * comments (DG);
0048: * 16-Jan-2002 : Added an optional crosshair, based on the implementation by
0049: * Jonathan Nash (DG);
0050: * 26-Feb-2002 : Updated import statements (DG);
0051: * 22-Apr-2002 : Added a setRange() method (DG);
0052: * 25-Jun-2002 : Removed redundant local variable (DG);
0053: * 25-Jul-2002 : Changed order of parameters in ValueAxis constructor (DG);
0054: * 21-Aug-2002 : The setTickUnit() method now turns off auto-tick unit
0055: * selection (fix for bug id 528885) (DG);
0056: * 05-Sep-2002 : Updated the constructors to reflect changes in the Axis
0057: * class (DG);
0058: * 18-Sep-2002 : Fixed errors reported by Checkstyle (DG);
0059: * 25-Sep-2002 : Added new setRange() methods, and deprecated
0060: * setAxisRange() (DG);
0061: * 04-Oct-2002 : Changed auto tick selection to parallel number axis
0062: * classes (DG);
0063: * 24-Oct-2002 : Added a date format override (DG);
0064: * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG);
0065: * 14-Jan-2003 : Changed autoRangeMinimumSize from Number --> double, moved
0066: * crosshair settings to the plot (DG);
0067: * 15-Jan-2003 : Removed anchor date (DG);
0068: * 20-Jan-2003 : Removed unnecessary constructors (DG);
0069: * 26-Mar-2003 : Implemented Serializable (DG);
0070: * 02-May-2003 : Added additional units to createStandardDateTickUnits()
0071: * method, as suggested by mhilpert in bug report 723187 (DG);
0072: * 13-May-2003 : Merged HorizontalDateAxis and VerticalDateAxis (DG);
0073: * 24-May-2003 : Added support for underlying timeline for
0074: * SegmentedTimeline (BK);
0075: * 16-Jul-2003 : Applied patch from Pawel Pabis to fix overlapping dates (DG);
0076: * 22-Jul-2003 : Applied patch from Pawel Pabis for monthly ticks (DG);
0077: * 25-Jul-2003 : Fixed bug 777561 and 777586 (DG);
0078: * 13-Aug-2003 : Implemented Cloneable and added equals() method (DG);
0079: * 02-Sep-2003 : Fixes for bug report 790506 (DG);
0080: * 04-Sep-2003 : Fixed tick label alignment when axis appears at the top (DG);
0081: * 10-Sep-2003 : Fixes for segmented timeline (DG);
0082: * 17-Sep-2003 : Fixed a layout bug when multiple domain axes are used (DG);
0083: * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG);
0084: * 07-Nov-2003 : Modified to use new tick classes (DG);
0085: * 12-Nov-2003 : Modified tick labelling to use roll unit from DateTickUnit
0086: * when a calculated tick value is hidden (which can occur in
0087: * segmented date axes) (DG);
0088: * 24-Nov-2003 : Fixed some problems with the auto tick unit selection, and
0089: * fixed bug 846277 (labels missing for inverted axis) (DG);
0090: * 30-Dec-2003 : Fixed bug in refreshTicksHorizontal() when start of time unit
0091: * (ex. 1st of month) was hidden, causing infinite loop (BK);
0092: * 13-Jan-2004 : Fixed bug in previousStandardDate() method (fix by Richard
0093: * Wardle) (DG);
0094: * 21-Jan-2004 : Renamed translateJava2DToValue --> java2DToValue, and
0095: * translateValueToJava2D --> valueToJava2D (DG);
0096: * 12-Mar-2004 : Fixed bug where date format override is ignored for vertical
0097: * axis (DG);
0098: * 16-Mar-2004 : Added plotState to draw() method (DG);
0099: * 07-Apr-2004 : Changed string width calculation (DG);
0100: * 21-Apr-2004 : Fixed bug in estimateMaximumTickLabelWidth() method (bug id
0101: * 939148) (DG);
0102: * 11-Jan-2005 : Removed deprecated methods in preparation for 1.0.0
0103: * release (DG);
0104: * 13-Jan-2005 : Fixed bug (see
0105: * http://www.jfree.org/forum/viewtopic.php?t=11330) (DG);
0106: * 21-Apr-2005 : Replaced Insets with RectangleInsets, removed redundant
0107: * argument from selectAutoTickUnit() (DG);
0108: * ------------- JFREECHART 1.0.x ---------------------------------------------
0109: * 10-Feb-2006 : Added some API doc comments in respect of bug 821046 (DG);
0110: * 19-Apr-2006 : Fixed bug 1472942 in equals() method (DG);
0111: * 25-Sep-2006 : Fixed bug 1564977 missing tick labels (DG);
0112: * 15-Jan-2007 : Added get/setTimeZone() suggested by 'skunk' (DG);
0113: * 18-Jan-2007 : Fixed bug 1638678, time zone for calendar in
0114: * previousStandardDate() (DG);
0115: * 04-Apr-2007 : Use time zone in date calculations (CB);
0116: * 19-Apr-2007 : Fix exceptions in setMinimum/MaximumDate() (DG);
0117: * 03-May-2007 : Fixed minor bugs in previousStandardDate(), with new JUnit
0118: * tests (DG);
0119: *
0120: */
0121:
0122: package org.jfree.chart.axis;
0123:
0124: import java.awt.Font;
0125: import java.awt.FontMetrics;
0126: import java.awt.Graphics2D;
0127: import java.awt.font.FontRenderContext;
0128: import java.awt.font.LineMetrics;
0129: import java.awt.geom.Rectangle2D;
0130: import java.io.Serializable;
0131: import java.text.DateFormat;
0132: import java.text.SimpleDateFormat;
0133: import java.util.Calendar;
0134: import java.util.Date;
0135: import java.util.List;
0136: import java.util.TimeZone;
0137:
0138: import org.jfree.chart.event.AxisChangeEvent;
0139: import org.jfree.chart.plot.Plot;
0140: import org.jfree.chart.plot.PlotRenderingInfo;
0141: import org.jfree.chart.plot.ValueAxisPlot;
0142: import org.jfree.data.Range;
0143: import org.jfree.data.time.DateRange;
0144: import org.jfree.data.time.Month;
0145: import org.jfree.data.time.RegularTimePeriod;
0146: import org.jfree.data.time.Year;
0147: import org.jfree.ui.RectangleEdge;
0148: import org.jfree.ui.RectangleInsets;
0149: import org.jfree.ui.TextAnchor;
0150: import org.jfree.util.ObjectUtilities;
0151:
0152: /**
0153: * The base class for axes that display dates. You will find it easier to
0154: * understand how this axis works if you bear in mind that it really
0155: * displays/measures integer (or long) data, where the integers are
0156: * milliseconds since midnight, 1-Jan-1970. When displaying tick labels, the
0157: * millisecond values are converted back to dates using a
0158: * <code>DateFormat</code> instance.
0159: * <P>
0160: * You can also create a {@link org.jfree.chart.axis.Timeline} and supply in
0161: * the constructor to create an axis that only contains certain domain values.
0162: * For example, this allows you to create a date axis that only contains
0163: * working days.
0164: */
0165: public class DateAxis extends ValueAxis implements Cloneable,
0166: Serializable {
0167:
0168: /** For serialization. */
0169: private static final long serialVersionUID = -1013460999649007604L;
0170:
0171: /** The default axis range. */
0172: public static final DateRange DEFAULT_DATE_RANGE = new DateRange();
0173:
0174: /** The default minimum auto range size. */
0175: public static final double DEFAULT_AUTO_RANGE_MINIMUM_SIZE_IN_MILLISECONDS = 2.0;
0176:
0177: /** The default date tick unit. */
0178: public static final DateTickUnit DEFAULT_DATE_TICK_UNIT = new DateTickUnit(
0179: DateTickUnit.DAY, 1, new SimpleDateFormat());
0180:
0181: /** The default anchor date. */
0182: public static final Date DEFAULT_ANCHOR_DATE = new Date();
0183:
0184: /** The current tick unit. */
0185: private DateTickUnit tickUnit;
0186:
0187: /** The override date format. */
0188: private DateFormat dateFormatOverride;
0189:
0190: /**
0191: * Tick marks can be displayed at the start or the middle of the time
0192: * period.
0193: */
0194: private DateTickMarkPosition tickMarkPosition = DateTickMarkPosition.START;
0195:
0196: /**
0197: * A timeline that includes all milliseconds (as defined by
0198: * <code>java.util.Date</code>) in the real time line.
0199: */
0200: private static class DefaultTimeline implements Timeline,
0201: Serializable {
0202:
0203: /**
0204: * Converts a millisecond into a timeline value.
0205: *
0206: * @param millisecond the millisecond.
0207: *
0208: * @return The timeline value.
0209: */
0210: public long toTimelineValue(long millisecond) {
0211: return millisecond;
0212: }
0213:
0214: /**
0215: * Converts a date into a timeline value.
0216: *
0217: * @param date the domain value.
0218: *
0219: * @return The timeline value.
0220: */
0221: public long toTimelineValue(Date date) {
0222: return date.getTime();
0223: }
0224:
0225: /**
0226: * Converts a timeline value into a millisecond (as encoded by
0227: * <code>java.util.Date</code>).
0228: *
0229: * @param value the value.
0230: *
0231: * @return The millisecond.
0232: */
0233: public long toMillisecond(long value) {
0234: return value;
0235: }
0236:
0237: /**
0238: * Returns <code>true</code> if the timeline includes the specified
0239: * domain value.
0240: *
0241: * @param millisecond the millisecond.
0242: *
0243: * @return <code>true</code>.
0244: */
0245: public boolean containsDomainValue(long millisecond) {
0246: return true;
0247: }
0248:
0249: /**
0250: * Returns <code>true</code> if the timeline includes the specified
0251: * domain value.
0252: *
0253: * @param date the date.
0254: *
0255: * @return <code>true</code>.
0256: */
0257: public boolean containsDomainValue(Date date) {
0258: return true;
0259: }
0260:
0261: /**
0262: * Returns <code>true</code> if the timeline includes the specified
0263: * domain value range.
0264: *
0265: * @param from the start value.
0266: * @param to the end value.
0267: *
0268: * @return <code>true</code>.
0269: */
0270: public boolean containsDomainRange(long from, long to) {
0271: return true;
0272: }
0273:
0274: /**
0275: * Returns <code>true</code> if the timeline includes the specified
0276: * domain value range.
0277: *
0278: * @param from the start date.
0279: * @param to the end date.
0280: *
0281: * @return <code>true</code>.
0282: */
0283: public boolean containsDomainRange(Date from, Date to) {
0284: return true;
0285: }
0286:
0287: /**
0288: * Tests an object for equality with this instance.
0289: *
0290: * @param object the object.
0291: *
0292: * @return A boolean.
0293: */
0294: public boolean equals(Object object) {
0295: if (object == null) {
0296: return false;
0297: }
0298: if (object == this ) {
0299: return true;
0300: }
0301: if (object instanceof DefaultTimeline) {
0302: return true;
0303: }
0304: return false;
0305: }
0306: }
0307:
0308: /** A static default timeline shared by all standard DateAxis */
0309: private static final Timeline DEFAULT_TIMELINE = new DefaultTimeline();
0310:
0311: /** The time zone for the axis. */
0312: private TimeZone timeZone;
0313:
0314: /** Our underlying timeline. */
0315: private Timeline timeline;
0316:
0317: /**
0318: * Creates a date axis with no label.
0319: */
0320: public DateAxis() {
0321: this (null);
0322: }
0323:
0324: /**
0325: * Creates a date axis with the specified label.
0326: *
0327: * @param label the axis label (<code>null</code> permitted).
0328: */
0329: public DateAxis(String label) {
0330: this (label, TimeZone.getDefault());
0331: }
0332:
0333: /**
0334: * Creates a date axis. A timeline is specified for the axis. This allows
0335: * special transformations to occur between a domain of values and the
0336: * values included in the axis.
0337: *
0338: * @see org.jfree.chart.axis.SegmentedTimeline
0339: *
0340: * @param label the axis label (<code>null</code> permitted).
0341: * @param zone the time zone.
0342: */
0343: public DateAxis(String label, TimeZone zone) {
0344: super (label, DateAxis.createStandardDateTickUnits(zone));
0345: setTickUnit(DateAxis.DEFAULT_DATE_TICK_UNIT, false, false);
0346: setAutoRangeMinimumSize(DEFAULT_AUTO_RANGE_MINIMUM_SIZE_IN_MILLISECONDS);
0347: setRange(DEFAULT_DATE_RANGE, false, false);
0348: this .dateFormatOverride = null;
0349: this .timeZone = zone;
0350: this .timeline = DEFAULT_TIMELINE;
0351: }
0352:
0353: /**
0354: * Returns the time zone for the axis.
0355: *
0356: * @return The time zone.
0357: *
0358: * @since 1.0.4
0359: * @see #setTimeZone(TimeZone)
0360: */
0361: public TimeZone getTimeZone() {
0362: return this .timeZone;
0363: }
0364:
0365: /**
0366: * Sets the time zone for the axis and sends an {@link AxisChangeEvent} to
0367: * all registered listeners.
0368: *
0369: * @param zone the time zone (<code>null</code> not permitted).
0370: *
0371: * @since 1.0.4
0372: * @see #getTimeZone()
0373: */
0374: public void setTimeZone(TimeZone zone) {
0375: if (!this .timeZone.equals(zone)) {
0376: this .timeZone = zone;
0377: setStandardTickUnits(createStandardDateTickUnits(zone));
0378: notifyListeners(new AxisChangeEvent(this ));
0379: }
0380: }
0381:
0382: /**
0383: * Returns the underlying timeline used by this axis.
0384: *
0385: * @return The timeline.
0386: */
0387: public Timeline getTimeline() {
0388: return this .timeline;
0389: }
0390:
0391: /**
0392: * Sets the underlying timeline to use for this axis.
0393: * <P>
0394: * If the timeline is changed, an {@link AxisChangeEvent} is sent to all
0395: * registered listeners.
0396: *
0397: * @param timeline the timeline.
0398: */
0399: public void setTimeline(Timeline timeline) {
0400: if (this .timeline != timeline) {
0401: this .timeline = timeline;
0402: notifyListeners(new AxisChangeEvent(this ));
0403: }
0404: }
0405:
0406: /**
0407: * Returns the tick unit for the axis.
0408: * <p>
0409: * Note: if the <code>autoTickUnitSelection</code> flag is
0410: * <code>true</code> the tick unit may be changed while the axis is being
0411: * drawn, so in that case the return value from this method may be
0412: * irrelevant if the method is called before the axis has been drawn.
0413: *
0414: * @return The tick unit (possibly <code>null</code>).
0415: *
0416: * @see #setTickUnit(DateTickUnit)
0417: * @see ValueAxis#isAutoTickUnitSelection()
0418: */
0419: public DateTickUnit getTickUnit() {
0420: return this .tickUnit;
0421: }
0422:
0423: /**
0424: * Sets the tick unit for the axis. The auto-tick-unit-selection flag is
0425: * set to <code>false</code>, and registered listeners are notified that
0426: * the axis has been changed.
0427: *
0428: * @param unit the tick unit.
0429: *
0430: * @see #getTickUnit()
0431: * @see #setTickUnit(DateTickUnit, boolean, boolean)
0432: */
0433: public void setTickUnit(DateTickUnit unit) {
0434: setTickUnit(unit, true, true);
0435: }
0436:
0437: /**
0438: * Sets the tick unit attribute.
0439: *
0440: * @param unit the new tick unit.
0441: * @param notify notify registered listeners?
0442: * @param turnOffAutoSelection turn off auto selection?
0443: *
0444: * @see #getTickUnit()
0445: */
0446: public void setTickUnit(DateTickUnit unit, boolean notify,
0447: boolean turnOffAutoSelection) {
0448:
0449: this .tickUnit = unit;
0450: if (turnOffAutoSelection) {
0451: setAutoTickUnitSelection(false, false);
0452: }
0453: if (notify) {
0454: notifyListeners(new AxisChangeEvent(this ));
0455: }
0456:
0457: }
0458:
0459: /**
0460: * Returns the date format override. If this is non-null, then it will be
0461: * used to format the dates on the axis.
0462: *
0463: * @return The formatter (possibly <code>null</code>).
0464: */
0465: public DateFormat getDateFormatOverride() {
0466: return this .dateFormatOverride;
0467: }
0468:
0469: /**
0470: * Sets the date format override. If this is non-null, then it will be
0471: * used to format the dates on the axis.
0472: *
0473: * @param formatter the date formatter (<code>null</code> permitted).
0474: */
0475: public void setDateFormatOverride(DateFormat formatter) {
0476: this .dateFormatOverride = formatter;
0477: notifyListeners(new AxisChangeEvent(this ));
0478: }
0479:
0480: /**
0481: * Sets the upper and lower bounds for the axis and sends an
0482: * {@link AxisChangeEvent} to all registered listeners. As a side-effect,
0483: * the auto-range flag is set to false.
0484: *
0485: * @param range the new range (<code>null</code> not permitted).
0486: */
0487: public void setRange(Range range) {
0488: setRange(range, true, true);
0489: }
0490:
0491: /**
0492: * Sets the range for the axis, if requested, sends an
0493: * {@link AxisChangeEvent} to all registered listeners. As a side-effect,
0494: * the auto-range flag is set to <code>false</code> (optional).
0495: *
0496: * @param range the range (<code>null</code> not permitted).
0497: * @param turnOffAutoRange a flag that controls whether or not the auto
0498: * range is turned off.
0499: * @param notify a flag that controls whether or not listeners are
0500: * notified.
0501: */
0502: public void setRange(Range range, boolean turnOffAutoRange,
0503: boolean notify) {
0504: if (range == null) {
0505: throw new IllegalArgumentException("Null 'range' argument.");
0506: }
0507: // usually the range will be a DateRange, but if it isn't do a
0508: // conversion...
0509: if (!(range instanceof DateRange)) {
0510: range = new DateRange(range);
0511: }
0512: super .setRange(range, turnOffAutoRange, notify);
0513: }
0514:
0515: /**
0516: * Sets the axis range and sends an {@link AxisChangeEvent} to all
0517: * registered listeners.
0518: *
0519: * @param lower the lower bound for the axis.
0520: * @param upper the upper bound for the axis.
0521: */
0522: public void setRange(Date lower, Date upper) {
0523: if (lower.getTime() >= upper.getTime()) {
0524: throw new IllegalArgumentException(
0525: "Requires 'lower' < 'upper'.");
0526: }
0527: setRange(new DateRange(lower, upper));
0528: }
0529:
0530: /**
0531: * Sets the axis range and sends an {@link AxisChangeEvent} to all
0532: * registered listeners.
0533: *
0534: * @param lower the lower bound for the axis.
0535: * @param upper the upper bound for the axis.
0536: */
0537: public void setRange(double lower, double upper) {
0538: if (lower >= upper) {
0539: throw new IllegalArgumentException(
0540: "Requires 'lower' < 'upper'.");
0541: }
0542: setRange(new DateRange(lower, upper));
0543: }
0544:
0545: /**
0546: * Returns the earliest date visible on the axis.
0547: *
0548: * @return The date.
0549: *
0550: * @see #setMinimumDate(Date)
0551: * @see #getMaximumDate()
0552: */
0553: public Date getMinimumDate() {
0554: Date result = null;
0555: Range range = getRange();
0556: if (range instanceof DateRange) {
0557: DateRange r = (DateRange) range;
0558: result = r.getLowerDate();
0559: } else {
0560: result = new Date((long) range.getLowerBound());
0561: }
0562: return result;
0563: }
0564:
0565: /**
0566: * Sets the minimum date visible on the axis and sends an
0567: * {@link AxisChangeEvent} to all registered listeners. If
0568: * <code>date</code> is on or after the current maximum date for
0569: * the axis, the maximum date will be shifted to preserve the current
0570: * length of the axis.
0571: *
0572: * @param date the date (<code>null</code> not permitted).
0573: *
0574: * @see #getMinimumDate()
0575: * @see #setMaximumDate(Date)
0576: */
0577: public void setMinimumDate(Date date) {
0578: if (date == null) {
0579: throw new IllegalArgumentException("Null 'date' argument.");
0580: }
0581: // check the new minimum date relative to the current maximum date
0582: Date maxDate = getMaximumDate();
0583: long maxMillis = maxDate.getTime();
0584: long newMinMillis = date.getTime();
0585: if (maxMillis <= newMinMillis) {
0586: Date oldMin = getMinimumDate();
0587: long length = maxMillis - oldMin.getTime();
0588: maxDate = new Date(newMinMillis + length);
0589: }
0590: setRange(new DateRange(date, maxDate), true, false);
0591: notifyListeners(new AxisChangeEvent(this ));
0592: }
0593:
0594: /**
0595: * Returns the latest date visible on the axis.
0596: *
0597: * @return The date.
0598: *
0599: * @see #setMaximumDate(Date)
0600: * @see #getMinimumDate()
0601: */
0602: public Date getMaximumDate() {
0603: Date result = null;
0604: Range range = getRange();
0605: if (range instanceof DateRange) {
0606: DateRange r = (DateRange) range;
0607: result = r.getUpperDate();
0608: } else {
0609: result = new Date((long) range.getUpperBound());
0610: }
0611: return result;
0612: }
0613:
0614: /**
0615: * Sets the maximum date visible on the axis and sends an
0616: * {@link AxisChangeEvent} to all registered listeners. If
0617: * <code>maximumDate</code> is on or before the current minimum date for
0618: * the axis, the minimum date will be shifted to preserve the current
0619: * length of the axis.
0620: *
0621: * @param maximumDate the date (<code>null</code> not permitted).
0622: *
0623: * @see #getMinimumDate()
0624: * @see #setMinimumDate(Date)
0625: */
0626: public void setMaximumDate(Date maximumDate) {
0627: if (maximumDate == null) {
0628: throw new IllegalArgumentException(
0629: "Null 'maximumDate' argument.");
0630: }
0631: // check the new maximum date relative to the current minimum date
0632: Date minDate = getMinimumDate();
0633: long minMillis = minDate.getTime();
0634: long newMaxMillis = maximumDate.getTime();
0635: if (minMillis >= newMaxMillis) {
0636: Date oldMax = getMaximumDate();
0637: long length = oldMax.getTime() - minMillis;
0638: minDate = new Date(newMaxMillis - length);
0639: }
0640: setRange(new DateRange(minDate, maximumDate), true, false);
0641: notifyListeners(new AxisChangeEvent(this ));
0642: }
0643:
0644: /**
0645: * Returns the tick mark position (start, middle or end of the time period).
0646: *
0647: * @return The position (never <code>null</code>).
0648: */
0649: public DateTickMarkPosition getTickMarkPosition() {
0650: return this .tickMarkPosition;
0651: }
0652:
0653: /**
0654: * Sets the tick mark position (start, middle or end of the time period)
0655: * and sends an {@link AxisChangeEvent} to all registered listeners.
0656: *
0657: * @param position the position (<code>null</code> not permitted).
0658: */
0659: public void setTickMarkPosition(DateTickMarkPosition position) {
0660: if (position == null) {
0661: throw new IllegalArgumentException(
0662: "Null 'position' argument.");
0663: }
0664: this .tickMarkPosition = position;
0665: notifyListeners(new AxisChangeEvent(this ));
0666: }
0667:
0668: /**
0669: * Configures the axis to work with the specified plot. If the axis has
0670: * auto-scaling, then sets the maximum and minimum values.
0671: */
0672: public void configure() {
0673: if (isAutoRange()) {
0674: autoAdjustRange();
0675: }
0676: }
0677:
0678: /**
0679: * Returns <code>true</code> if the axis hides this value, and
0680: * <code>false</code> otherwise.
0681: *
0682: * @param millis the data value.
0683: *
0684: * @return A value.
0685: */
0686: public boolean isHiddenValue(long millis) {
0687: return (!this .timeline.containsDomainValue(new Date(millis)));
0688: }
0689:
0690: /**
0691: * Translates the data value to the display coordinates (Java 2D User Space)
0692: * of the chart.
0693: *
0694: * @param value the date to be plotted.
0695: * @param area the rectangle (in Java2D space) where the data is to be
0696: * plotted.
0697: * @param edge the axis location.
0698: *
0699: * @return The coordinate corresponding to the supplied data value.
0700: */
0701: public double valueToJava2D(double value, Rectangle2D area,
0702: RectangleEdge edge) {
0703:
0704: value = this .timeline.toTimelineValue((long) value);
0705:
0706: DateRange range = (DateRange) getRange();
0707: double axisMin = this .timeline.toTimelineValue(range
0708: .getLowerDate());
0709: double axisMax = this .timeline.toTimelineValue(range
0710: .getUpperDate());
0711: double result = 0.0;
0712: if (RectangleEdge.isTopOrBottom(edge)) {
0713: double minX = area.getX();
0714: double maxX = area.getMaxX();
0715: if (isInverted()) {
0716: result = maxX
0717: + ((value - axisMin) / (axisMax - axisMin))
0718: * (minX - maxX);
0719: } else {
0720: result = minX
0721: + ((value - axisMin) / (axisMax - axisMin))
0722: * (maxX - minX);
0723: }
0724: } else if (RectangleEdge.isLeftOrRight(edge)) {
0725: double minY = area.getMinY();
0726: double maxY = area.getMaxY();
0727: if (isInverted()) {
0728: result = minY
0729: + (((value - axisMin) / (axisMax - axisMin)) * (maxY - minY));
0730: } else {
0731: result = maxY
0732: - (((value - axisMin) / (axisMax - axisMin)) * (maxY - minY));
0733: }
0734: }
0735: return result;
0736:
0737: }
0738:
0739: /**
0740: * Translates a date to Java2D coordinates, based on the range displayed by
0741: * this axis for the specified data area.
0742: *
0743: * @param date the date.
0744: * @param area the rectangle (in Java2D space) where the data is to be
0745: * plotted.
0746: * @param edge the axis location.
0747: *
0748: * @return The coordinate corresponding to the supplied date.
0749: */
0750: public double dateToJava2D(Date date, Rectangle2D area,
0751: RectangleEdge edge) {
0752: double value = date.getTime();
0753: return valueToJava2D(value, area, edge);
0754: }
0755:
0756: /**
0757: * Translates a Java2D coordinate into the corresponding data value. To
0758: * perform this translation, you need to know the area used for plotting
0759: * data, and which edge the axis is located on.
0760: *
0761: * @param java2DValue the coordinate in Java2D space.
0762: * @param area the rectangle (in Java2D space) where the data is to be
0763: * plotted.
0764: * @param edge the axis location.
0765: *
0766: * @return A data value.
0767: */
0768: public double java2DToValue(double java2DValue, Rectangle2D area,
0769: RectangleEdge edge) {
0770:
0771: DateRange range = (DateRange) getRange();
0772: double axisMin = this .timeline.toTimelineValue(range
0773: .getLowerDate());
0774: double axisMax = this .timeline.toTimelineValue(range
0775: .getUpperDate());
0776:
0777: double min = 0.0;
0778: double max = 0.0;
0779: if (RectangleEdge.isTopOrBottom(edge)) {
0780: min = area.getX();
0781: max = area.getMaxX();
0782: } else if (RectangleEdge.isLeftOrRight(edge)) {
0783: min = area.getMaxY();
0784: max = area.getY();
0785: }
0786:
0787: double result;
0788: if (isInverted()) {
0789: result = axisMax
0790: - ((java2DValue - min) / (max - min) * (axisMax - axisMin));
0791: } else {
0792: result = axisMin
0793: + ((java2DValue - min) / (max - min) * (axisMax - axisMin));
0794: }
0795:
0796: return this .timeline.toMillisecond((long) result);
0797: }
0798:
0799: /**
0800: * Calculates the value of the lowest visible tick on the axis.
0801: *
0802: * @param unit date unit to use.
0803: *
0804: * @return The value of the lowest visible tick on the axis.
0805: */
0806: public Date calculateLowestVisibleTickValue(DateTickUnit unit) {
0807: return nextStandardDate(getMinimumDate(), unit);
0808: }
0809:
0810: /**
0811: * Calculates the value of the highest visible tick on the axis.
0812: *
0813: * @param unit date unit to use.
0814: *
0815: * @return The value of the highest visible tick on the axis.
0816: */
0817: public Date calculateHighestVisibleTickValue(DateTickUnit unit) {
0818: return previousStandardDate(getMaximumDate(), unit);
0819: }
0820:
0821: /**
0822: * Returns the previous "standard" date, for a given date and tick unit.
0823: *
0824: * @param date the reference date.
0825: * @param unit the tick unit.
0826: *
0827: * @return The previous "standard" date.
0828: */
0829: protected Date previousStandardDate(Date date, DateTickUnit unit) {
0830:
0831: int milliseconds;
0832: int seconds;
0833: int minutes;
0834: int hours;
0835: int days;
0836: int months;
0837: int years;
0838:
0839: Calendar calendar = Calendar.getInstance(this .timeZone);
0840: calendar.setTime(date);
0841: int count = unit.getCount();
0842: int current = calendar.get(unit.getCalendarField());
0843: int value = count * (current / count);
0844:
0845: switch (unit.getUnit()) {
0846:
0847: case (DateTickUnit.MILLISECOND):
0848: years = calendar.get(Calendar.YEAR);
0849: months = calendar.get(Calendar.MONTH);
0850: days = calendar.get(Calendar.DATE);
0851: hours = calendar.get(Calendar.HOUR_OF_DAY);
0852: minutes = calendar.get(Calendar.MINUTE);
0853: seconds = calendar.get(Calendar.SECOND);
0854: calendar.set(years, months, days, hours, minutes, seconds);
0855: calendar.set(Calendar.MILLISECOND, value);
0856: Date mm = calendar.getTime();
0857: if (mm.getTime() >= date.getTime()) {
0858: calendar.set(Calendar.MILLISECOND, value - 1);
0859: mm = calendar.getTime();
0860: }
0861: return calendar.getTime();
0862:
0863: case (DateTickUnit.SECOND):
0864: years = calendar.get(Calendar.YEAR);
0865: months = calendar.get(Calendar.MONTH);
0866: days = calendar.get(Calendar.DATE);
0867: hours = calendar.get(Calendar.HOUR_OF_DAY);
0868: minutes = calendar.get(Calendar.MINUTE);
0869: if (this .tickMarkPosition == DateTickMarkPosition.START) {
0870: milliseconds = 0;
0871: } else if (this .tickMarkPosition == DateTickMarkPosition.MIDDLE) {
0872: milliseconds = 500;
0873: } else {
0874: milliseconds = 999;
0875: }
0876: calendar.set(Calendar.MILLISECOND, milliseconds);
0877: calendar.set(years, months, days, hours, minutes, value);
0878: Date dd = calendar.getTime();
0879: if (dd.getTime() >= date.getTime()) {
0880: calendar.set(Calendar.SECOND, value - 1);
0881: dd = calendar.getTime();
0882: }
0883: return calendar.getTime();
0884:
0885: case (DateTickUnit.MINUTE):
0886: years = calendar.get(Calendar.YEAR);
0887: months = calendar.get(Calendar.MONTH);
0888: days = calendar.get(Calendar.DATE);
0889: hours = calendar.get(Calendar.HOUR_OF_DAY);
0890: if (this .tickMarkPosition == DateTickMarkPosition.START) {
0891: seconds = 0;
0892: } else if (this .tickMarkPosition == DateTickMarkPosition.MIDDLE) {
0893: seconds = 30;
0894: } else {
0895: seconds = 59;
0896: }
0897: calendar.clear(Calendar.MILLISECOND);
0898: calendar.set(years, months, days, hours, value, seconds);
0899: Date d0 = calendar.getTime();
0900: if (d0.getTime() >= date.getTime()) {
0901: calendar.set(Calendar.MINUTE, value - 1);
0902: d0 = calendar.getTime();
0903: }
0904: return d0;
0905:
0906: case (DateTickUnit.HOUR):
0907: years = calendar.get(Calendar.YEAR);
0908: months = calendar.get(Calendar.MONTH);
0909: days = calendar.get(Calendar.DATE);
0910: if (this .tickMarkPosition == DateTickMarkPosition.START) {
0911: minutes = 0;
0912: seconds = 0;
0913: } else if (this .tickMarkPosition == DateTickMarkPosition.MIDDLE) {
0914: minutes = 30;
0915: seconds = 0;
0916: } else {
0917: minutes = 59;
0918: seconds = 59;
0919: }
0920: calendar.clear(Calendar.MILLISECOND);
0921: calendar.set(years, months, days, value, minutes, seconds);
0922: Date d1 = calendar.getTime();
0923: if (d1.getTime() >= date.getTime()) {
0924: calendar.set(Calendar.HOUR_OF_DAY, value - 1);
0925: d1 = calendar.getTime();
0926: }
0927: return d1;
0928:
0929: case (DateTickUnit.DAY):
0930: years = calendar.get(Calendar.YEAR);
0931: months = calendar.get(Calendar.MONTH);
0932: if (this .tickMarkPosition == DateTickMarkPosition.START) {
0933: hours = 0;
0934: minutes = 0;
0935: seconds = 0;
0936: } else if (this .tickMarkPosition == DateTickMarkPosition.MIDDLE) {
0937: hours = 12;
0938: minutes = 0;
0939: seconds = 0;
0940: } else {
0941: hours = 23;
0942: minutes = 59;
0943: seconds = 59;
0944: }
0945: calendar.clear(Calendar.MILLISECOND);
0946: calendar.set(years, months, value, hours, 0, 0);
0947: // long result = calendar.getTimeInMillis();
0948: // won't work with JDK 1.3
0949: Date d2 = calendar.getTime();
0950: if (d2.getTime() >= date.getTime()) {
0951: calendar.set(Calendar.DATE, value - 1);
0952: d2 = calendar.getTime();
0953: }
0954: return d2;
0955:
0956: case (DateTickUnit.MONTH):
0957: years = calendar.get(Calendar.YEAR);
0958: calendar.clear(Calendar.MILLISECOND);
0959: calendar.set(years, value, 1, 0, 0, 0);
0960: Month month = new Month(calendar.getTime(), this .timeZone);
0961: Date standardDate = calculateDateForPosition(month,
0962: this .tickMarkPosition);
0963: long millis = standardDate.getTime();
0964: if (millis >= date.getTime()) {
0965: month = (Month) month.previous();
0966: standardDate = calculateDateForPosition(month,
0967: this .tickMarkPosition);
0968: }
0969: return standardDate;
0970:
0971: case (DateTickUnit.YEAR):
0972: if (this .tickMarkPosition == DateTickMarkPosition.START) {
0973: months = 0;
0974: days = 1;
0975: } else if (this .tickMarkPosition == DateTickMarkPosition.MIDDLE) {
0976: months = 6;
0977: days = 1;
0978: } else {
0979: months = 11;
0980: days = 31;
0981: }
0982: calendar.clear(Calendar.MILLISECOND);
0983: calendar.set(value, months, days, 0, 0, 0);
0984: Date d3 = calendar.getTime();
0985: if (d3.getTime() >= date.getTime()) {
0986: calendar.set(Calendar.YEAR, value - 1);
0987: d3 = calendar.getTime();
0988: }
0989: return d3;
0990:
0991: default:
0992: return null;
0993:
0994: }
0995:
0996: }
0997:
0998: /**
0999: * Returns a {@link java.util.Date} corresponding to the specified position
1000: * within a {@link RegularTimePeriod}.
1001: *
1002: * @param period the period.
1003: * @param position the position (<code>null</code> not permitted).
1004: *
1005: * @return A date.
1006: */
1007: private Date calculateDateForPosition(RegularTimePeriod period,
1008: DateTickMarkPosition position) {
1009:
1010: if (position == null) {
1011: throw new IllegalArgumentException(
1012: "Null 'position' argument.");
1013: }
1014: Date result = null;
1015: if (position == DateTickMarkPosition.START) {
1016: result = new Date(period.getFirstMillisecond());
1017: } else if (position == DateTickMarkPosition.MIDDLE) {
1018: result = new Date(period.getMiddleMillisecond());
1019: } else if (position == DateTickMarkPosition.END) {
1020: result = new Date(period.getLastMillisecond());
1021: }
1022: return result;
1023:
1024: }
1025:
1026: /**
1027: * Returns the first "standard" date (based on the specified field and
1028: * units).
1029: *
1030: * @param date the reference date.
1031: * @param unit the date tick unit.
1032: *
1033: * @return The next "standard" date.
1034: */
1035: protected Date nextStandardDate(Date date, DateTickUnit unit) {
1036: Date previous = previousStandardDate(date, unit);
1037: Calendar calendar = Calendar.getInstance(this .timeZone);
1038: calendar.setTime(previous);
1039: calendar.add(unit.getCalendarField(), unit.getCount());
1040: return calendar.getTime();
1041: }
1042:
1043: /**
1044: * Returns a collection of standard date tick units that uses the default
1045: * time zone. This collection will be used by default, but you are free
1046: * to create your own collection if you want to (see the
1047: * {@link ValueAxis#setStandardTickUnits(TickUnitSource)} method inherited
1048: * from the {@link ValueAxis} class).
1049: *
1050: * @return A collection of standard date tick units.
1051: */
1052: public static TickUnitSource createStandardDateTickUnits() {
1053: return createStandardDateTickUnits(TimeZone.getDefault());
1054: }
1055:
1056: /**
1057: * Returns a collection of standard date tick units. This collection will
1058: * be used by default, but you are free to create your own collection if
1059: * you want to (see the
1060: * {@link ValueAxis#setStandardTickUnits(TickUnitSource)} method inherited
1061: * from the {@link ValueAxis} class).
1062: *
1063: * @param zone the time zone (<code>null</code> not permitted).
1064: *
1065: * @return A collection of standard date tick units.
1066: */
1067: public static TickUnitSource createStandardDateTickUnits(
1068: TimeZone zone) {
1069:
1070: if (zone == null) {
1071: throw new IllegalArgumentException("Null 'zone' argument.");
1072: }
1073: TickUnits units = new TickUnits();
1074:
1075: // date formatters
1076: DateFormat f1 = new SimpleDateFormat("HH:mm:ss.SSS");
1077: DateFormat f2 = new SimpleDateFormat("HH:mm:ss");
1078: DateFormat f3 = new SimpleDateFormat("HH:mm");
1079: DateFormat f4 = new SimpleDateFormat("d-MMM, HH:mm");
1080: DateFormat f5 = new SimpleDateFormat("d-MMM");
1081: DateFormat f6 = new SimpleDateFormat("MMM-yyyy");
1082: DateFormat f7 = new SimpleDateFormat("yyyy");
1083:
1084: f1.setTimeZone(zone);
1085: f2.setTimeZone(zone);
1086: f3.setTimeZone(zone);
1087: f4.setTimeZone(zone);
1088: f5.setTimeZone(zone);
1089: f6.setTimeZone(zone);
1090: f7.setTimeZone(zone);
1091:
1092: // milliseconds
1093: units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 1, f1));
1094: units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 5,
1095: DateTickUnit.MILLISECOND, 1, f1));
1096: units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 10,
1097: DateTickUnit.MILLISECOND, 1, f1));
1098: units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 25,
1099: DateTickUnit.MILLISECOND, 5, f1));
1100: units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 50,
1101: DateTickUnit.MILLISECOND, 10, f1));
1102: units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 100,
1103: DateTickUnit.MILLISECOND, 10, f1));
1104: units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 250,
1105: DateTickUnit.MILLISECOND, 10, f1));
1106: units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 500,
1107: DateTickUnit.MILLISECOND, 50, f1));
1108:
1109: // seconds
1110: units.add(new DateTickUnit(DateTickUnit.SECOND, 1,
1111: DateTickUnit.MILLISECOND, 50, f2));
1112: units.add(new DateTickUnit(DateTickUnit.SECOND, 5,
1113: DateTickUnit.SECOND, 1, f2));
1114: units.add(new DateTickUnit(DateTickUnit.SECOND, 10,
1115: DateTickUnit.SECOND, 1, f2));
1116: units.add(new DateTickUnit(DateTickUnit.SECOND, 30,
1117: DateTickUnit.SECOND, 5, f2));
1118:
1119: // minutes
1120: units.add(new DateTickUnit(DateTickUnit.MINUTE, 1,
1121: DateTickUnit.SECOND, 5, f3));
1122: units.add(new DateTickUnit(DateTickUnit.MINUTE, 2,
1123: DateTickUnit.SECOND, 10, f3));
1124: units.add(new DateTickUnit(DateTickUnit.MINUTE, 5,
1125: DateTickUnit.MINUTE, 1, f3));
1126: units.add(new DateTickUnit(DateTickUnit.MINUTE, 10,
1127: DateTickUnit.MINUTE, 1, f3));
1128: units.add(new DateTickUnit(DateTickUnit.MINUTE, 15,
1129: DateTickUnit.MINUTE, 5, f3));
1130: units.add(new DateTickUnit(DateTickUnit.MINUTE, 20,
1131: DateTickUnit.MINUTE, 5, f3));
1132: units.add(new DateTickUnit(DateTickUnit.MINUTE, 30,
1133: DateTickUnit.MINUTE, 5, f3));
1134:
1135: // hours
1136: units.add(new DateTickUnit(DateTickUnit.HOUR, 1,
1137: DateTickUnit.MINUTE, 5, f3));
1138: units.add(new DateTickUnit(DateTickUnit.HOUR, 2,
1139: DateTickUnit.MINUTE, 10, f3));
1140: units.add(new DateTickUnit(DateTickUnit.HOUR, 4,
1141: DateTickUnit.MINUTE, 30, f3));
1142: units.add(new DateTickUnit(DateTickUnit.HOUR, 6,
1143: DateTickUnit.HOUR, 1, f3));
1144: units.add(new DateTickUnit(DateTickUnit.HOUR, 12,
1145: DateTickUnit.HOUR, 1, f4));
1146:
1147: // days
1148: units.add(new DateTickUnit(DateTickUnit.DAY, 1,
1149: DateTickUnit.HOUR, 1, f5));
1150: units.add(new DateTickUnit(DateTickUnit.DAY, 2,
1151: DateTickUnit.HOUR, 1, f5));
1152: units.add(new DateTickUnit(DateTickUnit.DAY, 7,
1153: DateTickUnit.DAY, 1, f5));
1154: units.add(new DateTickUnit(DateTickUnit.DAY, 15,
1155: DateTickUnit.DAY, 1, f5));
1156:
1157: // months
1158: units.add(new DateTickUnit(DateTickUnit.MONTH, 1,
1159: DateTickUnit.DAY, 1, f6));
1160: units.add(new DateTickUnit(DateTickUnit.MONTH, 2,
1161: DateTickUnit.DAY, 1, f6));
1162: units.add(new DateTickUnit(DateTickUnit.MONTH, 3,
1163: DateTickUnit.MONTH, 1, f6));
1164: units.add(new DateTickUnit(DateTickUnit.MONTH, 4,
1165: DateTickUnit.MONTH, 1, f6));
1166: units.add(new DateTickUnit(DateTickUnit.MONTH, 6,
1167: DateTickUnit.MONTH, 1, f6));
1168:
1169: // years
1170: units.add(new DateTickUnit(DateTickUnit.YEAR, 1,
1171: DateTickUnit.MONTH, 1, f7));
1172: units.add(new DateTickUnit(DateTickUnit.YEAR, 2,
1173: DateTickUnit.MONTH, 3, f7));
1174: units.add(new DateTickUnit(DateTickUnit.YEAR, 5,
1175: DateTickUnit.YEAR, 1, f7));
1176: units.add(new DateTickUnit(DateTickUnit.YEAR, 10,
1177: DateTickUnit.YEAR, 1, f7));
1178: units.add(new DateTickUnit(DateTickUnit.YEAR, 25,
1179: DateTickUnit.YEAR, 5, f7));
1180: units.add(new DateTickUnit(DateTickUnit.YEAR, 50,
1181: DateTickUnit.YEAR, 10, f7));
1182: units.add(new DateTickUnit(DateTickUnit.YEAR, 100,
1183: DateTickUnit.YEAR, 20, f7));
1184:
1185: return units;
1186:
1187: }
1188:
1189: /**
1190: * Rescales the axis to ensure that all data is visible.
1191: */
1192: protected void autoAdjustRange() {
1193:
1194: Plot plot = getPlot();
1195:
1196: if (plot == null) {
1197: return; // no plot, no data
1198: }
1199:
1200: if (plot instanceof ValueAxisPlot) {
1201: ValueAxisPlot vap = (ValueAxisPlot) plot;
1202:
1203: Range r = vap.getDataRange(this );
1204: if (r == null) {
1205: if (this .timeline instanceof SegmentedTimeline) {
1206: //Timeline hasn't method getStartTime()
1207: r = new DateRange(
1208: ((SegmentedTimeline) this .timeline)
1209: .getStartTime(),
1210: ((SegmentedTimeline) this .timeline)
1211: .getStartTime() + 1);
1212: } else {
1213: r = new DateRange();
1214: }
1215: }
1216:
1217: long upper = this .timeline.toTimelineValue((long) r
1218: .getUpperBound());
1219: long lower;
1220: long fixedAutoRange = (long) getFixedAutoRange();
1221: if (fixedAutoRange > 0.0) {
1222: lower = upper - fixedAutoRange;
1223: } else {
1224: lower = this .timeline.toTimelineValue((long) r
1225: .getLowerBound());
1226: double range = upper - lower;
1227: long minRange = (long) getAutoRangeMinimumSize();
1228: if (range < minRange) {
1229: long expand = (long) (minRange - range) / 2;
1230: upper = upper + expand;
1231: lower = lower - expand;
1232: }
1233: upper = upper + (long) (range * getUpperMargin());
1234: lower = lower - (long) (range * getLowerMargin());
1235: }
1236:
1237: upper = this .timeline.toMillisecond(upper);
1238: lower = this .timeline.toMillisecond(lower);
1239: DateRange dr = new DateRange(new Date(lower), new Date(
1240: upper));
1241: setRange(dr, false, false);
1242: }
1243:
1244: }
1245:
1246: /**
1247: * Selects an appropriate tick value for the axis. The strategy is to
1248: * display as many ticks as possible (selected from an array of 'standard'
1249: * tick units) without the labels overlapping.
1250: *
1251: * @param g2 the graphics device.
1252: * @param dataArea the area defined by the axes.
1253: * @param edge the axis location.
1254: */
1255: protected void selectAutoTickUnit(Graphics2D g2,
1256: Rectangle2D dataArea, RectangleEdge edge) {
1257:
1258: if (RectangleEdge.isTopOrBottom(edge)) {
1259: selectHorizontalAutoTickUnit(g2, dataArea, edge);
1260: } else if (RectangleEdge.isLeftOrRight(edge)) {
1261: selectVerticalAutoTickUnit(g2, dataArea, edge);
1262: }
1263:
1264: }
1265:
1266: /**
1267: * Selects an appropriate tick size for the axis. The strategy is to
1268: * display as many ticks as possible (selected from a collection of
1269: * 'standard' tick units) without the labels overlapping.
1270: *
1271: * @param g2 the graphics device.
1272: * @param dataArea the area defined by the axes.
1273: * @param edge the axis location.
1274: */
1275: protected void selectHorizontalAutoTickUnit(Graphics2D g2,
1276: Rectangle2D dataArea, RectangleEdge edge) {
1277:
1278: long shift = 0;
1279: if (this .timeline instanceof SegmentedTimeline) {
1280: shift = ((SegmentedTimeline) this .timeline).getStartTime();
1281: }
1282: double zero = valueToJava2D(shift + 0.0, dataArea, edge);
1283: double tickLabelWidth = estimateMaximumTickLabelWidth(g2,
1284: getTickUnit());
1285:
1286: // start with the current tick unit...
1287: TickUnitSource tickUnits = getStandardTickUnits();
1288: TickUnit unit1 = tickUnits.getCeilingTickUnit(getTickUnit());
1289: double x1 = valueToJava2D(shift + unit1.getSize(), dataArea,
1290: edge);
1291: double unit1Width = Math.abs(x1 - zero);
1292:
1293: // then extrapolate...
1294: double guess = (tickLabelWidth / unit1Width) * unit1.getSize();
1295: DateTickUnit unit2 = (DateTickUnit) tickUnits
1296: .getCeilingTickUnit(guess);
1297: double x2 = valueToJava2D(shift + unit2.getSize(), dataArea,
1298: edge);
1299: double unit2Width = Math.abs(x2 - zero);
1300: tickLabelWidth = estimateMaximumTickLabelWidth(g2, unit2);
1301: if (tickLabelWidth > unit2Width) {
1302: unit2 = (DateTickUnit) tickUnits.getLargerTickUnit(unit2);
1303: }
1304: setTickUnit(unit2, false, false);
1305: }
1306:
1307: /**
1308: * Selects an appropriate tick size for the axis. The strategy is to
1309: * display as many ticks as possible (selected from a collection of
1310: * 'standard' tick units) without the labels overlapping.
1311: *
1312: * @param g2 the graphics device.
1313: * @param dataArea the area in which the plot should be drawn.
1314: * @param edge the axis location.
1315: */
1316: protected void selectVerticalAutoTickUnit(Graphics2D g2,
1317: Rectangle2D dataArea, RectangleEdge edge) {
1318:
1319: // start with the current tick unit...
1320: TickUnitSource tickUnits = getStandardTickUnits();
1321: double zero = valueToJava2D(0.0, dataArea, edge);
1322:
1323: // start with a unit that is at least 1/10th of the axis length
1324: double estimate1 = getRange().getLength() / 10.0;
1325: DateTickUnit candidate1 = (DateTickUnit) tickUnits
1326: .getCeilingTickUnit(estimate1);
1327: double labelHeight1 = estimateMaximumTickLabelHeight(g2,
1328: candidate1);
1329: double y1 = valueToJava2D(candidate1.getSize(), dataArea, edge);
1330: double candidate1UnitHeight = Math.abs(y1 - zero);
1331:
1332: // now extrapolate based on label height and unit height...
1333: double estimate2 = (labelHeight1 / candidate1UnitHeight)
1334: * candidate1.getSize();
1335: DateTickUnit candidate2 = (DateTickUnit) tickUnits
1336: .getCeilingTickUnit(estimate2);
1337: double labelHeight2 = estimateMaximumTickLabelHeight(g2,
1338: candidate2);
1339: double y2 = valueToJava2D(candidate2.getSize(), dataArea, edge);
1340: double unit2Height = Math.abs(y2 - zero);
1341:
1342: // make final selection...
1343: DateTickUnit finalUnit;
1344: if (labelHeight2 < unit2Height) {
1345: finalUnit = candidate2;
1346: } else {
1347: finalUnit = (DateTickUnit) tickUnits
1348: .getLargerTickUnit(candidate2);
1349: }
1350: setTickUnit(finalUnit, false, false);
1351:
1352: }
1353:
1354: /**
1355: * Estimates the maximum width of the tick labels, assuming the specified
1356: * tick unit is used.
1357: * <P>
1358: * Rather than computing the string bounds of every tick on the axis, we
1359: * just look at two values: the lower bound and the upper bound for the
1360: * axis. These two values will usually be representative.
1361: *
1362: * @param g2 the graphics device.
1363: * @param unit the tick unit to use for calculation.
1364: *
1365: * @return The estimated maximum width of the tick labels.
1366: */
1367: private double estimateMaximumTickLabelWidth(Graphics2D g2,
1368: DateTickUnit unit) {
1369:
1370: RectangleInsets tickLabelInsets = getTickLabelInsets();
1371: double result = tickLabelInsets.getLeft()
1372: + tickLabelInsets.getRight();
1373:
1374: Font tickLabelFont = getTickLabelFont();
1375: FontRenderContext frc = g2.getFontRenderContext();
1376: LineMetrics lm = tickLabelFont.getLineMetrics("ABCxyz", frc);
1377: if (isVerticalTickLabels()) {
1378: // all tick labels have the same width (equal to the height of
1379: // the font)...
1380: result += lm.getHeight();
1381: } else {
1382: // look at lower and upper bounds...
1383: DateRange range = (DateRange) getRange();
1384: Date lower = range.getLowerDate();
1385: Date upper = range.getUpperDate();
1386: String lowerStr = null;
1387: String upperStr = null;
1388: DateFormat formatter = getDateFormatOverride();
1389: if (formatter != null) {
1390: lowerStr = formatter.format(lower);
1391: upperStr = formatter.format(upper);
1392: } else {
1393: lowerStr = unit.dateToString(lower);
1394: upperStr = unit.dateToString(upper);
1395: }
1396: FontMetrics fm = g2.getFontMetrics(tickLabelFont);
1397: double w1 = fm.stringWidth(lowerStr);
1398: double w2 = fm.stringWidth(upperStr);
1399: result += Math.max(w1, w2);
1400: }
1401:
1402: return result;
1403:
1404: }
1405:
1406: /**
1407: * Estimates the maximum width of the tick labels, assuming the specified
1408: * tick unit is used.
1409: * <P>
1410: * Rather than computing the string bounds of every tick on the axis, we
1411: * just look at two values: the lower bound and the upper bound for the
1412: * axis. These two values will usually be representative.
1413: *
1414: * @param g2 the graphics device.
1415: * @param unit the tick unit to use for calculation.
1416: *
1417: * @return The estimated maximum width of the tick labels.
1418: */
1419: private double estimateMaximumTickLabelHeight(Graphics2D g2,
1420: DateTickUnit unit) {
1421:
1422: RectangleInsets tickLabelInsets = getTickLabelInsets();
1423: double result = tickLabelInsets.getTop()
1424: + tickLabelInsets.getBottom();
1425:
1426: Font tickLabelFont = getTickLabelFont();
1427: FontRenderContext frc = g2.getFontRenderContext();
1428: LineMetrics lm = tickLabelFont.getLineMetrics("ABCxyz", frc);
1429: if (!isVerticalTickLabels()) {
1430: // all tick labels have the same width (equal to the height of
1431: // the font)...
1432: result += lm.getHeight();
1433: } else {
1434: // look at lower and upper bounds...
1435: DateRange range = (DateRange) getRange();
1436: Date lower = range.getLowerDate();
1437: Date upper = range.getUpperDate();
1438: String lowerStr = null;
1439: String upperStr = null;
1440: DateFormat formatter = getDateFormatOverride();
1441: if (formatter != null) {
1442: lowerStr = formatter.format(lower);
1443: upperStr = formatter.format(upper);
1444: } else {
1445: lowerStr = unit.dateToString(lower);
1446: upperStr = unit.dateToString(upper);
1447: }
1448: FontMetrics fm = g2.getFontMetrics(tickLabelFont);
1449: double w1 = fm.stringWidth(lowerStr);
1450: double w2 = fm.stringWidth(upperStr);
1451: result += Math.max(w1, w2);
1452: }
1453:
1454: return result;
1455:
1456: }
1457:
1458: /**
1459: * Calculates the positions of the tick labels for the axis, storing the
1460: * results in the tick label list (ready for drawing).
1461: *
1462: * @param g2 the graphics device.
1463: * @param state the axis state.
1464: * @param dataArea the area in which the plot should be drawn.
1465: * @param edge the location of the axis.
1466: *
1467: * @return A list of ticks.
1468: */
1469: public List refreshTicks(Graphics2D g2, AxisState state,
1470: Rectangle2D dataArea, RectangleEdge edge) {
1471:
1472: List result = null;
1473: if (RectangleEdge.isTopOrBottom(edge)) {
1474: result = refreshTicksHorizontal(g2, dataArea, edge);
1475: } else if (RectangleEdge.isLeftOrRight(edge)) {
1476: result = refreshTicksVertical(g2, dataArea, edge);
1477: }
1478: return result;
1479:
1480: }
1481:
1482: /**
1483: * Recalculates the ticks for the date axis.
1484: *
1485: * @param g2 the graphics device.
1486: * @param dataArea the area in which the data is to be drawn.
1487: * @param edge the location of the axis.
1488: *
1489: * @return A list of ticks.
1490: */
1491: protected List refreshTicksHorizontal(Graphics2D g2,
1492: Rectangle2D dataArea, RectangleEdge edge) {
1493:
1494: List result = new java.util.ArrayList();
1495:
1496: Font tickLabelFont = getTickLabelFont();
1497: g2.setFont(tickLabelFont);
1498:
1499: if (isAutoTickUnitSelection()) {
1500: selectAutoTickUnit(g2, dataArea, edge);
1501: }
1502:
1503: DateTickUnit unit = getTickUnit();
1504: Date tickDate = calculateLowestVisibleTickValue(unit);
1505: Date upperDate = getMaximumDate();
1506:
1507: while (tickDate.before(upperDate)) {
1508:
1509: if (!isHiddenValue(tickDate.getTime())) {
1510: // work out the value, label and position
1511: String tickLabel;
1512: DateFormat formatter = getDateFormatOverride();
1513: if (formatter != null) {
1514: tickLabel = formatter.format(tickDate);
1515: } else {
1516: tickLabel = this .tickUnit.dateToString(tickDate);
1517: }
1518: TextAnchor anchor = null;
1519: TextAnchor rotationAnchor = null;
1520: double angle = 0.0;
1521: if (isVerticalTickLabels()) {
1522: anchor = TextAnchor.CENTER_RIGHT;
1523: rotationAnchor = TextAnchor.CENTER_RIGHT;
1524: if (edge == RectangleEdge.TOP) {
1525: angle = Math.PI / 2.0;
1526: } else {
1527: angle = -Math.PI / 2.0;
1528: }
1529: } else {
1530: if (edge == RectangleEdge.TOP) {
1531: anchor = TextAnchor.BOTTOM_CENTER;
1532: rotationAnchor = TextAnchor.BOTTOM_CENTER;
1533: } else {
1534: anchor = TextAnchor.TOP_CENTER;
1535: rotationAnchor = TextAnchor.TOP_CENTER;
1536: }
1537: }
1538:
1539: Tick tick = new DateTick(tickDate, tickLabel, anchor,
1540: rotationAnchor, angle);
1541: result.add(tick);
1542: tickDate = unit.addToDate(tickDate, this .timeZone);
1543: } else {
1544: tickDate = unit.rollDate(tickDate, this .timeZone);
1545: continue;
1546: }
1547:
1548: // could add a flag to make the following correction optional...
1549: switch (unit.getUnit()) {
1550:
1551: case (DateTickUnit.MILLISECOND):
1552: case (DateTickUnit.SECOND):
1553: case (DateTickUnit.MINUTE):
1554: case (DateTickUnit.HOUR):
1555: case (DateTickUnit.DAY):
1556: break;
1557: case (DateTickUnit.MONTH):
1558: tickDate = calculateDateForPosition(new Month(tickDate,
1559: this .timeZone), this .tickMarkPosition);
1560: break;
1561: case (DateTickUnit.YEAR):
1562: tickDate = calculateDateForPosition(new Year(tickDate,
1563: this .timeZone), this .tickMarkPosition);
1564: break;
1565:
1566: default:
1567: break;
1568:
1569: }
1570:
1571: }
1572: return result;
1573:
1574: }
1575:
1576: /**
1577: * Recalculates the ticks for the date axis.
1578: *
1579: * @param g2 the graphics device.
1580: * @param dataArea the area in which the plot should be drawn.
1581: * @param edge the location of the axis.
1582: *
1583: * @return A list of ticks.
1584: */
1585: protected List refreshTicksVertical(Graphics2D g2,
1586: Rectangle2D dataArea, RectangleEdge edge) {
1587:
1588: List result = new java.util.ArrayList();
1589:
1590: Font tickLabelFont = getTickLabelFont();
1591: g2.setFont(tickLabelFont);
1592:
1593: if (isAutoTickUnitSelection()) {
1594: selectAutoTickUnit(g2, dataArea, edge);
1595: }
1596: DateTickUnit unit = getTickUnit();
1597: Date tickDate = calculateLowestVisibleTickValue(unit);
1598: //Date upperDate = calculateHighestVisibleTickValue(unit);
1599: Date upperDate = getMaximumDate();
1600: while (tickDate.before(upperDate)) {
1601:
1602: if (!isHiddenValue(tickDate.getTime())) {
1603: // work out the value, label and position
1604: String tickLabel;
1605: DateFormat formatter = getDateFormatOverride();
1606: if (formatter != null) {
1607: tickLabel = formatter.format(tickDate);
1608: } else {
1609: tickLabel = this .tickUnit.dateToString(tickDate);
1610: }
1611: TextAnchor anchor = null;
1612: TextAnchor rotationAnchor = null;
1613: double angle = 0.0;
1614: if (isVerticalTickLabels()) {
1615: anchor = TextAnchor.BOTTOM_CENTER;
1616: rotationAnchor = TextAnchor.BOTTOM_CENTER;
1617: if (edge == RectangleEdge.LEFT) {
1618: angle = -Math.PI / 2.0;
1619: } else {
1620: angle = Math.PI / 2.0;
1621: }
1622: } else {
1623: if (edge == RectangleEdge.LEFT) {
1624: anchor = TextAnchor.CENTER_RIGHT;
1625: rotationAnchor = TextAnchor.CENTER_RIGHT;
1626: } else {
1627: anchor = TextAnchor.CENTER_LEFT;
1628: rotationAnchor = TextAnchor.CENTER_LEFT;
1629: }
1630: }
1631:
1632: Tick tick = new DateTick(tickDate, tickLabel, anchor,
1633: rotationAnchor, angle);
1634: result.add(tick);
1635: tickDate = unit.addToDate(tickDate, this .timeZone);
1636: } else {
1637: tickDate = unit.rollDate(tickDate, this .timeZone);
1638: }
1639: }
1640: return result;
1641: }
1642:
1643: /**
1644: * Draws the axis on a Java 2D graphics device (such as the screen or a
1645: * printer).
1646: *
1647: * @param g2 the graphics device (<code>null</code> not permitted).
1648: * @param cursor the cursor location.
1649: * @param plotArea the area within which the axes and data should be
1650: * drawn (<code>null</code> not permitted).
1651: * @param dataArea the area within which the data should be drawn
1652: * (<code>null</code> not permitted).
1653: * @param edge the location of the axis (<code>null</code> not permitted).
1654: * @param plotState collects information about the plot
1655: * (<code>null</code> permitted).
1656: *
1657: * @return The axis state (never <code>null</code>).
1658: */
1659: public AxisState draw(Graphics2D g2, double cursor,
1660: Rectangle2D plotArea, Rectangle2D dataArea,
1661: RectangleEdge edge, PlotRenderingInfo plotState) {
1662:
1663: // if the axis is not visible, don't draw it...
1664: if (!isVisible()) {
1665: AxisState state = new AxisState(cursor);
1666: // even though the axis is not visible, we need to refresh ticks in
1667: // case the grid is being drawn...
1668: List ticks = refreshTicks(g2, state, dataArea, edge);
1669: state.setTicks(ticks);
1670: return state;
1671: }
1672:
1673: // draw the tick marks and labels...
1674: AxisState state = drawTickMarksAndLabels(g2, cursor, plotArea,
1675: dataArea, edge);
1676:
1677: // draw the axis label (note that 'state' is passed in *and*
1678: // returned)...
1679: state = drawLabel(getLabel(), g2, plotArea, dataArea, edge,
1680: state);
1681:
1682: return state;
1683:
1684: }
1685:
1686: /**
1687: * Zooms in on the current range.
1688: *
1689: * @param lowerPercent the new lower bound.
1690: * @param upperPercent the new upper bound.
1691: */
1692: public void zoomRange(double lowerPercent, double upperPercent) {
1693: double start = this .timeline.toTimelineValue((long) getRange()
1694: .getLowerBound());
1695: double length = (this .timeline
1696: .toTimelineValue((long) getRange().getUpperBound()) - this .timeline
1697: .toTimelineValue((long) getRange().getLowerBound()));
1698: Range adjusted = null;
1699: if (isInverted()) {
1700: adjusted = new DateRange(
1701: this .timeline
1702: .toMillisecond((long) (start + (length * (1 - upperPercent)))),
1703: this .timeline
1704: .toMillisecond((long) (start + (length * (1 - lowerPercent)))));
1705: } else {
1706: adjusted = new DateRange(this .timeline
1707: .toMillisecond((long) (start + length
1708: * lowerPercent)), this .timeline
1709: .toMillisecond((long) (start + length
1710: * upperPercent)));
1711: }
1712: setRange(adjusted);
1713: }
1714:
1715: /**
1716: * Tests this axis for equality with an arbitrary object.
1717: *
1718: * @param obj the object (<code>null</code> permitted).
1719: *
1720: * @return A boolean.
1721: */
1722: public boolean equals(Object obj) {
1723: if (obj == this ) {
1724: return true;
1725: }
1726: if (!(obj instanceof DateAxis)) {
1727: return false;
1728: }
1729: DateAxis that = (DateAxis) obj;
1730: if (!ObjectUtilities.equal(this .tickUnit, that.tickUnit)) {
1731: return false;
1732: }
1733: if (!ObjectUtilities.equal(this .dateFormatOverride,
1734: that.dateFormatOverride)) {
1735: return false;
1736: }
1737: if (!ObjectUtilities.equal(this .tickMarkPosition,
1738: that.tickMarkPosition)) {
1739: return false;
1740: }
1741: if (!ObjectUtilities.equal(this .timeline, that.timeline)) {
1742: return false;
1743: }
1744: if (!super .equals(obj)) {
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: } else {
1759: return 0;
1760: }
1761: }
1762:
1763: /**
1764: * Returns a clone of the object.
1765: *
1766: * @return A clone.
1767: *
1768: * @throws CloneNotSupportedException if some component of the axis does
1769: * not support cloning.
1770: */
1771: public Object clone() throws CloneNotSupportedException {
1772:
1773: DateAxis clone = (DateAxis) super .clone();
1774:
1775: // 'dateTickUnit' is immutable : no need to clone
1776: if (this .dateFormatOverride != null) {
1777: clone.dateFormatOverride = (DateFormat) this .dateFormatOverride
1778: .clone();
1779: }
1780: // 'tickMarkPosition' is immutable : no need to clone
1781:
1782: return clone;
1783:
1784: }
1785:
1786: }
|