/* =========================================================== * JFreeChart : a free chart library for the Java(tm) platform * =========================================================== * * (C) Copyright 2000-2014, by Object Refinery Limited and Contributors. * * Project Info: http://www.jfree.org/jfreechart/index.html * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA. * * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners.] * * ------------- * DateAxis.java * ------------- * (C) Copyright 2000-2013, by Object Refinery Limited and Contributors. * * Original Author: David Gilbert; * Contributor(s): Jonathan Nash; * David Li; * Michael Rauch; * Bill Kelemen; * Pawel Pabis; * Chris Boek; * Peter Kolb (patches 1934255 and 2603321); * Andrew Mickish (patch 1870189); * Fawad Halim (bug 2201869); * * Changes (from 23-Jun-2001) * -------------------------- * 23-Jun-2001 : Modified to work with null data source (DG); * 18-Sep-2001 : Updated header (DG); * 27-Nov-2001 : Changed constructors from public to protected, updated Javadoc * comments (DG); * 16-Jan-2002 : Added an optional crosshair, based on the implementation by * Jonathan Nash (DG); * 26-Feb-2002 : Updated import statements (DG); * 22-Apr-2002 : Added a setRange() method (DG); * 25-Jun-2002 : Removed redundant local variable (DG); * 25-Jul-2002 : Changed order of parameters in ValueAxis constructor (DG); * 21-Aug-2002 : The setTickUnit() method now turns off auto-tick unit * selection (fix for bug id 528885) (DG); * 05-Sep-2002 : Updated the constructors to reflect changes in the Axis * class (DG); * 18-Sep-2002 : Fixed errors reported by Checkstyle (DG); * 25-Sep-2002 : Added new setRange() methods, and deprecated * setAxisRange() (DG); * 04-Oct-2002 : Changed auto tick selection to parallel number axis * classes (DG); * 24-Oct-2002 : Added a date format override (DG); * 08-Nov-2002 : Moved to new package com.jrefinery.chart.axis (DG); * 14-Jan-2003 : Changed autoRangeMinimumSize from Number --> double, moved * crosshair settings to the plot (DG); * 15-Jan-2003 : Removed anchor date (DG); * 20-Jan-2003 : Removed unnecessary constructors (DG); * 26-Mar-2003 : Implemented Serializable (DG); * 02-May-2003 : Added additional units to createStandardDateTickUnits() * method, as suggested by mhilpert in bug report 723187 (DG); * 13-May-2003 : Merged HorizontalDateAxis and VerticalDateAxis (DG); * 24-May-2003 : Added support for underlying timeline for * SegmentedTimeline (BK); * 16-Jul-2003 : Applied patch from Pawel Pabis to fix overlapping dates (DG); * 22-Jul-2003 : Applied patch from Pawel Pabis for monthly ticks (DG); * 25-Jul-2003 : Fixed bug 777561 and 777586 (DG); * 13-Aug-2003 : Implemented Cloneable and added equals() method (DG); * 02-Sep-2003 : Fixes for bug report 790506 (DG); * 04-Sep-2003 : Fixed tick label alignment when axis appears at the top (DG); * 10-Sep-2003 : Fixes for segmented timeline (DG); * 17-Sep-2003 : Fixed a layout bug when multiple domain axes are used (DG); * 29-Oct-2003 : Added workaround for font alignment in PDF output (DG); * 07-Nov-2003 : Modified to use new tick classes (DG); * 12-Nov-2003 : Modified tick labelling to use roll unit from DateTickUnit * when a calculated tick value is hidden (which can occur in * segmented date axes) (DG); * 24-Nov-2003 : Fixed some problems with the auto tick unit selection, and * fixed bug 846277 (labels missing for inverted axis) (DG); * 30-Dec-2003 : Fixed bug in refreshTicksHorizontal() when start of time unit * (ex. 1st of month) was hidden, causing infinite loop (BK); * 13-Jan-2004 : Fixed bug in previousStandardDate() method (fix by Richard * Wardle) (DG); * 21-Jan-2004 : Renamed translateJava2DToValue --> java2DToValue, and * translateValueToJava2D --> valueToJava2D (DG); * 12-Mar-2004 : Fixed bug where date format override is ignored for vertical * axis (DG); * 16-Mar-2004 : Added plotState to draw() method (DG); * 07-Apr-2004 : Changed string width calculation (DG); * 21-Apr-2004 : Fixed bug in estimateMaximumTickLabelWidth() method (bug id * 939148) (DG); * 11-Jan-2005 : Removed deprecated methods in preparation for 1.0.0 * release (DG); * 13-Jan-2005 : Fixed bug (see * http://www.jfree.org/forum/viewtopic.php?t=11330) (DG); * 21-Apr-2005 : Replaced Insets with RectangleInsets, removed redundant * argument from selectAutoTickUnit() (DG); * ------------- JFREECHART 1.0.x --------------------------------------------- * 10-Feb-2006 : Added some API doc comments in respect of bug 821046 (DG); * 19-Apr-2006 : Fixed bug 1472942 in equals() method (DG); * 25-Sep-2006 : Fixed bug 1564977 missing tick labels (DG); * 15-Jan-2007 : Added get/setTimeZone() suggested by 'skunk' (DG); * 18-Jan-2007 : Fixed bug 1638678, time zone for calendar in * previousStandardDate() (DG); * 04-Apr-2007 : Use time zone in date calculations (CB); * 19-Apr-2007 : Fix exceptions in setMinimum/MaximumDate() (DG); * 03-May-2007 : Fixed minor bugs in previousStandardDate(), with new JUnit * tests (DG); * 21-Nov-2007 : Fixed warnings from FindBugs (DG); * 01-Sep-2008 : Use new methods from DateRange, added fix for bug * 2078057 (DG); * 18-Sep-2008 : Added locale to go with timezone (DG); * 25-Sep-2008 : Added minor tick support, see patch 1934255 by Peter Kolb (DG); * 25-Nov-2008 : Added bug fix 2201869 by Fawad Halim (DG); * 21-Jan-2009 : Check tickUnit for minor tick count (DG); * 19-Mar-2009 : Added entity support - see patch 2603321 by Peter Kolb (DG); * 08-Feb-2012 : Bugfix for endless-loop, bug 3484403 by rbrabe (MH); * 16-Jun-2012 : Removed JCommon dependencies (DG); * */ package org.jfree.chart.axis; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.font.FontRenderContext; import java.awt.font.LineMetrics; import java.awt.geom.Rectangle2D; import java.io.Serializable; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.TimeZone; import org.jfree.chart.event.AxisChangeEvent; import org.jfree.chart.ui.RectangleEdge; import org.jfree.chart.ui.RectangleInsets; import org.jfree.chart.ui.TextAnchor; import org.jfree.chart.util.ObjectUtils; import org.jfree.chart.plot.Plot; import org.jfree.chart.plot.PlotRenderingInfo; import org.jfree.chart.plot.ValueAxisPlot; import org.jfree.chart.util.ParamChecks; import org.jfree.data.Range; import org.jfree.data.time.DateRange; import org.jfree.data.time.Month; import org.jfree.data.time.RegularTimePeriod; import org.jfree.data.time.Year; /** * The base class for axes that display dates. You will find it easier to * understand how this axis works if you bear in mind that it really * displays/measures integer (or long) data, where the integers are * milliseconds since midnight, 1-Jan-1970. When displaying tick labels, the * millisecond values are converted back to dates using a * <code>DateFormat</code> instance. * <P> * You can also create a {@link org.jfree.chart.axis.Timeline} and supply in * the constructor to create an axis that only contains certain domain values. * For example, this allows you to create a date axis that only contains * working days. */ public class DateAxis extends ValueAxis implements Cloneable, Serializable { /** For serialization. */ private static final long serialVersionUID = -1013460999649007604L; /** The default axis range. */ public static final DateRange DEFAULT_DATE_RANGE = new DateRange(); /** The default minimum auto range size. */ public static final double DEFAULT_AUTO_RANGE_MINIMUM_SIZE_IN_MILLISECONDS = 2.0; /** The default anchor date. */ public static final Date DEFAULT_ANCHOR_DATE = new Date(); /** The current tick unit. */ private DateTickUnit tickUnit; /** The override date format. */ private DateFormat dateFormatOverride; /** * Tick marks can be displayed at the start or the middle of the time * period. */ private DateTickMarkPosition tickMarkPosition = DateTickMarkPosition.START; /** * A timeline that includes all milliseconds (as defined by * <code>java.util.Date</code>) in the real time line. */ private static class DefaultTimeline implements Timeline, Serializable { /** * Converts a millisecond into a timeline value. * * @param millisecond the millisecond. * * @return The timeline value. */ @Override public long toTimelineValue(long millisecond) { return millisecond; } /** * Converts a date into a timeline value. * * @param date the domain value. * * @return The timeline value. */ @Override public long toTimelineValue(Date date) { return date.getTime(); } /** * Converts a timeline value into a millisecond (as encoded by * <code>java.util.Date</code>). * * @param value the value. * * @return The millisecond. */ @Override public long toMillisecond(long value) { return value; } /** * Returns <code>true</code> if the timeline includes the specified * domain value. * * @param millisecond the millisecond. * * @return <code>true</code>. */ @Override public boolean containsDomainValue(long millisecond) { return true; } /** * Returns <code>true</code> if the timeline includes the specified * domain value. * * @param date the date. * * @return <code>true</code>. */ @Override public boolean containsDomainValue(Date date) { return true; } /** * Returns <code>true</code> if the timeline includes the specified * domain value range. * * @param from the start value. * @param to the end value. * * @return <code>true</code>. */ @Override public boolean containsDomainRange(long from, long to) { return true; } /** * Returns <code>true</code> if the timeline includes the specified * domain value range. * * @param from the start date. * @param to the end date. * * @return <code>true</code>. */ @Override public boolean containsDomainRange(Date from, Date to) { return true; } /** * Tests an object for equality with this instance. * * @param object the object. * * @return A boolean. */ @Override public boolean equals(Object object) { if (object == null) { return false; } if (object == this) { return true; } if (object instanceof DefaultTimeline) { return true; } return false; } } /** A static default timeline shared by all standard DateAxis */ private static final Timeline DEFAULT_TIMELINE = new DefaultTimeline(); /** The time zone for the axis. */ private TimeZone timeZone; /** * The locale for the axis (<code>null</code> is not permitted). * * @since 1.0.11 */ private Locale locale; /** Our underlying timeline. */ private Timeline timeline; /** * Creates a date axis with no label. */ public DateAxis() { this(null); } /** * Creates a date axis with the specified label. * * @param label the axis label (<code>null</code> permitted). */ public DateAxis(String label) { this(label, TimeZone.getDefault(), Locale.getDefault()); } /** * Creates a date axis. A timeline is specified for the axis. This allows * special transformations to occur between a domain of values and the * values included in the axis. * * @param label the axis label (<code>null</code> permitted). * @param zone the time zone. * @param locale the locale (<code>null</code> not permitted). * * @since 1.0.11 */ public DateAxis(String label, TimeZone zone, Locale locale) { super(label, DateAxis.createStandardDateTickUnits(zone, locale)); this.tickUnit = new DateTickUnit(DateTickUnitType.DAY, 1, new SimpleDateFormat()); setAutoRangeMinimumSize( DEFAULT_AUTO_RANGE_MINIMUM_SIZE_IN_MILLISECONDS); setRange(DEFAULT_DATE_RANGE, false, false); this.dateFormatOverride = null; this.timeZone = zone; this.locale = locale; this.timeline = DEFAULT_TIMELINE; } /** * Returns the time zone for the axis. * * @return The time zone (never <code>null</code>). * * @since 1.0.4 * * @see #setTimeZone(TimeZone) */ public TimeZone getTimeZone() { return this.timeZone; } /** * Sets the time zone for the axis and sends an {@link AxisChangeEvent} to * all registered listeners. * * @param zone the time zone (<code>null</code> not permitted). * * @since 1.0.4 * * @see #getTimeZone() */ public void setTimeZone(TimeZone zone) { ParamChecks.nullNotPermitted(zone, "zone"); this.timeZone = zone; setStandardTickUnits(createStandardDateTickUnits(zone, this.locale)); fireChangeEvent(); } /** * Returns the locale for this axis. * * @return The locale (never <code>null</code>). * * @since 1.0.18 */ public Locale getLocale() { return this.locale; } /** * Sets the locale for the axis and sends a change event to all registered * listeners. * * @param locale the new locale (<code>null</code> not permitted). */ public void setLocale(Locale locale) { ParamChecks.nullNotPermitted(locale, "locale"); this.locale = locale; setStandardTickUnits(createStandardDateTickUnits(this.timeZone, this.locale)); fireChangeEvent(); } /** * Returns the underlying timeline used by this axis. * * @return The timeline. */ public Timeline getTimeline() { return this.timeline; } /** * Sets the underlying timeline to use for this axis. If the timeline is * changed, an {@link AxisChangeEvent} is sent to all registered listeners. * * @param timeline the timeline. */ public void setTimeline(Timeline timeline) { if (this.timeline != timeline) { this.timeline = timeline; fireChangeEvent(); } } /** * Returns the tick unit for the axis. * <p> * Note: if the <code>autoTickUnitSelection</code> flag is * <code>true</code> the tick unit may be changed while the axis is being * drawn, so in that case the return value from this method may be * irrelevant if the method is called before the axis has been drawn. * * @return The tick unit (possibly <code>null</code>). * * @see #setTickUnit(DateTickUnit) * @see ValueAxis#isAutoTickUnitSelection() */ public DateTickUnit getTickUnit() { return this.tickUnit; } /** * Sets the tick unit for the axis. The auto-tick-unit-selection flag is * set to <code>false</code>, and registered listeners are notified that * the axis has been changed. * * @param unit the tick unit. * * @see #getTickUnit() * @see #setTickUnit(DateTickUnit, boolean, boolean) */ public void setTickUnit(DateTickUnit unit) { setTickUnit(unit, true, true); } /** * Sets the tick unit attribute and, if requested, sends an * {@link AxisChangeEvent} to all registered listeners. * * @param unit the new tick unit. * @param notify notify registered listeners? * @param turnOffAutoSelection turn off auto selection? * * @see #getTickUnit() */ public void setTickUnit(DateTickUnit unit, boolean notify, boolean turnOffAutoSelection) { this.tickUnit = unit; if (turnOffAutoSelection) { setAutoTickUnitSelection(false, false); } if (notify) { fireChangeEvent(); } } /** * Returns the date format override. If this is non-null, then it will be * used to format the dates on the axis. * * @return The formatter (possibly <code>null</code>). */ public DateFormat getDateFormatOverride() { return this.dateFormatOverride; } /** * Sets the date format override and sends an {@link AxisChangeEvent} to * all registered listeners. If this is non-null, then it will be * used to format the dates on the axis. * * @param formatter the date formatter (<code>null</code> permitted). */ public void setDateFormatOverride(DateFormat formatter) { this.dateFormatOverride = formatter; fireChangeEvent(); } /** * Sets the upper and lower bounds for the axis and sends an * {@link AxisChangeEvent} to all registered listeners. As a side-effect, * the auto-range flag is set to false. * * @param range the new range (<code>null</code> not permitted). */ @Override public void setRange(Range range) { setRange(range, true, true); } /** * Sets the range for the axis, if requested, sends an * {@link AxisChangeEvent} to all registered listeners. As a side-effect, * the auto-range flag is set to <code>false</code> (optional). * * @param range the range (<code>null</code> not permitted). * @param turnOffAutoRange a flag that controls whether or not the auto * range is turned off. * @param notify a flag that controls whether or not listeners are * notified. */ @Override public void setRange(Range range, boolean turnOffAutoRange, boolean notify) { ParamChecks.nullNotPermitted(range, "range"); // usually the range will be a DateRange, but if it isn't do a // conversion... if (!(range instanceof DateRange)) { range = new DateRange(range); } super.setRange(range, turnOffAutoRange, notify); } /** * Sets the axis range and sends an {@link AxisChangeEvent} to all * registered listeners. * * @param lower the lower bound for the axis. * @param upper the upper bound for the axis. */ public void setRange(Date lower, Date upper) { if (lower.getTime() >= upper.getTime()) { throw new IllegalArgumentException("Requires 'lower' < 'upper'."); } setRange(new DateRange(lower, upper)); } /** * Sets the axis range and sends an {@link AxisChangeEvent} to all * registered listeners. * * @param lower the lower bound for the axis. * @param upper the upper bound for the axis. */ @Override public void setRange(double lower, double upper) { if (lower >= upper) { throw new IllegalArgumentException("Requires 'lower' < 'upper'."); } setRange(new DateRange(lower, upper)); } /** * Returns the earliest date visible on the axis. * * @return The date. * * @see #setMinimumDate(Date) * @see #getMaximumDate() */ public Date getMinimumDate() { Date result; Range range = getRange(); if (range instanceof DateRange) { DateRange r = (DateRange) range; result = r.getLowerDate(); } else { result = new Date((long) range.getLowerBound()); } return result; } /** * Sets the minimum date visible on the axis and sends an * {@link AxisChangeEvent} to all registered listeners. If * <code>date</code> is on or after the current maximum date for * the axis, the maximum date will be shifted to preserve the current * length of the axis. * * @param date the date (<code>null</code> not permitted). * * @see #getMinimumDate() * @see #setMaximumDate(Date) */ public void setMinimumDate(Date date) { ParamChecks.nullNotPermitted(date, "date"); // check the new minimum date relative to the current maximum date Date maxDate = getMaximumDate(); long maxMillis = maxDate.getTime(); long newMinMillis = date.getTime(); if (maxMillis <= newMinMillis) { Date oldMin = getMinimumDate(); long length = maxMillis - oldMin.getTime(); maxDate = new Date(newMinMillis + length); } setRange(new DateRange(date, maxDate), true, false); fireChangeEvent(); } /** * Returns the latest date visible on the axis. * * @return The date. * * @see #setMaximumDate(Date) * @see #getMinimumDate() */ public Date getMaximumDate() { Date result; Range range = getRange(); if (range instanceof DateRange) { DateRange r = (DateRange) range; result = r.getUpperDate(); } else { result = new Date((long) range.getUpperBound()); } return result; } /** * Sets the maximum date visible on the axis and sends an * {@link AxisChangeEvent} to all registered listeners. If * <code>maximumDate</code> is on or before the current minimum date for * the axis, the minimum date will be shifted to preserve the current * length of the axis. * * @param maximumDate the date (<code>null</code> not permitted). * * @see #getMinimumDate() * @see #setMinimumDate(Date) */ public void setMaximumDate(Date maximumDate) { ParamChecks.nullNotPermitted(maximumDate, "maximumDate"); // check the new maximum date relative to the current minimum date Date minDate = getMinimumDate(); long minMillis = minDate.getTime(); long newMaxMillis = maximumDate.getTime(); if (minMillis >= newMaxMillis) { Date oldMax = getMaximumDate(); long length = oldMax.getTime() - minMillis; minDate = new Date(newMaxMillis - length); } setRange(new DateRange(minDate, maximumDate), true, false); fireChangeEvent(); } /** * Returns the tick mark position (start, middle or end of the time period). * * @return The position (never <code>null</code>). */ public DateTickMarkPosition getTickMarkPosition() { return this.tickMarkPosition; } /** * Sets the tick mark position (start, middle or end of the time period) * and sends an {@link AxisChangeEvent} to all registered listeners. * * @param position the position (<code>null</code> not permitted). */ public void setTickMarkPosition(DateTickMarkPosition position) { ParamChecks.nullNotPermitted(position, "position"); this.tickMarkPosition = position; fireChangeEvent(); } /** * Configures the axis to work with the specified plot. If the axis has * auto-scaling, then sets the maximum and minimum values. */ @Override public void configure() { if (isAutoRange()) { autoAdjustRange(); } } /** * Returns <code>true</code> if the axis hides this value, and * <code>false</code> otherwise. * * @param millis the data value. * * @return A value. */ public boolean isHiddenValue(long millis) { return (!this.timeline.containsDomainValue(new Date(millis))); } /** * Translates the data value to the display coordinates (Java 2D User Space) * of the chart. * * @param value the date to be plotted. * @param area the rectangle (in Java2D space) where the data is to be * plotted. * @param edge the axis location. * * @return The coordinate corresponding to the supplied data value. */ @Override public double valueToJava2D(double value, Rectangle2D area, RectangleEdge edge) { value = this.timeline.toTimelineValue((long) value); DateRange range = (DateRange) getRange(); double axisMin = this.timeline.toTimelineValue(range.getLowerMillis()); double axisMax = this.timeline.toTimelineValue(range.getUpperMillis()); double result = 0.0; if (RectangleEdge.isTopOrBottom(edge)) { double minX = area.getX(); double maxX = area.getMaxX(); if (isInverted()) { result = maxX + ((value - axisMin) / (axisMax - axisMin)) * (minX - maxX); } else { result = minX + ((value - axisMin) / (axisMax - axisMin)) * (maxX - minX); } } else if (RectangleEdge.isLeftOrRight(edge)) { double minY = area.getMinY(); double maxY = area.getMaxY(); if (isInverted()) { result = minY + (((value - axisMin) / (axisMax - axisMin)) * (maxY - minY)); } else { result = maxY - (((value - axisMin) / (axisMax - axisMin)) * (maxY - minY)); } } return result; } /** * Translates a date to Java2D coordinates, based on the range displayed by * this axis for the specified data area. * * @param date the date. * @param area the rectangle (in Java2D space) where the data is to be * plotted. * @param edge the axis location. * * @return The coordinate corresponding to the supplied date. */ public double dateToJava2D(Date date, Rectangle2D area, RectangleEdge edge) { double value = date.getTime(); return valueToJava2D(value, area, edge); } /** * Translates a Java2D coordinate into the corresponding data value. To * perform this translation, you need to know the area used for plotting * data, and which edge the axis is located on. * * @param java2DValue the coordinate in Java2D space. * @param area the rectangle (in Java2D space) where the data is to be * plotted. * @param edge the axis location. * * @return A data value. */ @Override public double java2DToValue(double java2DValue, Rectangle2D area, RectangleEdge edge) { DateRange range = (DateRange) getRange(); double axisMin = this.timeline.toTimelineValue(range.getLowerMillis()); double axisMax = this.timeline.toTimelineValue(range.getUpperMillis()); double min = 0.0; double max = 0.0; if (RectangleEdge.isTopOrBottom(edge)) { min = area.getX(); max = area.getMaxX(); } else if (RectangleEdge.isLeftOrRight(edge)) { min = area.getMaxY(); max = area.getY(); } double result; if (isInverted()) { result = axisMax - ((java2DValue - min) / (max - min) * (axisMax - axisMin)); } else { result = axisMin + ((java2DValue - min) / (max - min) * (axisMax - axisMin)); } return this.timeline.toMillisecond((long) result); } /** * Calculates the value of the lowest visible tick on the axis. * * @param unit date unit to use. * * @return The value of the lowest visible tick on the axis. */ public Date calculateLowestVisibleTickValue(DateTickUnit unit) { return nextStandardDate(getMinimumDate(), unit); } /** * Calculates the value of the highest visible tick on the axis. * * @param unit date unit to use. * * @return The value of the highest visible tick on the axis. */ public Date calculateHighestVisibleTickValue(DateTickUnit unit) { return previousStandardDate(getMaximumDate(), unit); } /** * Returns the previous "standard" date, for a given date and tick unit. * * @param date the reference date. * @param unit the tick unit. * * @return The previous "standard" date. */ protected Date previousStandardDate(Date date, DateTickUnit unit) { int milliseconds; int seconds; int minutes; int hours; int days; int months; int years; Calendar calendar = Calendar.getInstance(this.timeZone, this.locale); calendar.setTime(date); int count = unit.getMultiple(); int current = calendar.get(unit.getCalendarField()); int value = count * (current / count); if (DateTickUnitType.MILLISECOND.equals(unit.getUnitType())) { years = calendar.get(Calendar.YEAR); months = calendar.get(Calendar.MONTH); days = calendar.get(Calendar.DATE); hours = calendar.get(Calendar.HOUR_OF_DAY); minutes = calendar.get(Calendar.MINUTE); seconds = calendar.get(Calendar.SECOND); calendar.set(years, months, days, hours, minutes, seconds); calendar.set(Calendar.MILLISECOND, value); Date mm = calendar.getTime(); if (mm.getTime() >= date.getTime()) { calendar.set(Calendar.MILLISECOND, value - 1); mm = calendar.getTime(); } return mm; } else if (DateTickUnitType.SECOND.equals(unit.getUnitType())) { years = calendar.get(Calendar.YEAR); months = calendar.get(Calendar.MONTH); days = calendar.get(Calendar.DATE); hours = calendar.get(Calendar.HOUR_OF_DAY); minutes = calendar.get(Calendar.MINUTE); if (this.tickMarkPosition == DateTickMarkPosition.START) { milliseconds = 0; } else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) { milliseconds = 500; } else { milliseconds = 999; } calendar.set(Calendar.MILLISECOND, milliseconds); calendar.set(years, months, days, hours, minutes, value); Date dd = calendar.getTime(); if (dd.getTime() >= date.getTime()) { calendar.set(Calendar.SECOND, value - 1); dd = calendar.getTime(); } return dd; } else if (DateTickUnitType.MINUTE.equals(unit.getUnitType())) { years = calendar.get(Calendar.YEAR); months = calendar.get(Calendar.MONTH); days = calendar.get(Calendar.DATE); hours = calendar.get(Calendar.HOUR_OF_DAY); if (this.tickMarkPosition == DateTickMarkPosition.START) { seconds = 0; } else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) { seconds = 30; } else { seconds = 59; } calendar.clear(Calendar.MILLISECOND); calendar.set(years, months, days, hours, value, seconds); Date d0 = calendar.getTime(); if (d0.getTime() >= date.getTime()) { calendar.set(Calendar.MINUTE, value - 1); d0 = calendar.getTime(); } return d0; } else if (DateTickUnitType.HOUR.equals(unit.getUnitType())) { years = calendar.get(Calendar.YEAR); months = calendar.get(Calendar.MONTH); days = calendar.get(Calendar.DATE); if (this.tickMarkPosition == DateTickMarkPosition.START) { minutes = 0; seconds = 0; } else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) { minutes = 30; seconds = 0; } else { minutes = 59; seconds = 59; } calendar.clear(Calendar.MILLISECOND); calendar.set(years, months, days, value, minutes, seconds); Date d1 = calendar.getTime(); if (d1.getTime() >= date.getTime()) { calendar.set(Calendar.HOUR_OF_DAY, value - 1); d1 = calendar.getTime(); } return d1; } else if (DateTickUnitType.DAY.equals(unit.getUnitType())) { years = calendar.get(Calendar.YEAR); months = calendar.get(Calendar.MONTH); if (this.tickMarkPosition == DateTickMarkPosition.START) { hours = 0; } else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) { hours = 12; } else { hours = 23; } calendar.clear(Calendar.MILLISECOND); calendar.set(years, months, value, hours, 0, 0); // long result = calendar.getTimeInMillis(); // won't work with JDK 1.3 Date d2 = calendar.getTime(); if (d2.getTime() >= date.getTime()) { calendar.set(Calendar.DATE, value - 1); d2 = calendar.getTime(); } return d2; } else if (DateTickUnitType.MONTH.equals(unit.getUnitType())) { years = calendar.get(Calendar.YEAR); calendar.clear(Calendar.MILLISECOND); calendar.set(years, value, 1, 0, 0, 0); Month month = new Month(calendar.getTime(), this.timeZone, this.locale); Date standardDate = calculateDateForPosition( month, this.tickMarkPosition); long millis = standardDate.getTime(); if (millis >= date.getTime()) { month = (Month) month.previous(); // need to peg the month in case the time zone isn't the // default - see bug 2078057 month.peg(Calendar.getInstance(this.timeZone)); standardDate = calculateDateForPosition( month, this.tickMarkPosition); } return standardDate; } else if (DateTickUnitType.YEAR.equals(unit.getUnitType())) { if (this.tickMarkPosition == DateTickMarkPosition.START) { months = 0; days = 1; } else if (this.tickMarkPosition == DateTickMarkPosition.MIDDLE) { months = 6; days = 1; } else { months = 11; days = 31; } calendar.clear(Calendar.MILLISECOND); calendar.set(value, months, days, 0, 0, 0); Date d3 = calendar.getTime(); if (d3.getTime() >= date.getTime()) { calendar.set(Calendar.YEAR, value - 1); d3 = calendar.getTime(); } return d3; } else { return null; } } /** * Returns a {@link java.util.Date} corresponding to the specified position * within a {@link RegularTimePeriod}. * * @param period the period. * @param position the position (<code>null</code> not permitted). * * @return A date. */ private Date calculateDateForPosition(RegularTimePeriod period, DateTickMarkPosition position) { ParamChecks.nullNotPermitted(period, "period"); Date result = null; if (position == DateTickMarkPosition.START) { result = new Date(period.getFirstMillisecond()); } else if (position == DateTickMarkPosition.MIDDLE) { result = new Date(period.getMiddleMillisecond()); } else if (position == DateTickMarkPosition.END) { result = new Date(period.getLastMillisecond()); } return result; } /** * Returns the first "standard" date (based on the specified field and * units). * * @param date the reference date. * @param unit the date tick unit. * * @return The next "standard" date. */ protected Date nextStandardDate(Date date, DateTickUnit unit) { Date previous = previousStandardDate(date, unit); Calendar calendar = Calendar.getInstance(this.timeZone, this.locale); calendar.setTime(previous); calendar.add(unit.getCalendarField(), unit.getMultiple()); return calendar.getTime(); } /** * Returns a collection of standard date tick units that uses the default * time zone. This collection will be used by default, but you are free * to create your own collection if you want to (see the * {@link ValueAxis#setStandardTickUnits(TickUnitSource)} method inherited * from the {@link ValueAxis} class). * * @return A collection of standard date tick units. */ public static TickUnitSource createStandardDateTickUnits() { return createStandardDateTickUnits(TimeZone.getDefault(), Locale.getDefault()); } /** * Returns a collection of standard date tick units. This collection will * be used by default, but you are free to create your own collection if * you want to (see the * {@link ValueAxis#setStandardTickUnits(TickUnitSource)} method inherited * from the {@link ValueAxis} class). * * @param zone the time zone (<code>null</code> not permitted). * @param locale the locale (<code>null</code> not permitted). * * @return A collection of standard date tick units. * * @since 1.0.11 */ public static TickUnitSource createStandardDateTickUnits(TimeZone zone, Locale locale) { ParamChecks.nullNotPermitted(zone, "zone"); ParamChecks.nullNotPermitted(locale, "locale"); TickUnits units = new TickUnits(); // date formatters DateFormat f1 = new SimpleDateFormat("HH:mm:ss.SSS", locale); DateFormat f2 = new SimpleDateFormat("HH:mm:ss", locale); DateFormat f3 = new SimpleDateFormat("HH:mm", locale); DateFormat f4 = new SimpleDateFormat("d-MMM, HH:mm", locale); DateFormat f5 = new SimpleDateFormat("d-MMM", locale); DateFormat f6 = new SimpleDateFormat("MMM-yyyy", locale); DateFormat f7 = new SimpleDateFormat("yyyy", locale); f1.setTimeZone(zone); f2.setTimeZone(zone); f3.setTimeZone(zone); f4.setTimeZone(zone); f5.setTimeZone(zone); f6.setTimeZone(zone); f7.setTimeZone(zone); // milliseconds units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 1, f1)); units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 5, DateTickUnitType.MILLISECOND, 1, f1)); units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 10, DateTickUnitType.MILLISECOND, 1, f1)); units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 25, DateTickUnitType.MILLISECOND, 5, f1)); units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 50, DateTickUnitType.MILLISECOND, 10, f1)); units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 100, DateTickUnitType.MILLISECOND, 10, f1)); units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 250, DateTickUnitType.MILLISECOND, 10, f1)); units.add(new DateTickUnit(DateTickUnitType.MILLISECOND, 500, DateTickUnitType.MILLISECOND, 50, f1)); // seconds units.add(new DateTickUnit(DateTickUnitType.SECOND, 1, DateTickUnitType.MILLISECOND, 50, f2)); units.add(new DateTickUnit(DateTickUnitType.SECOND, 5, DateTickUnitType.SECOND, 1, f2)); units.add(new DateTickUnit(DateTickUnitType.SECOND, 10, DateTickUnitType.SECOND, 1, f2)); units.add(new DateTickUnit(DateTickUnitType.SECOND, 30, DateTickUnitType.SECOND, 5, f2)); // minutes units.add(new DateTickUnit(DateTickUnitType.MINUTE, 1, DateTickUnitType.SECOND, 5, f3)); units.add(new DateTickUnit(DateTickUnitType.MINUTE, 2, DateTickUnitType.SECOND, 10, f3)); units.add(new DateTickUnit(DateTickUnitType.MINUTE, 5, DateTickUnitType.MINUTE, 1, f3)); units.add(new DateTickUnit(DateTickUnitType.MINUTE, 10, DateTickUnitType.MINUTE, 1, f3)); units.add(new DateTickUnit(DateTickUnitType.MINUTE, 15, DateTickUnitType.MINUTE, 5, f3)); units.add(new DateTickUnit(DateTickUnitType.MINUTE, 20, DateTickUnitType.MINUTE, 5, f3)); units.add(new DateTickUnit(DateTickUnitType.MINUTE, 30, DateTickUnitType.MINUTE, 5, f3)); // hours units.add(new DateTickUnit(DateTickUnitType.HOUR, 1, DateTickUnitType.MINUTE, 5, f3)); units.add(new DateTickUnit(DateTickUnitType.HOUR, 2, DateTickUnitType.MINUTE, 10, f3)); units.add(new DateTickUnit(DateTickUnitType.HOUR, 4, DateTickUnitType.MINUTE, 30, f3)); units.add(new DateTickUnit(DateTickUnitType.HOUR, 6, DateTickUnitType.HOUR, 1, f3)); units.add(new DateTickUnit(DateTickUnitType.HOUR, 12, DateTickUnitType.HOUR, 1, f4)); // days units.add(new DateTickUnit(DateTickUnitType.DAY, 1, DateTickUnitType.HOUR, 1, f5)); units.add(new DateTickUnit(DateTickUnitType.DAY, 2, DateTickUnitType.HOUR, 1, f5)); units.add(new DateTickUnit(DateTickUnitType.DAY, 7, DateTickUnitType.DAY, 1, f5)); units.add(new DateTickUnit(DateTickUnitType.DAY, 15, DateTickUnitType.DAY, 1, f5)); // months units.add(new DateTickUnit(DateTickUnitType.MONTH, 1, DateTickUnitType.DAY, 1, f6)); units.add(new DateTickUnit(DateTickUnitType.MONTH, 2, DateTickUnitType.DAY, 1, f6)); units.add(new DateTickUnit(DateTickUnitType.MONTH, 3, DateTickUnitType.MONTH, 1, f6)); units.add(new DateTickUnit(DateTickUnitType.MONTH, 4, DateTickUnitType.MONTH, 1, f6)); units.add(new DateTickUnit(DateTickUnitType.MONTH, 6, DateTickUnitType.MONTH, 1, f6)); // years units.add(new DateTickUnit(DateTickUnitType.YEAR, 1, DateTickUnitType.MONTH, 1, f7)); units.add(new DateTickUnit(DateTickUnitType.YEAR, 2, DateTickUnitType.MONTH, 3, f7)); units.add(new DateTickUnit(DateTickUnitType.YEAR, 5, DateTickUnitType.YEAR, 1, f7)); units.add(new DateTickUnit(DateTickUnitType.YEAR, 10, DateTickUnitType.YEAR, 1, f7)); units.add(new DateTickUnit(DateTickUnitType.YEAR, 25, DateTickUnitType.YEAR, 5, f7)); units.add(new DateTickUnit(DateTickUnitType.YEAR, 50, DateTickUnitType.YEAR, 10, f7)); units.add(new DateTickUnit(DateTickUnitType.YEAR, 100, DateTickUnitType.YEAR, 20, f7)); return units; } /** * Rescales the axis to ensure that all data is visible. */ @Override protected void autoAdjustRange() { Plot plot = getPlot(); if (plot == null) { return; // no plot, no data } if (plot instanceof ValueAxisPlot) { ValueAxisPlot vap = (ValueAxisPlot) plot; Range r = vap.getDataRange(this); if (r == null) { r = new DateRange(); } long upper = this.timeline.toTimelineValue( (long) r.getUpperBound()); long lower; long fixedAutoRange = (long) getFixedAutoRange(); if (fixedAutoRange > 0.0) { lower = upper - fixedAutoRange; } else { lower = this.timeline.toTimelineValue((long) r.getLowerBound()); double range = upper - lower; long minRange = (long) getAutoRangeMinimumSize(); if (range < minRange) { long expand = (long) (minRange - range) / 2; upper = upper + expand; lower = lower - expand; } upper = upper + (long) (range * getUpperMargin()); lower = lower - (long) (range * getLowerMargin()); } upper = this.timeline.toMillisecond(upper); lower = this.timeline.toMillisecond(lower); DateRange dr = new DateRange(new Date(lower), new Date(upper)); setRange(dr, false, false); } } /** * Selects an appropriate tick value for the axis. The strategy is to * display as many ticks as possible (selected from an array of 'standard' * tick units) without the labels overlapping. * * @param g2 the graphics device. * @param dataArea the area defined by the axes. * @param edge the axis location. */ protected void selectAutoTickUnit(Graphics2D g2, Rectangle2D dataArea, RectangleEdge edge) { if (RectangleEdge.isTopOrBottom(edge)) { selectHorizontalAutoTickUnit(g2, dataArea, edge); } else if (RectangleEdge.isLeftOrRight(edge)) { selectVerticalAutoTickUnit(g2, dataArea, edge); } } /** * Selects an appropriate tick size for the axis. The strategy is to * display as many ticks as possible (selected from a collection of * 'standard' tick units) without the labels overlapping. * * @param g2 the graphics device. * @param dataArea the area defined by the axes. * @param edge the axis location. */ protected void selectHorizontalAutoTickUnit(Graphics2D g2, Rectangle2D dataArea, RectangleEdge edge) { long shift = 0; double zero = valueToJava2D(shift + 0.0, dataArea, edge); double tickLabelWidth = estimateMaximumTickLabelWidth(g2, getTickUnit()); // start with the current tick unit... TickUnitSource tickUnits = getStandardTickUnits(); TickUnit unit1 = tickUnits.getCeilingTickUnit(getTickUnit()); double x1 = valueToJava2D(shift + unit1.getSize(), dataArea, edge); double unit1Width = Math.abs(x1 - zero); // then extrapolate... double guess = (tickLabelWidth / unit1Width) * unit1.getSize(); DateTickUnit unit2 = (DateTickUnit) tickUnits.getCeilingTickUnit(guess); double x2 = valueToJava2D(shift + unit2.getSize(), dataArea, edge); double unit2Width = Math.abs(x2 - zero); tickLabelWidth = estimateMaximumTickLabelWidth(g2, unit2); if (tickLabelWidth > unit2Width) { unit2 = (DateTickUnit) tickUnits.getLargerTickUnit(unit2); } setTickUnit(unit2, false, false); } /** * Selects an appropriate tick size for the axis. The strategy is to * display as many ticks as possible (selected from a collection of * 'standard' tick units) without the labels overlapping. * * @param g2 the graphics device. * @param dataArea the area in which the plot should be drawn. * @param edge the axis location. */ protected void selectVerticalAutoTickUnit(Graphics2D g2, Rectangle2D dataArea, RectangleEdge edge) { // start with the current tick unit... TickUnitSource tickUnits = getStandardTickUnits(); double zero = valueToJava2D(0.0, dataArea, edge); // start with a unit that is at least 1/10th of the axis length double estimate1 = getRange().getLength() / 10.0; DateTickUnit candidate1 = (DateTickUnit) tickUnits.getCeilingTickUnit(estimate1); double labelHeight1 = estimateMaximumTickLabelHeight(g2, candidate1); double y1 = valueToJava2D(candidate1.getSize(), dataArea, edge); double candidate1UnitHeight = Math.abs(y1 - zero); // now extrapolate based on label height and unit height... double estimate2 = (labelHeight1 / candidate1UnitHeight) * candidate1.getSize(); DateTickUnit candidate2 = (DateTickUnit) tickUnits.getCeilingTickUnit(estimate2); double labelHeight2 = estimateMaximumTickLabelHeight(g2, candidate2); double y2 = valueToJava2D(candidate2.getSize(), dataArea, edge); double unit2Height = Math.abs(y2 - zero); // make final selection... DateTickUnit finalUnit; if (labelHeight2 < unit2Height) { finalUnit = candidate2; } else { finalUnit = (DateTickUnit) tickUnits.getLargerTickUnit(candidate2); } setTickUnit(finalUnit, false, false); } /** * Estimates the maximum width of the tick labels, assuming the specified * tick unit is used. * <P> * Rather than computing the string bounds of every tick on the axis, we * just look at two values: the lower bound and the upper bound for the * axis. These two values will usually be representative. * * @param g2 the graphics device. * @param unit the tick unit to use for calculation. * * @return The estimated maximum width of the tick labels. */ private double estimateMaximumTickLabelWidth(Graphics2D g2, DateTickUnit unit) { RectangleInsets tickLabelInsets = getTickLabelInsets(); double result = tickLabelInsets.getLeft() + tickLabelInsets.getRight(); Font tickLabelFont = getTickLabelFont(); FontRenderContext frc = g2.getFontRenderContext(); LineMetrics lm = tickLabelFont.getLineMetrics("ABCxyz", frc); if (isVerticalTickLabels()) { // all tick labels have the same width (equal to the height of // the font)... result += lm.getHeight(); } else { // look at lower and upper bounds... DateRange range = (DateRange) getRange(); Date lower = range.getLowerDate(); Date upper = range.getUpperDate(); String lowerStr, upperStr; DateFormat formatter = getDateFormatOverride(); if (formatter != null) { lowerStr = formatter.format(lower); upperStr = formatter.format(upper); } else { lowerStr = unit.dateToString(lower); upperStr = unit.dateToString(upper); } FontMetrics fm = g2.getFontMetrics(tickLabelFont); double w1 = fm.stringWidth(lowerStr); double w2 = fm.stringWidth(upperStr); result += Math.max(w1, w2); } return result; } /** * Estimates the maximum width of the tick labels, assuming the specified * tick unit is used. * <P> * Rather than computing the string bounds of every tick on the axis, we * just look at two values: the lower bound and the upper bound for the * axis. These two values will usually be representative. * * @param g2 the graphics device. * @param unit the tick unit to use for calculation. * * @return The estimated maximum width of the tick labels. */ private double estimateMaximumTickLabelHeight(Graphics2D g2, DateTickUnit unit) { RectangleInsets tickLabelInsets = getTickLabelInsets(); double result = tickLabelInsets.getTop() + tickLabelInsets.getBottom(); Font tickLabelFont = getTickLabelFont(); FontRenderContext frc = g2.getFontRenderContext(); LineMetrics lm = tickLabelFont.getLineMetrics("ABCxyz", frc); if (!isVerticalTickLabels()) { // all tick labels have the same width (equal to the height of // the font)... result += lm.getHeight(); } else { // look at lower and upper bounds... DateRange range = (DateRange) getRange(); Date lower = range.getLowerDate(); Date upper = range.getUpperDate(); String lowerStr, upperStr; DateFormat formatter = getDateFormatOverride(); if (formatter != null) { lowerStr = formatter.format(lower); upperStr = formatter.format(upper); } else { lowerStr = unit.dateToString(lower); upperStr = unit.dateToString(upper); } FontMetrics fm = g2.getFontMetrics(tickLabelFont); double w1 = fm.stringWidth(lowerStr); double w2 = fm.stringWidth(upperStr); result += Math.max(w1, w2); } return result; } /** * Calculates the positions of the tick labels for the axis, storing the * results in the tick label list (ready for drawing). * * @param g2 the graphics device. * @param state the axis state. * @param dataArea the area in which the plot should be drawn. * @param edge the location of the axis. * * @return A list of ticks. */ @Override public List<ValueTick> refreshTicks(Graphics2D g2, AxisState state, Rectangle2D dataArea, RectangleEdge edge) { List<ValueTick> result = null; if (RectangleEdge.isTopOrBottom(edge)) { result = refreshTicksHorizontal(g2, dataArea, edge); } else if (RectangleEdge.isLeftOrRight(edge)) { result = refreshTicksVertical(g2, dataArea, edge); } return result; } /** * Corrects the given tick date for the position setting. * * @param time the tick date/time. * @param unit the tick unit. * @param position the tick position. * * @return The adjusted time. */ private Date correctTickDateForPosition(Date time, DateTickUnit unit, DateTickMarkPosition position) { Date result = time; if (unit.getUnitType().equals(DateTickUnitType.MONTH)) { result = calculateDateForPosition(new Month(time, this.timeZone, this.locale), position); } else if (unit.getUnitType().equals(DateTickUnitType.YEAR)) { result = calculateDateForPosition(new Year(time, this.timeZone, this.locale), position); } return result; } /** * Recalculates the ticks for the date axis. * * @param g2 the graphics device. * @param dataArea the area in which the data is to be drawn. * @param edge the location of the axis. * * @return A list of ticks. */ protected List<ValueTick> refreshTicksHorizontal(Graphics2D g2, Rectangle2D dataArea, RectangleEdge edge) { List<ValueTick> result = new java.util.ArrayList<ValueTick>(); Font tickLabelFont = getTickLabelFont(); g2.setFont(tickLabelFont); if (isAutoTickUnitSelection()) { selectAutoTickUnit(g2, dataArea, edge); } DateTickUnit unit = getTickUnit(); Date tickDate = calculateLowestVisibleTickValue(unit); Date upperDate = getMaximumDate(); boolean hasRolled = false; while (tickDate.before(upperDate)) { // could add a flag to make the following correction optional... if (!hasRolled) { tickDate = correctTickDateForPosition(tickDate, unit, this.tickMarkPosition); } long lowestTickTime = tickDate.getTime(); long distance = unit.addToDate(tickDate, this.timeZone).getTime() - lowestTickTime; int minorTickIntervals = unit.getMinorTickIntervals(); for (int mt = 1; mt < minorTickIntervals; mt++) { long minorTickTime = lowestTickTime - distance * mt / minorTickIntervals; if (minorTickTime > 0 && getRange().contains(minorTickTime) && (!isHiddenValue(minorTickTime))) { result.add(new DateTick(TickType.MINOR, new Date(minorTickTime), "", TextAnchor.TOP_CENTER, TextAnchor.CENTER, 0.0)); } } if (!isHiddenValue(tickDate.getTime())) { // work out the value, label and position String tickLabel; DateFormat formatter = getDateFormatOverride(); if (formatter != null) { tickLabel = formatter.format(tickDate); } else { tickLabel = this.tickUnit.dateToString(tickDate); } TextAnchor anchor, rotationAnchor; double angle = 0.0; if (isVerticalTickLabels()) { anchor = TextAnchor.CENTER_RIGHT; rotationAnchor = TextAnchor.CENTER_RIGHT; if (edge == RectangleEdge.TOP) { angle = Math.PI / 2.0; } else { angle = -Math.PI / 2.0; } } else { if (edge == RectangleEdge.TOP) { anchor = TextAnchor.BOTTOM_CENTER; rotationAnchor = TextAnchor.BOTTOM_CENTER; } else { anchor = TextAnchor.TOP_CENTER; rotationAnchor = TextAnchor.TOP_CENTER; } } result.add(new DateTick(tickDate, tickLabel, anchor, rotationAnchor, angle)); hasRolled = false; long currentTickTime = tickDate.getTime(); tickDate = unit.addToDate(tickDate, this.timeZone); long nextTickTime = tickDate.getTime(); for (int mt = 1; mt < minorTickIntervals; mt++) { long minorTickTime = currentTickTime + (nextTickTime - currentTickTime) * mt / minorTickIntervals; if (getRange().contains(minorTickTime) && (!isHiddenValue(minorTickTime))) { result.add(new DateTick(TickType.MINOR, new Date(minorTickTime), "", TextAnchor.TOP_CENTER, TextAnchor.CENTER, 0.0)); } } } else { tickDate = unit.rollDate(tickDate, this.timeZone); hasRolled = true; continue; } } return result; } /** * Recalculates the ticks for the date axis. * * @param g2 the graphics device. * @param dataArea the area in which the plot should be drawn. * @param edge the location of the axis. * * @return A list of ticks. */ protected List<ValueTick> refreshTicksVertical(Graphics2D g2, Rectangle2D dataArea, RectangleEdge edge) { List<ValueTick> result = new java.util.ArrayList<ValueTick>(); Font tickLabelFont = getTickLabelFont(); g2.setFont(tickLabelFont); if (isAutoTickUnitSelection()) { selectAutoTickUnit(g2, dataArea, edge); } DateTickUnit unit = getTickUnit(); Date tickDate = calculateLowestVisibleTickValue(unit); Date upperDate = getMaximumDate(); boolean hasRolled = false; while (tickDate.before(upperDate)) { // could add a flag to make the following correction optional... if (!hasRolled) { tickDate = correctTickDateForPosition(tickDate, unit, this.tickMarkPosition); } long lowestTickTime = tickDate.getTime(); long distance = unit.addToDate(tickDate, this.timeZone).getTime() - lowestTickTime; int minorTickIntervals = unit.getMinorTickIntervals(); for (int mt = 1; mt < minorTickIntervals; mt++) { long minorTickTime = lowestTickTime - distance * mt / minorTickIntervals; if (minorTickTime > 0 && getRange().contains(minorTickTime) && (!isHiddenValue(minorTickTime))) { result.add(new DateTick(TickType.MINOR, new Date(minorTickTime), "", TextAnchor.TOP_CENTER, TextAnchor.CENTER, 0.0)); } } if (!isHiddenValue(tickDate.getTime())) { // work out the value, label and position String tickLabel; DateFormat formatter = getDateFormatOverride(); if (formatter != null) { tickLabel = formatter.format(tickDate); } else { tickLabel = this.tickUnit.dateToString(tickDate); } TextAnchor anchor, rotationAnchor; double angle = 0.0; if (isVerticalTickLabels()) { anchor = TextAnchor.BOTTOM_CENTER; rotationAnchor = TextAnchor.BOTTOM_CENTER; if (edge == RectangleEdge.LEFT) { angle = -Math.PI / 2.0; } else { angle = Math.PI / 2.0; } } else { if (edge == RectangleEdge.LEFT) { anchor = TextAnchor.CENTER_RIGHT; rotationAnchor = TextAnchor.CENTER_RIGHT; } else { anchor = TextAnchor.CENTER_LEFT; rotationAnchor = TextAnchor.CENTER_LEFT; } } result.add(new DateTick(tickDate, tickLabel, anchor, rotationAnchor, angle)); hasRolled = false; long currentTickTime = tickDate.getTime(); tickDate = unit.addToDate(tickDate, this.timeZone); long nextTickTime = tickDate.getTime(); for (int mt = 1; mt < minorTickIntervals; mt++) { long minorTickTime = currentTickTime + (nextTickTime - currentTickTime) * mt / minorTickIntervals; if (getRange().contains(minorTickTime) && (!isHiddenValue(minorTickTime))) { result.add(new DateTick(TickType.MINOR, new Date(minorTickTime), "", TextAnchor.TOP_CENTER, TextAnchor.CENTER, 0.0)); } } } else { tickDate = unit.rollDate(tickDate, this.timeZone); hasRolled = true; } } return result; } /** * Draws the axis on a Java 2D graphics device (such as the screen or a * printer). * * @param g2 the graphics device (<code>null</code> not permitted). * @param cursor the cursor location. * @param plotArea the area within which the axes and data should be * drawn (<code>null</code> not permitted). * @param dataArea the area within which the data should be drawn * (<code>null</code> not permitted). * @param edge the location of the axis (<code>null</code> not permitted). * @param plotState collects information about the plot * (<code>null</code> permitted). * * @return The axis state (never <code>null</code>). */ @Override public AxisState draw(Graphics2D g2, double cursor, Rectangle2D plotArea, Rectangle2D dataArea, RectangleEdge edge, PlotRenderingInfo plotState) { // if the axis is not visible, don't draw it... if (!isVisible()) { AxisState state = new AxisState(cursor); // even though the axis is not visible, we need to refresh ticks in // case the grid is being drawn... List<ValueTick> ticks = refreshTicks(g2, state, dataArea, edge); state.setTicks(ticks); return state; } AxisState state = drawTickMarksAndLabels(g2, cursor, plotArea, dataArea, edge); // draw the axis label (note that 'state' is passed in *and* // returned)... state = drawLabel(getLabel(), g2, plotArea, dataArea, edge, state); createAndAddEntity(cursor, state, dataArea, edge, plotState); return state; } /** * Zooms in on the current range (zoom-in stops once the axis length * reaches the equivalent of one millisecond). * * @param lowerPercent the new lower bound. * @param upperPercent the new upper bound. */ @Override public void zoomRange(double lowerPercent, double upperPercent) { double start = this.timeline.toTimelineValue( (long) getRange().getLowerBound()); double end = this.timeline.toTimelineValue( (long) getRange().getUpperBound()); double length = end - start; Range adjusted; long adjStart, adjEnd; if (isInverted()) { adjStart = (long) (start + (length * (1 - upperPercent))); adjEnd = (long) (start + (length * (1 - lowerPercent))); } else { adjStart = (long) (start + length * lowerPercent); adjEnd = (long) (start + length * upperPercent); } // when zooming to sub-millisecond ranges, it can be the case that // adjEnd == adjStart...and we can't have an axis with zero length // so we apply this instead: if (adjEnd <= adjStart) { adjEnd = adjStart + 1L; } adjusted = new DateRange(this.timeline.toMillisecond(adjStart), this.timeline.toMillisecond(adjEnd)); setRange(adjusted); } /** * Tests this axis for equality with an arbitrary object. * * @param obj the object (<code>null</code> permitted). * * @return A boolean. */ @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (!(obj instanceof DateAxis)) { return false; } DateAxis that = (DateAxis) obj; if (!ObjectUtils.equal(this.timeZone, that.timeZone)) { return false; } if (!ObjectUtils.equal(this.locale, that.locale)) { return false; } if (!ObjectUtils.equal(this.tickUnit, that.tickUnit)) { return false; } if (!ObjectUtils.equal(this.dateFormatOverride, that.dateFormatOverride)) { return false; } if (!ObjectUtils.equal(this.tickMarkPosition, that.tickMarkPosition)) { return false; } if (!ObjectUtils.equal(this.timeline, that.timeline)) { return false; } return super.equals(obj); } /** * Returns a hash code for this object. * * @return A hash code. */ @Override public int hashCode() { return super.hashCode(); } /** * Returns a clone of the object. * * @return A clone. * * @throws CloneNotSupportedException if some component of the axis does * not support cloning. */ @Override public Object clone() throws CloneNotSupportedException { DateAxis clone = (DateAxis) super.clone(); // 'dateTickUnit' is immutable : no need to clone if (this.dateFormatOverride != null) { clone.dateFormatOverride = (DateFormat) this.dateFormatOverride.clone(); } // 'tickMarkPosition' is immutable : no need to clone return clone; } }