nplot-gtk/lib/DateTimeAxis.cs

703 строки
21 KiB
C#

/*
NPlot - A charting library for .NET
DateTimeAxis.cs
Copyright (C) 2003
Matt Howlett
Redistribution and use of NPlot or parts there-of in source and
binary forms, with or without modification, are permitted provided
that the following conditions are met:
1. Re-distributions in source form must retain at the head of each
source file the above copyright notice, this list of conditions
and the following disclaimer.
2. Any product ("the product") that makes use NPlot or parts
there-of must either:
(a) allow any user of the product to obtain a complete machine-
readable copy of the corresponding source code for the
product and the version of NPlot used for a charge no more
than your cost of physically performing source distribution,
on a medium customarily used for software interchange, or:
(b) reproduce the following text in the documentation, about
box or other materials intended to be read by human users
of the product that is provided to every human user of the
product:
"This product includes software developed as
part of the NPlot library project available
from: http://www.nplot.com/"
The words "This product" may optionally be replace with
the actual name of the product.
------------------------------------------------------------------------
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.Drawing;
using System.Collections;
using System.Globalization;
// 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 (this.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
private void Init()
{
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="a">Axis to construct from</param>
public DateTimeAxis( Axis a )
: base( a )
{
this.Init();
this.NumberFormat = null;
}
/// <summary>
/// Default Constructor
/// </summary>
public DateTimeAxis()
: base()
{
this.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 )
{
this.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( (double)worldMin, (double)worldMax )
{
this.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( (double)worldMin.Ticks, (double)worldMax.Ticks )
{
this.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 = this.getDefaultLabelOffset( physicalMin, physicalMax );
boundingBox = null;
ArrayList largeTicks;
ArrayList smallTicks;
this.WorldTickPositions( physicalMin, physicalMax, out largeTicks, out smallTicks );
// draw small ticks.
for (int i=0; i<smallTicks.Count; ++i)
{
this.DrawTick( g, (double)smallTicks[i],
this.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);
this.DrawTick( g, (double)largeTicks[i],
this.LargeTickSize, label, new Point( 0, 0 ),
physicalMin, physicalMax, out tLabelOffset, out tBoundingBox );
Axis.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(this.NumberFormat == null || this.NumberFormat == String.Empty)
{
if ( this.LargeTickLabelType_ == LargeTickLabelType.year )
{
label = tickDate.Year.ToString();
}
else if ( this.LargeTickLabelType_ == LargeTickLabelType.month )
{
label = tickDate.ToString("MMM");
label += " ";
label += tickDate.Year.ToString().Substring(2,2);
}
else if ( this.LargeTickLabelType_ == LargeTickLabelType.day )
{
label = (tickDate.Day).ToString();
label += " ";
label += tickDate.ToString("MMM");
}
else if ( this.LargeTickLabelType_ == LargeTickLabelType.hourMinute )
{
string minutes = tickDate.Minute.ToString();
if (minutes.Length == 1)
{
minutes = "0" + minutes;
}
label = tickDate.Hour.ToString() + ":" + minutes;
}
else if ( this.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>
/// 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
}
/// <summary>
/// this gets set after a get LargeTickPositions.
/// </summary>
protected LargeTickLabelType LargeTickLabelType_;
/// <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)this.WorldMin );
DateTime worldMaxDate = new DateTime( (long)this.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) )
{
this.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 = (double)currentTickDate.Ticks;
if ( world >= this.WorldMin && world <= this.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) )
{
this.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 = (double)currentTickDate.Ticks;
if ( world >= this.WorldMin && world <= this.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) )
{
this.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 = (double)currentTickDate.Ticks;
if ( world >= this.WorldMin && world <= this.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))
{
this.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 = (double)currentTickDate.Ticks;
if ( world >= this.WorldMin && world <= this.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 = (double)currentTickDate.Ticks;
if ( world >= this.WorldMin && world <= this.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) > this.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) < this.WorldMax)
{
smallTickPositions.Add((double)largeTickPositions[largeTickPositions.Count - 1] + diff);
}
}
}
/// <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 TimeSpan largeTickStep_ = TimeSpan.Zero;
}
}