nplot/src/DateTimeAxis.cs

648 lines
24 KiB
C#

/*
* NPlot - A charting library for .NET
*
* DateTimeAxis.cs
* Copyright (C) 2003-2006 Matt Howlett and others.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
* INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
using System;
using System.Collections;
using System.Drawing;
// TODO: More control over how labels are displayed.
// TODO: SkipWeekends property.
// TODO: Make a relative (as opposed to absolute) TimeAxis.
namespace NPlot
{
/// <summary>
/// The DateTimeAxis class
/// </summary>
public class DateTimeAxis : Axis
{
#region Clone implementation
/// <summary>
/// Deep copy of DateTimeAxis.
/// </summary>
/// <returns>A copy of the DateTimeAxis Class.</returns>
public override object Clone()
{
DateTimeAxis a = new DateTimeAxis();
// ensure that this isn't being called on a derived type. If it is, then oh no!
if (GetType() != a.GetType())
{
throw new NPlotException("Clone not defined in derived type. Help!");
}
DoClone(this, a);
return a;
}
/// <summary>
/// Helper method for Clone.
/// </summary>
/// <param name="a">The original object to clone.</param>
/// <param name="b">The cloned object.</param>
protected static void DoClone(DateTimeAxis b, DateTimeAxis a)
{
Axis.DoClone(b, a);
}
#endregion
/// <summary>
/// this gets set after a get LargeTickPositions.
/// </summary>
protected LargeTickLabelType LargeTickLabelType_;
private TimeSpan largeTickStep_ = TimeSpan.Zero;
/// <summary>
/// Constructor
/// </summary>
/// <param name="a">Axis to construct from</param>
public DateTimeAxis(Axis a)
: base(a)
{
Init();
NumberFormat = null;
}
/// <summary>
/// Default Constructor
/// </summary>
public DateTimeAxis()
{
Init();
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="worldMin">World min of axis</param>
/// <param name="worldMax">World max of axis</param>
public DateTimeAxis(double worldMin, double worldMax)
: base(worldMin, worldMax)
{
Init();
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="worldMin">World min of axis</param>
/// <param name="worldMax">World max of axis</param>
public DateTimeAxis(long worldMin, long worldMax)
: base(worldMin, worldMax)
{
Init();
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="worldMin">World min of axis</param>
/// <param name="worldMax">World max of axis</param>
public DateTimeAxis(DateTime worldMin, DateTime worldMax)
: base(worldMin.Ticks, worldMax.Ticks)
{
Init();
}
/// <summary>
/// The distance between large ticks. If this is set to Zero [default],
/// this distance will be calculated automatically.
/// </summary>
public TimeSpan LargeTickStep
{
set { largeTickStep_ = value; }
get { return largeTickStep_; }
}
private void Init()
{
}
/// <summary>
/// Draw the ticks.
/// </summary>
/// <param name="g">The drawing surface on which to draw.</param>
/// <param name="physicalMin">The minimum physical extent of the axis.</param>
/// <param name="physicalMax">The maximum physical extent of the axis.</param>
/// <param name="boundingBox">out: smallest box that completely encompasses all of the ticks and tick labels.</param>
/// <param name="labelOffset">out: a suitable offset from the axis to draw the axis label.</param>
protected override void DrawTicks(
Graphics g,
Point physicalMin,
Point physicalMax,
out object labelOffset,
out object boundingBox)
{
// TODO: Look at offset and bounding box logic again here. why temp and other vars?
Point tLabelOffset;
Rectangle tBoundingBox;
labelOffset = getDefaultLabelOffset(physicalMin, physicalMax);
boundingBox = null;
ArrayList largeTicks;
ArrayList smallTicks;
WorldTickPositions(physicalMin, physicalMax, out largeTicks, out smallTicks);
// draw small ticks.
for (int i = 0; i < smallTicks.Count; ++i)
{
DrawTick(g, (double) smallTicks[i],
SmallTickSize, "", new Point(0, 0),
physicalMin, physicalMax,
out tLabelOffset, out tBoundingBox);
// assume label offset and bounding box unchanged by small tick bounds.
}
// draw large ticks.
for (int i = 0; i < largeTicks.Count; ++i)
{
DateTime tickDate = new DateTime((long) ((double) largeTicks[i]));
string label = LargeTickLabel(tickDate);
DrawTick(g, (double) largeTicks[i],
LargeTickSize, label, new Point(0, 0),
physicalMin, physicalMax, out tLabelOffset, out tBoundingBox);
UpdateOffsetAndBounds(ref labelOffset, ref boundingBox, tLabelOffset, tBoundingBox);
}
}
/// <summary>
/// Get the label corresponding to the provided date time
/// </summary>
/// <param name="tickDate">the date time to get the label for</param>
/// <returns>label for the provided DateTime</returns>
protected virtual string LargeTickLabel(DateTime tickDate)
{
string label = "";
if (NumberFormat == null || NumberFormat == String.Empty)
{
if (LargeTickLabelType_ == LargeTickLabelType.year)
{
label = tickDate.Year.ToString();
}
else if (LargeTickLabelType_ == LargeTickLabelType.month)
{
label = tickDate.ToString("MMM");
label += " ";
label += tickDate.Year.ToString().Substring(2, 2);
}
else if (LargeTickLabelType_ == LargeTickLabelType.day)
{
label = (tickDate.Day).ToString();
label += " ";
label += tickDate.ToString("MMM");
}
else if (LargeTickLabelType_ == LargeTickLabelType.hourMinute)
{
string minutes = tickDate.Minute.ToString();
if (minutes.Length == 1)
{
minutes = "0" + minutes;
}
label = tickDate.Hour.ToString() + ":" + minutes;
}
else if (LargeTickLabelType_ == LargeTickLabelType.hourMinuteSeconds)
{
string minutes = tickDate.Minute.ToString();
string seconds = tickDate.Second.ToString();
if (seconds.Length == 1)
{
seconds = "0" + seconds;
}
if (minutes.Length == 1)
{
minutes = "0" + minutes;
}
label = tickDate.Hour.ToString() + ":" + minutes + "." + seconds;
}
}
else
{
label = tickDate.ToString(NumberFormat);
}
return label;
}
/// <summary>
/// Determines the positions, in world coordinates, of the large ticks. No
/// small tick marks are currently calculated by this method.
/// </summary>
/// <param name="physicalMin">The physical position corresponding to the world minimum of the axis.</param>
/// <param name="physicalMax">The physical position corresponding to the world maximum of the axis.</param>
/// <param name="largeTickPositions">ArrayList containing the positions of the large ticks.</param>
/// <param name="smallTickPositions">null</param>
internal override void WorldTickPositions_FirstPass(
Point physicalMin,
Point physicalMax,
out ArrayList largeTickPositions,
out ArrayList smallTickPositions
)
{
smallTickPositions = null;
largeTickPositions = new ArrayList();
const int daysInMonth = 30;
TimeSpan timeLength = new TimeSpan((long) (WorldMax - WorldMin));
DateTime worldMinDate = new DateTime((long) WorldMin);
DateTime worldMaxDate = new DateTime((long) WorldMax);
if (largeTickStep_ == TimeSpan.Zero)
{
// if less than 10 minutes, then large ticks on second spacings.
if (timeLength < new TimeSpan(0, 0, 2, 0, 0))
{
LargeTickLabelType_ = LargeTickLabelType.hourMinuteSeconds;
double secondsSkip;
if (timeLength < new TimeSpan(0, 0, 0, 10, 0))
secondsSkip = 1.0;
else if (timeLength < new TimeSpan(0, 0, 0, 20, 0))
secondsSkip = 2.0;
else if (timeLength < new TimeSpan(0, 0, 0, 50, 0))
secondsSkip = 5.0;
else if (timeLength < new TimeSpan(0, 0, 2, 30, 0))
secondsSkip = 15.0;
else
secondsSkip = 30.0;
int second = worldMinDate.Second;
second -= second%(int) secondsSkip;
DateTime currentTickDate = new DateTime(
worldMinDate.Year,
worldMinDate.Month,
worldMinDate.Day,
worldMinDate.Hour,
worldMinDate.Minute,
second, 0);
while (currentTickDate < worldMaxDate)
{
double world = currentTickDate.Ticks;
if (world >= WorldMin && world <= WorldMax)
{
largeTickPositions.Add(world);
}
currentTickDate = currentTickDate.AddSeconds(secondsSkip);
}
}
// Less than 2 hours, then large ticks on minute spacings.
else if (timeLength < new TimeSpan(0, 2, 0, 0, 0))
{
LargeTickLabelType_ = LargeTickLabelType.hourMinute;
double minuteSkip;
if (timeLength < new TimeSpan(0, 0, 10, 0, 0))
minuteSkip = 1.0;
else if (timeLength < new TimeSpan(0, 0, 20, 0, 0))
minuteSkip = 2.0;
else if (timeLength < new TimeSpan(0, 0, 50, 0, 0))
minuteSkip = 5.0;
else if (timeLength < new TimeSpan(0, 2, 30, 0, 0))
minuteSkip = 15.0;
else //( timeLength < new TimeSpan( 0,5,0,0,0) )
minuteSkip = 30.0;
int minute = worldMinDate.Minute;
minute -= minute%(int) minuteSkip;
DateTime currentTickDate = new DateTime(
worldMinDate.Year,
worldMinDate.Month,
worldMinDate.Day,
worldMinDate.Hour,
minute, 0, 0);
while (currentTickDate < worldMaxDate)
{
double world = currentTickDate.Ticks;
if (world >= WorldMin && world <= WorldMax)
{
largeTickPositions.Add(world);
}
currentTickDate = currentTickDate.AddMinutes(minuteSkip);
}
}
// Less than 2 days, then large ticks on hour spacings.
else if (timeLength < new TimeSpan(2, 0, 0, 0, 0))
{
LargeTickLabelType_ = LargeTickLabelType.hourMinute;
double hourSkip;
if (timeLength < new TimeSpan(0, 10, 0, 0, 0))
hourSkip = 1.0;
else if (timeLength < new TimeSpan(0, 20, 0, 0, 0))
hourSkip = 2.0;
else
hourSkip = 6.0;
int hour = worldMinDate.Hour;
hour -= hour%(int) hourSkip;
DateTime currentTickDate = new DateTime(
worldMinDate.Year,
worldMinDate.Month,
worldMinDate.Day,
hour, 0, 0, 0);
while (currentTickDate < worldMaxDate)
{
double world = currentTickDate.Ticks;
if (world >= WorldMin && world <= WorldMax)
{
largeTickPositions.Add(world);
}
currentTickDate = currentTickDate.AddHours(hourSkip);
}
}
// less than 5 months, then large ticks on day spacings.
else if (timeLength < new TimeSpan(daysInMonth*4, 0, 0, 0, 0))
{
LargeTickLabelType_ = LargeTickLabelType.day;
double daySkip;
if (timeLength < new TimeSpan(10, 0, 0, 0, 0))
daySkip = 1.0;
else if (timeLength < new TimeSpan(20, 0, 0, 0, 0))
daySkip = 2.0;
else if (timeLength < new TimeSpan(7*10, 0, 0, 0, 0))
daySkip = 7.0;
else
daySkip = 14.0;
DateTime currentTickDate = new DateTime(
worldMinDate.Year,
worldMinDate.Month,
worldMinDate.Day);
if (daySkip == 2.0)
{
TimeSpan timeSinceBeginning = currentTickDate - DateTime.MinValue;
if (timeSinceBeginning.Days%2 == 1)
currentTickDate = currentTickDate.AddDays(-1.0);
}
if (daySkip == 7 || daySkip == 14.0)
{
DayOfWeek dow = currentTickDate.DayOfWeek;
switch (dow)
{
case DayOfWeek.Monday:
break;
case DayOfWeek.Tuesday:
currentTickDate = currentTickDate.AddDays(-1.0);
break;
case DayOfWeek.Wednesday:
currentTickDate = currentTickDate.AddDays(-2.0);
break;
case DayOfWeek.Thursday:
currentTickDate = currentTickDate.AddDays(-3.0);
break;
case DayOfWeek.Friday:
currentTickDate = currentTickDate.AddDays(-4.0);
break;
case DayOfWeek.Saturday:
currentTickDate = currentTickDate.AddDays(-5.0);
break;
case DayOfWeek.Sunday:
currentTickDate = currentTickDate.AddDays(-6.0);
break;
}
}
if (daySkip == 14.0f)
{
TimeSpan timeSinceBeginning = currentTickDate - DateTime.MinValue;
if ((timeSinceBeginning.Days/7)%2 == 1)
{
currentTickDate = currentTickDate.AddDays(-7.0);
}
}
while (currentTickDate < worldMaxDate)
{
double world = currentTickDate.Ticks;
if (world >= WorldMin && world <= WorldMax)
{
largeTickPositions.Add(world);
}
currentTickDate = currentTickDate.AddDays(daySkip);
}
}
// else ticks on month or year spacings.
else if (timeLength >= new TimeSpan(daysInMonth*4, 0, 0, 0, 0))
{
int monthSpacing = 0;
if (timeLength.Days < daysInMonth*(12*3 + 6))
{
LargeTickLabelType_ = LargeTickLabelType.month;
if (timeLength.Days < daysInMonth*10)
monthSpacing = 1;
else if (timeLength.Days < daysInMonth*(12*2))
monthSpacing = 3;
else // if ( timeLength.Days < daysInMonth*(12*3+6) )
monthSpacing = 6;
}
else
{
LargeTickLabelType_ = LargeTickLabelType.year;
if (timeLength.Days < daysInMonth*(12*6))
monthSpacing = 12;
else if (timeLength.Days < daysInMonth*(12*12))
monthSpacing = 24;
else if (timeLength.Days < daysInMonth*(12*30))
monthSpacing = 60;
else
monthSpacing = 120;
//LargeTickLabelType_ = LargeTickLabelType.none;
}
// truncate start
DateTime currentTickDate = new DateTime(
worldMinDate.Year,
worldMinDate.Month,
1);
if (monthSpacing > 1)
{
currentTickDate = currentTickDate.AddMonths(
-(currentTickDate.Month - 1)%monthSpacing);
}
// Align on 2 or 5 year boundaries if necessary.
if (monthSpacing >= 24)
{
currentTickDate = currentTickDate.AddYears(
-(currentTickDate.Year)%(monthSpacing/12));
}
//this.firstLargeTick_ = (double)currentTickDate.Ticks;
if (LargeTickLabelType_ != LargeTickLabelType.none)
{
while (currentTickDate < worldMaxDate)
{
double world = currentTickDate.Ticks;
if (world >= WorldMin && world <= WorldMax)
{
largeTickPositions.Add(world);
}
currentTickDate = currentTickDate.AddMonths(monthSpacing);
}
}
}
}
else
{
for (DateTime date = worldMinDate; date < worldMaxDate; date += largeTickStep_)
{
largeTickPositions.Add((double) date.Ticks);
}
}
}
/// <summary>
/// Compute the small tick positions for largetick size of one or more years.
/// - inside the domain or the large tick positons, is take the mid-point of pairs of large ticks
/// - outside the large tick range, check if a half tick is inside the world min/max
/// This method works only if there are atleast 2 large ticks,
/// since we don't know if its minutes, hours, month, or yearly divisor.
/// </summary>
/// <param name="physicalMin">The physical position corresponding to the world minimum of the axis.</param>
/// <param name="physicalMax">The physical position corresponding to the world maximum of the axis.</param>
/// <param name="largeTickPositions">Read in the large tick positions</param>
/// <param name="smallTickPositions">Fill in the corresponding small tick positions</param>
/// <remarks>Added by Rosco Hill</remarks>
internal override void WorldTickPositions_SecondPass(
Point physicalMin,
Point physicalMax,
ArrayList largeTickPositions,
ref ArrayList smallTickPositions
)
{
if (largeTickPositions.Count < 2 || !(LargeTickLabelType_.Equals(LargeTickLabelType.year)))
{
smallTickPositions = new ArrayList();
;
}
else
{
smallTickPositions = new ArrayList();
double diff = 0.5*(((double) largeTickPositions[1]) - ((double) largeTickPositions[0]));
if (((double) largeTickPositions[0] - diff) > WorldMin)
{
smallTickPositions.Add((double) largeTickPositions[0] - diff);
}
for (int i = 0; i < largeTickPositions.Count - 1; i++)
{
smallTickPositions.Add(((double) largeTickPositions[i]) + diff);
}
if (((double) largeTickPositions[largeTickPositions.Count - 1] + diff) < WorldMax)
{
smallTickPositions.Add((double) largeTickPositions[largeTickPositions.Count - 1] + diff);
}
}
}
/// <summary>
/// Enumerates the different types of tick label possible.
/// </summary>
protected enum LargeTickLabelType
{
/// <summary>
/// default - no tick labels.
/// </summary>
none = 0,
/// <summary>
/// tick labels should be years
/// </summary>
year = 1,
/// <summary>
/// Tick labels should be month names
/// </summary>
month = 2,
/// <summary>
/// Tick labels should be day names
/// </summary>
day = 3,
/// <summary>
/// Tick labels should be hour / minutes.
/// </summary>
hourMinute = 4,
/// <summary>
/// tick labels should be hour / minute / second.
/// </summary>
hourMinuteSeconds = 5
}
}
}