package com.positive.charts.axis;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Rectangle;
import com.positive.charts.axis.ticks.DateTick;
import com.positive.charts.axis.ticks.DateTickUnit;
import com.positive.charts.axis.ticks.ITick;
import com.positive.charts.axis.ticks.TickUnit;
import com.positive.charts.axis.ticks.TickUnitSource;
import com.positive.charts.axis.ticks.TickUnits;
import com.positive.charts.block.RectangleInsets;
import com.positive.charts.common.RectangleEdge;
import com.positive.charts.data.Range;
import com.positive.charts.data.time.DateRange;
import com.positive.charts.data.time.Month;
import com.positive.charts.data.time.RegularTimePeriod;
import com.positive.charts.data.time.Year;
import com.positive.charts.event.AxisChangeEvent;
import com.positive.charts.plot.Plot;
import com.positive.charts.plot.PlotRenderingInfo;
import com.positive.charts.plot.ValueAxisPlot;
import com.positive.charts.util.RectangleUtil;
import com.positive.charts.util.TextAnchor;
/**
* 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 {
/**
* 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 {
/**
* 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>.
*/
public boolean containsDomainRange(final Date from, final Date to) {
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>.
*/
public boolean containsDomainRange(final long from, final long to) {
return true;
}
/**
* Returns <code>true</code> if the timeline includes the specified
* domain value.
*
* @param date
* the date.
*
* @return <code>true</code>.
*/
public boolean containsDomainValue(final Date date) {
return true;
}
/**
* Returns <code>true</code> if the timeline includes the specified
* domain value.
*
* @param millisecond
* the millisecond.
*
* @return <code>true</code>.
*/
public boolean containsDomainValue(final long millisecond) {
return true;
}
/**
* Tests an object for equality with this instance.
*
* @param object
* the object.
*
* @return A boolean.
*/
public boolean equals(final Object object) {
if (object == null) {
return false;
}
if (object == this) {
return true;
}
if (object instanceof DefaultTimeline) {
return true;
}
return false;
}
/**
* Converts a timeline value into a millisecond (as encoded by
* <code>java.util.Date</code>).
*
* @param value
* the value.
*
* @return The millisecond.
*/
public long toMillisecond(final long value) {
return value;
}
/**
* Converts a date into a timeline value.
*
* @param date
* the domain value.
*
* @return The timeline value.
*/
public long toTimelineValue(final Date date) {
return date.getTime();
}
/**
* Converts a millisecond into a timeline value.
*
* @param millisecond
* the millisecond.
*
* @return The timeline value.
*/
public long toTimelineValue(final long millisecond) {
return millisecond;
}
}
/** 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 date tick unit. */
public static final DateTickUnit DEFAULT_DATE_TICK_UNIT = new DateTickUnit(
DateTickUnit.DAY, 1, new SimpleDateFormat());
/** The default anchor date. */
public static final Date DEFAULT_ANCHOR_DATE = new Date();
/**
* 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());
}
/**
* 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).
*
* @return A collection of standard date tick units.
*/
public static TickUnitSource createStandardDateTickUnits(final TimeZone zone) {
if (zone == null) {
throw new IllegalArgumentException("Null 'zone' argument.");
}
final TickUnits units = new TickUnits();
// date formatters
final DateFormat f1 = new SimpleDateFormat("HH:mm:ss.SSS");
final DateFormat f2 = new SimpleDateFormat("HH:mm:ss");
final DateFormat f3 = new SimpleDateFormat("HH:mm");
final DateFormat f4 = new SimpleDateFormat("d-MMM, HH:mm");
final DateFormat f5 = new SimpleDateFormat("d-MMM");
final DateFormat f6 = new SimpleDateFormat("MMM-yyyy");
final DateFormat f7 = new SimpleDateFormat("yyyy");
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(DateTickUnit.MILLISECOND, 1, f1));
units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 5,
DateTickUnit.MILLISECOND, 1, f1));
units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 10,
DateTickUnit.MILLISECOND, 1, f1));
units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 25,
DateTickUnit.MILLISECOND, 5, f1));
units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 50,
DateTickUnit.MILLISECOND, 10, f1));
units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 100,
DateTickUnit.MILLISECOND, 10, f1));
units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 250,
DateTickUnit.MILLISECOND, 10, f1));
units.add(new DateTickUnit(DateTickUnit.MILLISECOND, 500,
DateTickUnit.MILLISECOND, 50, f1));
// seconds
units.add(new DateTickUnit(DateTickUnit.SECOND, 1,
DateTickUnit.MILLISECOND, 50, f2));
units.add(new DateTickUnit(DateTickUnit.SECOND, 5, DateTickUnit.SECOND,
1, f2));
units.add(new DateTickUnit(DateTickUnit.SECOND, 10,
DateTickUnit.SECOND, 1, f2));
units.add(new DateTickUnit(DateTickUnit.SECOND, 30,
DateTickUnit.SECOND, 5, f2));
// minutes
units.add(new DateTickUnit(DateTickUnit.MINUTE, 1, DateTickUnit.SECOND,
5, f3));
units.add(new DateTickUnit(DateTickUnit.MINUTE, 2, DateTickUnit.SECOND,
10, f3));
units.add(new DateTickUnit(DateTickUnit.MINUTE, 5, DateTickUnit.MINUTE,
1, f3));
units.add(new DateTickUnit(DateTickUnit.MINUTE, 10,
DateTickUnit.MINUTE, 1, f3));
units.add(new DateTickUnit(DateTickUnit.MINUTE, 15,
DateTickUnit.MINUTE, 5, f3));
units.add(new DateTickUnit(DateTickUnit.MINUTE, 20,
DateTickUnit.MINUTE, 5, f3));
units.add(new DateTickUnit(DateTickUnit.MINUTE, 30,
DateTickUnit.MINUTE, 5, f3));
// hours
units.add(new DateTickUnit(DateTickUnit.HOUR, 1, DateTickUnit.MINUTE,
5, f3));
units.add(new DateTickUnit(DateTickUnit.HOUR, 2, DateTickUnit.MINUTE,
10, f3));
units.add(new DateTickUnit(DateTickUnit.HOUR, 4, DateTickUnit.MINUTE,
30, f3));
units.add(new DateTickUnit(DateTickUnit.HOUR, 6, DateTickUnit.HOUR, 1,
f3));
units.add(new DateTickUnit(DateTickUnit.HOUR, 12, DateTickUnit.HOUR, 1,
f4));
// days
units.add(new DateTickUnit(DateTickUnit.DAY, 1, DateTickUnit.HOUR, 1,
f5));
units.add(new DateTickUnit(DateTickUnit.DAY, 2, DateTickUnit.HOUR, 1,
f5));
units
.add(new DateTickUnit(DateTickUnit.DAY, 7, DateTickUnit.DAY, 1,
f5));
units.add(new DateTickUnit(DateTickUnit.DAY, 15, DateTickUnit.DAY, 1,
f5));
// months
units.add(new DateTickUnit(DateTickUnit.MONTH, 1, DateTickUnit.DAY, 1,
f6));
units.add(new DateTickUnit(DateTickUnit.MONTH, 2, DateTickUnit.DAY, 1,
f6));
units.add(new DateTickUnit(DateTickUnit.MONTH, 3, DateTickUnit.MONTH,
1, f6));
units.add(new DateTickUnit(DateTickUnit.MONTH, 4, DateTickUnit.MONTH,
1, f6));
units.add(new DateTickUnit(DateTickUnit.MONTH, 6, DateTickUnit.MONTH,
1, f6));
// years
units.add(new DateTickUnit(DateTickUnit.YEAR, 1, DateTickUnit.MONTH, 1,
f7));
units.add(new DateTickUnit(DateTickUnit.YEAR, 2, DateTickUnit.MONTH, 3,
f7));
units.add(new DateTickUnit(DateTickUnit.YEAR, 5, DateTickUnit.YEAR, 1,
f7));
units.add(new DateTickUnit(DateTickUnit.YEAR, 10, DateTickUnit.YEAR, 1,
f7));
units.add(new DateTickUnit(DateTickUnit.YEAR, 25, DateTickUnit.YEAR, 5,
f7));
units.add(new DateTickUnit(DateTickUnit.YEAR, 50, DateTickUnit.YEAR,
10, f7));
units.add(new DateTickUnit(DateTickUnit.YEAR, 100, DateTickUnit.YEAR,
20, f7));
return units;
}
/** 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 DateTick.MarkPosition tickMarkPosition = DateTick.MarkPosition.START;
/** A static default timeline shared by all standard DateAxis */
private static final Timeline DEFAULT_TIMELINE = new DefaultTimeline();
/** The time zone for the axis. */
private final TimeZone timeZone;
/** 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(final String label) {
this(label, TimeZone.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.
*
* @see org.jfree.chart.axis.SegmentedTimeline
*
* @param label
* the axis label (<code>null</code> permitted).
* @param zone
* the time zone.
*/
public DateAxis(final String label, final TimeZone zone) {
super(label, DateAxis.createStandardDateTickUnits(zone));
this.setTickUnit(DateAxis.DEFAULT_DATE_TICK_UNIT, false, false);
this
.setAutoRangeMinimumSize(DEFAULT_AUTO_RANGE_MINIMUM_SIZE_IN_MILLISECONDS);
this.setRange(DEFAULT_DATE_RANGE, false, false);
this.dateFormatOverride = null;
this.timeZone = zone;
this.timeline = DEFAULT_TIMELINE;
}
/**
* Rescales the axis to ensure that all data is visible.
*/
protected void autoAdjustRange() {
final Plot plot = this.getPlot();
if (plot == null) {
return; // no plot, no data
}
if (plot instanceof ValueAxisPlot) {
final ValueAxisPlot vap = (ValueAxisPlot) plot;
Range r = vap.getDataRange(this);
if (r == null) {
if (this.timeline instanceof SegmentedTimeline) {
// Timeline hasn't method getStartTime()
r = new DateRange(
((SegmentedTimeline) this.timeline).getStartTime(),
((SegmentedTimeline) this.timeline).getStartTime() + 1);
} else {
r = new DateRange();
}
}
long upper = this.timeline
.toTimelineValue((long) r.getUpperBound());
long lower;
final long fixedAutoRange = (long) this.getFixedAutoRange();
if (fixedAutoRange > 0.0) {
lower = upper - fixedAutoRange;
} else {
lower = this.timeline.toTimelineValue((long) r.getLowerBound());
final double range = upper - lower;
final long minRange = (long) this.getAutoRangeMinimumSize();
if (range < minRange) {
final long expand = (long) (minRange - range) / 2;
upper = upper + expand;
lower = lower - expand;
}
upper = upper + (long) (range * this.getUpperMargin());
lower = lower - (long) (range * this.getLowerMargin());
}
upper = this.timeline.toMillisecond(upper);
lower = this.timeline.toMillisecond(lower);
final DateRange dr = new DateRange(new Date(lower), new Date(upper));
this.setRange(dr, false, false);
}
}
/**
* 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(final RegularTimePeriod period,
final DateTick.MarkPosition position) {
if (position == null) {
throw new IllegalArgumentException("Null 'position' argument.");
}
Date result = null;
if (position == DateTick.MarkPosition.START) {
result = new Date(period.getFirstMillisecond());
} else if (position == DateTick.MarkPosition.MIDDLE) {
result = new Date(period.getMiddleMillisecond());
} else if (position == DateTick.MarkPosition.END) {
result = new Date(period.getLastMillisecond());
}
return result;
}
/**
* 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(final DateTickUnit unit) {
return this.previousStandardDate(this.getMaximumDate(), unit);
}
/**
* 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(final DateTickUnit unit) {
return this.nextStandardDate(this.getMinimumDate(), unit);
}
/**
* Configures the axis to work with the specified plot. If the axis has
* auto-scaling, then sets the maximum and minimum values.
*/
public void configure() {
if (this.isAutoRange()) {
this.autoAdjustRange();
}
}
/**
* 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(final Date date, final Rectangle area,
final RectangleEdge edge) {
final double value = date.getTime();
return this.valueToJava2D(value, area, edge);
}
/**
* 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>).
*/
public AxisState draw(final GC g2, final double cursor,
final Rectangle plotArea, final Rectangle dataArea,
final RectangleEdge edge, final PlotRenderingInfo plotState) {
// if the axis is not visible, don't draw it...
if (!this.isVisible()) {
final AxisState state = new AxisState(cursor);
// even though the axis is not visible, we need to refresh ticks in
// case the grid is being drawn...
final List ticks = this.refreshTicks(g2, state, dataArea, edge);
state.setTicks(ticks);
return state;
}
// draw the tick marks and labels...
AxisState state = this.drawTickMarksAndLabels(g2, cursor, plotArea,
dataArea, edge);
// draw the axis label (note that 'state' is passed in *and*
// returned)...
state = this.drawLabel(this.getLabel(), g2, plotArea, dataArea, edge,
state);
return state;
}
/**
* 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(final GC g2,
final DateTickUnit unit) {
final RectangleInsets tickLabelInsets = this.getTickLabelInsets();
double result = tickLabelInsets.getTop() + tickLabelInsets.getBottom();
final Font tickLabelFont = this.getTickLabelFont();
g2.setFont(tickLabelFont);
final FontMetrics lm = g2.getFontMetrics();
if (!this.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...
final DateRange range = (DateRange) this.getRange();
final Date lower = range.getLowerDate();
final Date upper = range.getUpperDate();
String lowerStr = null;
String upperStr = null;
final DateFormat formatter = this.getDateFormatOverride();
if (formatter != null) {
lowerStr = formatter.format(lower);
upperStr = formatter.format(upper);
} else {
lowerStr = unit.dateToString(lower);
upperStr = unit.dateToString(upper);
}
final double w1 = g2.textExtent(lowerStr).x;
final double w2 = g2.textExtent(upperStr).x;
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 estimateMaximumTickLabelWidth(final GC g2,
final DateTickUnit unit) {
final RectangleInsets tickLabelInsets = this.getTickLabelInsets();
double result = tickLabelInsets.getLeft() + tickLabelInsets.getRight();
final Font tickLabelFont = this.getTickLabelFont();
g2.setFont(tickLabelFont);
// FontRenderContext frc = g2.getFontRenderContext();
// LineMetrics lm = tickLabelFont.getLineMetrics("ABCxyz", frc);
final FontMetrics lm = g2.getFontMetrics();
if (this.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...
final DateRange range = (DateRange) this.getRange();
final Date lower = range.getLowerDate();
final Date upper = range.getUpperDate();
String lowerStr = null;
String upperStr = null;
final DateFormat formatter = this.getDateFormatOverride();
if (formatter != null) {
lowerStr = formatter.format(lower);
upperStr = formatter.format(upper);
} else {
lowerStr = unit.dateToString(lower);
upperStr = unit.dateToString(upper);
}
final double w1 = g2.textExtent(lowerStr).x;
final double w2 = g2.textExtent(upperStr).x;
result += Math.max(w1, w2);
}
return result;
}
/**
* 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;
}
/**
* Returns the latest date visible on the axis.
*
* @return The date.
*/
public Date getMaximumDate() {
Date result = null;
final Range range = this.getRange();
if (range instanceof DateRange) {
final DateRange r = (DateRange) range;
result = r.getUpperDate();
} else {
result = new Date((long) range.getUpperBound());
}
return result;
}
/**
* Returns the earliest date visible on the axis.
*
* @return The date.
*/
public Date getMinimumDate() {
Date result = null;
final Range range = this.getRange();
if (range instanceof DateRange) {
final DateRange r = (DateRange) range;
result = r.getLowerDate();
} else {
result = new Date((long) range.getLowerBound());
}
return result;
}
/**
* Returns the tick mark position (start, middle or end of the time period).
*
* @return The position (never <code>null</code>).
*/
public DateTick.MarkPosition getTickMarkPosition() {
return this.tickMarkPosition;
}
/**
* Returns the tick unit for the axis.
*
* @return The tick unit (possibly <code>null</code>).
*/
public DateTickUnit getTickUnit() {
return this.tickUnit;
}
/**
* Returns the underlying timeline used by this axis.
*
* @return The timeline.
*/
public Timeline getTimeline() {
return this.timeline;
}
private boolean hasDuplicates() {
new java.util.ArrayList();
final DateTickUnit unit = this.getTickUnit();
Date tickDate = this.calculateLowestVisibleTickValue(unit);
String previousTickLabel = "";
String tickLabel = "";
final Date upperDate = this.getMaximumDate();
while (tickDate.before(upperDate)) {
if (!this.isHiddenValue(tickDate.getTime())) {
final DateFormat formatter = this.getDateFormatOverride();
if (formatter != null) {
tickLabel = formatter.format(tickDate);
} else {
tickLabel = this.tickUnit.dateToString(tickDate);
}
// TODO: Code below fixes duplicated labels problem
if (tickLabel.equals(previousTickLabel)) {
tickLabel = "";
return true;
} else {
previousTickLabel = tickLabel;
}
tickDate = unit.addToDate(tickDate);
} else {
tickDate = unit.rollDate(tickDate);
}
}
return false;
}
/**
* Returns a hash code for this object.
*
* @return A hash code.
*/
public int hashCode() {
if (this.getLabel() != null) {
return this.getLabel().hashCode();
} else {
return 0;
}
}
/**
* 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(final long millis) {
return (!this.timeline.containsDomainValue(new Date(millis)));
}
/**
* 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.
*/
public double java2DToValue(final double java2DValue, final Rectangle area,
final RectangleEdge edge) {
final DateRange range = (DateRange) this.getRange();
final double axisMin = this.timeline.toTimelineValue(range
.getLowerDate());
final double axisMax = this.timeline.toTimelineValue(range
.getUpperDate());
double min = 0.0;
double max = 0.0;
if (edge.isTopOrBottom()) {
min = area.x;
max = RectangleUtil.getMaxX(area);
} else if (edge.isLeftOrRight()) {
min = RectangleUtil.getMaxY(area);
max = area.y;
}
double result;
if (this.isInverted()) {
result = axisMax
- ((java2DValue - min) / (max - min) * (axisMax - axisMin));
} else {
result = axisMin
+ ((java2DValue - min) / (max - min) * (axisMax - axisMin));
}
return this.timeline.toMillisecond((long) 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(final Date date, final DateTickUnit unit) {
final Date previous = this.previousStandardDate(date, unit);
final Calendar calendar = Calendar.getInstance();
calendar.setTime(previous);
calendar.add(unit.getCalendarField(), unit.getCount());
return calendar.getTime();
}
/**
* 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(final Date date, final DateTickUnit unit) {
int milliseconds;
int seconds;
int minutes;
int hours;
int days;
int months;
int years;
final Calendar calendar = Calendar.getInstance(this.timeZone);
calendar.setTime(date);
final int count = unit.getCount();
final int current = calendar.get(unit.getCalendarField());
final int value = count * (current / count);
switch (unit.getUnit()) {
case (DateTickUnit.MILLISECOND):
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);
return calendar.getTime();
case (DateTickUnit.SECOND):
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 == DateTick.MarkPosition.START) {
milliseconds = 0;
} else if (this.tickMarkPosition == DateTick.MarkPosition.MIDDLE) {
milliseconds = 500;
} else {
milliseconds = 999;
}
calendar.set(Calendar.MILLISECOND, milliseconds);
calendar.set(years, months, days, hours, minutes, value);
return calendar.getTime();
case (DateTickUnit.MINUTE):
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 == DateTick.MarkPosition.START) {
seconds = 0;
} else if (this.tickMarkPosition == DateTick.MarkPosition.MIDDLE) {
seconds = 30;
} else {
seconds = 59;
}
calendar.clear(Calendar.MILLISECOND);
calendar.set(years, months, days, hours, value, seconds);
return calendar.getTime();
case (DateTickUnit.HOUR):
years = calendar.get(Calendar.YEAR);
months = calendar.get(Calendar.MONTH);
days = calendar.get(Calendar.DATE);
if (this.tickMarkPosition == DateTick.MarkPosition.START) {
minutes = 0;
seconds = 0;
} else if (this.tickMarkPosition == DateTick.MarkPosition.MIDDLE) {
minutes = 30;
seconds = 0;
} else {
minutes = 59;
seconds = 59;
}
calendar.clear(Calendar.MILLISECOND);
calendar.set(years, months, days, value, minutes, seconds);
return calendar.getTime();
case (DateTickUnit.DAY):
years = calendar.get(Calendar.YEAR);
months = calendar.get(Calendar.MONTH);
if (this.tickMarkPosition == DateTick.MarkPosition.START) {
hours = 0;
minutes = 0;
seconds = 0;
} else if (this.tickMarkPosition == DateTick.MarkPosition.MIDDLE) {
hours = 12;
minutes = 0;
seconds = 0;
} else {
hours = 23;
minutes = 59;
seconds = 59;
}
calendar.clear(Calendar.MILLISECOND);
calendar.set(years, months, value, hours, 0, 0);
// long result = calendar.getTimeInMillis();
// won't work with JDK 1.3
final long result = calendar.getTime().getTime();
if (result > date.getTime()) {
calendar.set(years, months, value - 1, hours, 0, 0);
}
return calendar.getTime();
case (DateTickUnit.MONTH):
years = calendar.get(Calendar.YEAR);
calendar.clear(Calendar.MILLISECOND);
calendar.set(years, value, 1, 0, 0, 0);
Month month = new Month(calendar.getTime());
Date standardDate = this.calculateDateForPosition(month,
this.tickMarkPosition);
final long millis = standardDate.getTime();
if (millis > date.getTime()) {
month = (Month) month.previous();
standardDate = this.calculateDateForPosition(month,
this.tickMarkPosition);
}
return standardDate;
case (DateTickUnit.YEAR):
if (this.tickMarkPosition == DateTick.MarkPosition.START) {
months = 0;
days = 1;
} else if (this.tickMarkPosition == DateTick.MarkPosition.MIDDLE) {
months = 6;
days = 1;
} else {
months = 11;
days = 31;
}
calendar.clear(Calendar.MILLISECOND);
calendar.set(value, months, days, 0, 0, 0);
return calendar.getTime();
default:
return null;
}
}
/**
* 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.
*/
public List refreshTicks(final GC g2, final AxisState state,
final Rectangle dataArea, final RectangleEdge edge) {
List result = null;
if (edge.isTopOrBottom()) {
result = this.refreshTicksHorizontal(g2, dataArea, edge);
} else if (edge.isLeftOrRight()) {
result = this.refreshTicksVertical(g2, dataArea, edge);
}
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 refreshTicksHorizontal(final GC g2,
final Rectangle dataArea, final RectangleEdge edge) {
final List result = new java.util.ArrayList();
final Font tickLabelFont = this.getTickLabelFont();
g2.setFont(tickLabelFont);
if (this.isAutoTickUnitSelection()) {
this.selectAutoTickUnit(g2, dataArea, edge);
}
final DateTickUnit unit = this.getTickUnit();
Date tickDate = this.calculateLowestVisibleTickValue(unit);
final Date upperDate = this.getMaximumDate();
// float lastX = Float.MIN_VALUE;
String previousTickLabel = "";
DateFormat formatter = this.getDateFormatOverride();
if ((formatter != null) && this.hasDuplicates()) {
previousTickLabel = formatter.format(tickDate);
}
while (tickDate.before(upperDate)) {
if (!this.isHiddenValue(tickDate.getTime())) {
// work out the value, label and position
String tickLabel;
formatter = this.getDateFormatOverride();
if (formatter != null) {
tickLabel = formatter.format(tickDate);
} else {
tickLabel = this.tickUnit.dateToString(tickDate);
}
// TODO: Code below fixes duplicated labels problem
if (tickLabel.equals(previousTickLabel)) {
tickLabel = "";
} else {
previousTickLabel = tickLabel;
}
TextAnchor anchor = null;
TextAnchor rotationAnchor = null;
double angle = 0.0;
if (this.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;
}
}
final ITick tick = new DateTick(tickDate, tickLabel, anchor,
rotationAnchor, angle);
result.add(tick);
tickDate = unit.addToDate(tickDate);
} else {
tickDate = unit.rollDate(tickDate);
continue;
}
// could add a flag to make the following correction optional...
switch (unit.getUnit()) {
case (DateTickUnit.MILLISECOND):
case (DateTickUnit.SECOND):
case (DateTickUnit.MINUTE):
case (DateTickUnit.HOUR):
case (DateTickUnit.DAY):
break;
case (DateTickUnit.MONTH):
tickDate = this.calculateDateForPosition(new Month(tickDate),
this.tickMarkPosition);
break;
case (DateTickUnit.YEAR):
tickDate = this.calculateDateForPosition(new Year(tickDate),
this.tickMarkPosition);
break;
default:
break;
}
}
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 refreshTicksVertical(final GC g2, final Rectangle dataArea,
final RectangleEdge edge) {
final List result = new java.util.ArrayList();
final Font tickLabelFont = this.getTickLabelFont();
g2.setFont(tickLabelFont);
if (this.isAutoTickUnitSelection()) {
this.selectAutoTickUnit(g2, dataArea, edge);
}
final DateTickUnit unit = this.getTickUnit();
Date tickDate = this.calculateLowestVisibleTickValue(unit);
// Date upperDate = calculateHighestVisibleTickValue(unit);
final Date upperDate = this.getMaximumDate();
while (tickDate.before(upperDate)) {
if (!this.isHiddenValue(tickDate.getTime())) {
// work out the value, label and position
String tickLabel;
final DateFormat formatter = this.getDateFormatOverride();
if (formatter != null) {
tickLabel = formatter.format(tickDate);
} else {
tickLabel = this.tickUnit.dateToString(tickDate);
}
TextAnchor anchor = null;
TextAnchor rotationAnchor = null;
double angle = 0.0;
if (this.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;
}
}
final ITick tick = new DateTick(tickDate, tickLabel, anchor,
rotationAnchor, angle);
result.add(tick);
tickDate = unit.addToDate(tickDate);
} else {
tickDate = unit.rollDate(tickDate);
}
}
return result;
}
/**
* 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(final GC g2, final Rectangle dataArea,
final RectangleEdge edge) {
if (edge.isTopOrBottom()) {
this.selectHorizontalAutoTickUnit(g2, dataArea, edge);
} else if (edge.isLeftOrRight()) {
this.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(final GC g2,
final Rectangle dataArea, final RectangleEdge edge) {
long shift = 0;
if (this.timeline instanceof SegmentedTimeline) {
shift = ((SegmentedTimeline) this.timeline).getStartTime();
}
final double zero = this.valueToJava2D(shift + 0.0, dataArea, edge);
double tickLabelWidth = this.estimateMaximumTickLabelWidth(g2, this
.getTickUnit());
// start with the current tick unit...
final TickUnitSource tickUnits = this.getStandardTickUnits();
final TickUnit unit1 = tickUnits.getCeilingTickUnit(this.getTickUnit());
final double x1 = this.valueToJava2D(shift + unit1.getSize(), dataArea,
edge);
final double unit1Width = Math.abs(x1 - zero);
// then extrapolate...
final double guess = (tickLabelWidth / unit1Width) * unit1.getSize();
DateTickUnit unit2 = (DateTickUnit) tickUnits.getCeilingTickUnit(guess);
final double x2 = this.valueToJava2D(shift + unit2.getSize(), dataArea,
edge);
final double unit2Width = Math.abs(x2 - zero);
tickLabelWidth = this.estimateMaximumTickLabelWidth(g2, unit2);
if (tickLabelWidth > unit2Width) {
unit2 = (DateTickUnit) tickUnits.getLargerTickUnit(unit2);
}
this.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(final GC g2,
final Rectangle dataArea, final RectangleEdge edge) {
// start with the current tick unit...
final TickUnitSource tickUnits = this.getStandardTickUnits();
final double zero = this.valueToJava2D(0.0, dataArea, edge);
// start with a unit that is at least 1/10th of the axis length
final double estimate1 = this.getRange().getLength() / 10.0;
final DateTickUnit candidate1 = (DateTickUnit) tickUnits
.getCeilingTickUnit(estimate1);
final double labelHeight1 = this.estimateMaximumTickLabelHeight(g2,
candidate1);
final double y1 = this.valueToJava2D(candidate1.getSize(), dataArea,
edge);
final double candidate1UnitHeight = Math.abs(y1 - zero);
// now extrapolate based on label height and unit height...
final double estimate2 = (labelHeight1 / candidate1UnitHeight)
* candidate1.getSize();
final DateTickUnit candidate2 = (DateTickUnit) tickUnits
.getCeilingTickUnit(estimate2);
final double labelHeight2 = this.estimateMaximumTickLabelHeight(g2,
candidate2);
final double y2 = this.valueToJava2D(candidate2.getSize(), dataArea,
edge);
final double unit2Height = Math.abs(y2 - zero);
// make final selection...
DateTickUnit finalUnit;
if (labelHeight2 < unit2Height) {
finalUnit = candidate2;
} else {
finalUnit = (DateTickUnit) tickUnits.getLargerTickUnit(candidate2);
}
this.setTickUnit(finalUnit, false, false);
}
/**
* Sets the date format override. 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(final DateFormat formatter) {
this.dateFormatOverride = formatter;
this.notifyListeners(new AxisChangeEvent(this));
}
/**
* Sets the maximum date visible on the axis. An {@link AxisChangeEvent} is
* sent to all registered listeners.
*
* @param maximumDate
* the date (<code>null</code> not permitted).
*/
public void setMaximumDate(final Date maximumDate) {
this.setRange(new DateRange(this.getMinimumDate(), maximumDate), true,
false);
this.notifyListeners(new AxisChangeEvent(this));
}
/**
* Sets the minimum date visible on the axis and sends an
* {@link AxisChangeEvent} to all registered listeners.
*
* @param date
* the date (<code>null</code> not permitted).
*/
public void setMinimumDate(final Date date) {
this.setRange(new DateRange(date, this.getMaximumDate()), true, false);
this.notifyListeners(new AxisChangeEvent(this));
}
/**
* 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(final Date lower, final Date upper) {
if (lower.getTime() >= upper.getTime()) {
throw new IllegalArgumentException("Requires 'lower' < 'upper'.");
}
this.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.
*/
public void setRange(final double lower, final double upper) {
if (lower >= upper) {
throw new IllegalArgumentException("Requires 'lower' < 'upper'.");
}
this.setRange(new DateRange(lower, upper));
}
/**
* 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).
*/
public void setRange(final Range range) {
this.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.
*/
public void setRange(Range range, final boolean turnOffAutoRange,
final boolean notify) {
if (range == null) {
throw new IllegalArgumentException("Null 'range' argument.");
}
// 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 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(final DateTick.MarkPosition position) {
if (position == null) {
throw new IllegalArgumentException("Null 'position' argument.");
}
this.tickMarkPosition = position;
this.notifyListeners(new AxisChangeEvent(this));
}
/**
* 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.
*/
public void setTickUnit(final DateTickUnit unit) {
this.setTickUnit(unit, true, true);
}
/**
* Sets the tick unit attribute without any other side effects.
*
* @param unit
* the new tick unit.
* @param notify
* notify registered listeners?
* @param turnOffAutoSelection
* turn off auto selection?
*/
public void setTickUnit(final DateTickUnit unit, final boolean notify,
final boolean turnOffAutoSelection) {
this.tickUnit = unit;
if (turnOffAutoSelection) {
this.setAutoTickUnitSelection(false, false);
}
if (notify) {
this.notifyListeners(new AxisChangeEvent(this));
}
}
/**
* Sets the underlying timeline to use for this axis.
* <P>
* If the timeline is changed, an {@link AxisChangeEvent} is sent to all
* registered listeners.
*
* @param timeline
* the timeline.
*/
public void setTimeline(final Timeline timeline) {
if (this.timeline != timeline) {
this.timeline = timeline;
this.notifyListeners(new AxisChangeEvent(this));
}
}
/**
* 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.
*/
public double valueToJava2D(double value, final Rectangle area,
final RectangleEdge edge) {
value = this.timeline.toTimelineValue((long) value);
final DateRange range = (DateRange) this.getRange();
final double axisMin = this.timeline.toTimelineValue(range
.getLowerDate());
final double axisMax = this.timeline.toTimelineValue(range
.getUpperDate());
double result = 0.0;
if (edge.isTopOrBottom()) {
final double minX = RectangleUtil.getMinX(area);
final double maxX = RectangleUtil.getMaxX(area);
if (this.isInverted()) {
result = maxX + ((value - axisMin) / (axisMax - axisMin))
* (minX - maxX);
} else {
result = minX + ((value - axisMin) / (axisMax - axisMin))
* (maxX - minX);
}
} else if (edge.isLeftOrRight()) {
final double minY = RectangleUtil.getMinY(area);
final double maxY = RectangleUtil.getMaxY(area);
if (this.isInverted()) {
result = minY
+ (((value - axisMin) / (axisMax - axisMin)) * (maxY - minY));
} else {
result = maxY
- (((value - axisMin) / (axisMax - axisMin)) * (maxY - minY));
}
}
return result;
}
/**
* Zooms in on the current range.
*
* @param lowerPercent
* the new lower bound.
* @param upperPercent
* the new upper bound.
*/
public void zoomRange(final double lowerPercent, final double upperPercent) {
final double start = this.timeline.toTimelineValue((long) this
.getRange().getLowerBound());
final double length = (this.timeline.toTimelineValue((long) this
.getRange().getUpperBound()) - this.timeline
.toTimelineValue((long) this.getRange().getLowerBound()));
Range adjusted = null;
if (this.isInverted()) {
adjusted = new DateRange(
this.timeline
.toMillisecond((long) (start + (length * (1 - upperPercent)))),
this.timeline
.toMillisecond((long) (start + (length * (1 - lowerPercent)))));
} else {
adjusted = new DateRange(this.timeline
.toMillisecond((long) (start + length * lowerPercent)),
this.timeline.toMillisecond((long) (start + length
* upperPercent)));
}
this.setRange(adjusted);
}
}