How to handle daylight saving time shift
Posted: Wed Sep 21, 2016 3:01 pm
Hello all,
The local clock shift due to daylight saving, is common problem in spring and autumn, every year.
Depending on local machine Regional settings, day light saving is enabled or disabled.
There's probably as many preferences what should happen when DST kicks as they are users. Most common ones are handled as follows:
- Leaping with a clear gap or event spawning new series
- Making data seamless and change the old time stamps
- Adding marker when DST occurs
- Using CustomAxisTicks or xAxis.FormatValueLabel to manipulate with the axis labels.
I'll be focusing in Making data seamless and change the old time stamps functionality here.
So, what happens when DST occurs when LightningChart is monitoring data in real-time? When using XAxis with ValueType = DateTime, and getting local timestamps e.g. with DateTime.Now, and converting those DateTimes to X axis values with xAxis.DateTimeToAxisValue, the monitoring will leap either one hour in the future or in the past.
PointLineSeries data must be ascending i.e. Points[i+1].X >= Points.X. When DST shifts clock 1 hour backwards, DateTimeToAxisValue will return smaller values than it already returned. If appending these values after existing points, rendering will be quite messed up as it doesn't follow the ascending rule. Therefore, old data points have to be shifted, decreasing their X values by 3600 seconds.
When DST kicks in and shifts clock 1 hour forwards, the DateTimeToAxisValues will have about 3600 seconds larger values than before, making it leap with long stroke forward. Therefore, incrementing old data point X values by 3600 is needed.
The application should subscribe to Microsoft.Win32.SystemEvents.TimeChanged event to get notified of clock change.
I modified WinForms demo application's ExampleTemperatureGraph.cs to handle timeshift.
The code is presented as follows. Please note especially SystemEvents_TimeChanged event handler ApplyDataPointsFix code.
When setting PC clock to March 26, 2017, 2:59:30 AM, and running the demo, the data is smooth over the transition, and labels are updated accordingly based on the new time (3AM -> 4 AM) switch.
The local clock shift due to daylight saving, is common problem in spring and autumn, every year.
Depending on local machine Regional settings, day light saving is enabled or disabled.
There's probably as many preferences what should happen when DST kicks as they are users. Most common ones are handled as follows:
- Leaping with a clear gap or event spawning new series
- Making data seamless and change the old time stamps
- Adding marker when DST occurs
- Using CustomAxisTicks or xAxis.FormatValueLabel to manipulate with the axis labels.
I'll be focusing in Making data seamless and change the old time stamps functionality here.
So, what happens when DST occurs when LightningChart is monitoring data in real-time? When using XAxis with ValueType = DateTime, and getting local timestamps e.g. with DateTime.Now, and converting those DateTimes to X axis values with xAxis.DateTimeToAxisValue, the monitoring will leap either one hour in the future or in the past.
PointLineSeries data must be ascending i.e. Points[i+1].X >= Points.X. When DST shifts clock 1 hour backwards, DateTimeToAxisValue will return smaller values than it already returned. If appending these values after existing points, rendering will be quite messed up as it doesn't follow the ascending rule. Therefore, old data points have to be shifted, decreasing their X values by 3600 seconds.
When DST kicks in and shifts clock 1 hour forwards, the DateTimeToAxisValues will have about 3600 seconds larger values than before, making it leap with long stroke forward. Therefore, incrementing old data point X values by 3600 is needed.
The application should subscribe to Microsoft.Win32.SystemEvents.TimeChanged event to get notified of clock change.
I modified WinForms demo application's ExampleTemperatureGraph.cs to handle timeshift.
The code is presented as follows. Please note especially SystemEvents_TimeChanged event handler ApplyDataPointsFix code.
Code: Select all
using System;
using System.Drawing;
using System.Windows.Forms;
using System.Threading.Tasks;
using Arction.WinForms.Charting;
using Arction.WinForms.Charting.Axes;
using Arction.WinForms.Charting.SeriesXY;
namespace DemoAppWinForms
{
/// <summary>
/// Simple temperature monitoring example
/// </summary>
public partial class ExampleTemperatureGraph : RealtimeExample
{
private LightningChartUltimate _chart;
//Latest value of series x, used to set x-axis scrolling position properly
private double _previousX;
private double _previousTemperature;
private bool _addNaN = false;
private DateTime _previousDateTime;
//Number randomizer
private Random _random;
/// <summary>
/// Constructor.
/// </summary>
public ExampleTemperatureGraph()
{
_previousTemperature = 50.0;
_previousX = 0;
_random = new Random((int)DateTime.Now.Ticks);
InitializeComponent();
CreateChart();
Microsoft.Win32.SystemEvents.TimeChanged += SystemEvents_TimeChanged;
}
void SystemEvents_TimeChanged(object sender, EventArgs e)
{
if (_previousX != 0)
{
System.Diagnostics.Debug.WriteLine("*** DST OCCURRED ***");
double xNow = _chart.ViewXY.XAxes[0].DateTimeToAxisValue(DateTime.Now);
if((_previousX - xNow) > 50*60)
{
//Daylight saving occurred, -1 hours. (in autumn in Europe)
ApplyDataPointsFix(-3600);
}
else
{
//Daylight saving occurred, +1 hours (in spring in Europe)
ApplyDataPointsFix(3600);
}
}
}
void ApplyDataPointsFix(double deltaSeconds)
{
_chart.BeginUpdate();
System.Diagnostics.Debug.WriteLine("DST fix: " + deltaSeconds.ToString("0.00"));
foreach (PointLineSeries pls in _chart.ViewXY.PointLineSeries)
{
int pointCount = pls.PointCount;
SeriesPoint[] points = pls.Points;
for (int i = 0; i < pointCount; i++)
{
points[i].X += deltaSeconds;
}
double xPrev = points[0].X;
int countToKeep = 1;
for (int i = 1; i < pointCount; i++)
{
if (deltaSeconds < 0)
{
if (points[i].X < xPrev + 3599) //Drop points that were possibly generated before TimeChanged event notification came from the system (it comes slightly delayed).
{
countToKeep++;
}
}
else if (deltaSeconds > 0)
{
countToKeep++;
}
xPrev = points[i].X;
}
Array.Resize(ref points, countToKeep);
pls.Points = points;
}
_chart.ViewXY.XAxes[0].SetRange(_chart.ViewXY.XAxes[0].Minimum + deltaSeconds, _chart.ViewXY.XAxes[0].Maximum + deltaSeconds);
_chart.ViewXY.XAxes[0].ScrollPosition += deltaSeconds;
_previousX += deltaSeconds;
_chart.EndUpdate();
}
internal override bool IsRunning
{
get
{
return timer1.Enabled;
}
}
public override void Start()
{
timer1.Start();
base.RaiseStartedEvent();
}
public override void Stop()
{
timer1.Stop();
base.RaiseStoppedEvent();
}
/// <summary>
/// Create chart.
/// </summary>
private void CreateChart()
{
//Create new chart
LightningChartUltimate chart = new LightningChartUltimate(LicenseKeys.LicenseKeyStrings.LightningChartUltimate);
//Assign to member
_chart = chart;
//Disable rendering, strongly recommended before updating chart properties
chart.BeginUpdate();
//Reduce memory usage and increase performance. Destroys out-scrolled data.
chart.ViewXY.DropOldSeriesData = true;
//Chart parent must be set
chart.Parent = this;
//Chart name
chart.Name = "Temperature measurement chart";
//Fill parent area with chart
chart.Dock = DockStyle.Fill;
//Set up x-axis properties
AxisX xAxis = chart.ViewXY.XAxes[0];
xAxis.ValueType = AxisValueType.DateTime;
DateTime now = DateTime.Now;
double dMinX = xAxis.DateTimeToAxisValue(now);
double dMaxX = xAxis.DateTimeToAxisValue(now) + 30;
xAxis.SetRange(dMinX, dMaxX);
xAxis.Title.Visible = true;
xAxis.Title.Text = "Time";
xAxis.AutoFormatLabels = false;
xAxis.LabelsTimeFormat = "dd/MM/yyyy\nHH:mm.ss";
xAxis.LabelsAngle = 90;
xAxis.MajorGrid.Color = Color.FromArgb(50, Color.White);
//Set graph background gradient type to Linear with direction to down
//Set gradient start color to DimGray and gradient end color to Black
chart.ViewXY.GraphBackground.Color = Color.DimGray;
chart.ViewXY.GraphBackground.GradientColor = Color.Black;
chart.ViewXY.GraphBackground.GradientDirection = 270;
chart.ViewXY.GraphBackground.GradientFill = GradientFill.Linear;
//Use the default Y axis
//Set title and major grid
AxisY yAxis = chart.ViewXY.YAxes[0];
yAxis.Title.Text = "Temperature / °C";
yAxis.MajorGrid.Visible = false;
//Set range
yAxis.SetRange(0, 100);
//Add a PointLineSeries. PointLineSeries is performance optimized
//measurement data series where X interval varies.
PointLineSeries series = new PointLineSeries(chart.ViewXY, xAxis, yAxis);
series.LineStyle.Color = Color.Yellow;
series.MouseInteraction = false;
// Enable DataBreaking, with data-gap-defining value = NaN
series.DataBreaking.Enabled = true;
//Add series to ViewXY's collection
chart.ViewXY.PointLineSeries.Add(series);
//Don't show legendbox
chart.ViewXY.LegendBox.Visible = false;
chart.ViewXY.DropOldSeriesData = false;
//Show Y axis grid strips
chart.ViewXY.AxisLayout.AxisGridStrips = XYAxisGridStrips.Y;
//Allow chart rendering
chart.EndUpdate();
base.RaiseChartsCreatedEvent();
}
private void timer1_Tick(object sender, EventArgs e)
{
if (_chart == null)
return;
//Disable updates, to prevent several extra refreshes
_chart.BeginUpdate();
//Array for 1 point
SeriesPoint[] points = new SeriesPoint[1];
//Convert 'Now' to X value
DateTime dt = DateTime.Now;
_previousX = _chart.ViewXY.XAxes[0].DateTimeToAxisValue(dt);
//Store the X value
points[0].X = _previousX;
_previousDateTime = dt;
if (_addNaN)
{
// Add NaN for Y value
points[0].Y = double.NaN;
}
else
{
//Randomize and store Y value
points[0].Y = CalculateYValue();
}
//Add the new point into end of first PointLineSeries
_chart.ViewXY.PointLineSeries[0].AddPoints(points, false);
//Set real-time monitoring scroll position, to latest X point.
//ScrollPosition indicates the position where monitoring is currently progressing.
_chart.ViewXY.XAxes[0].ScrollPosition = _previousX;
//Allow updates again, and update
_chart.EndUpdate();
base.RaiseDataGeneratedEvent(points.Length);
}
//Calculate Y value.
private double CalculateYValue()
{
//Use latest value and generate some difference to it.
double y = _previousTemperature + (_random.NextDouble() -0.5)* 1.0;
//Limit between 0 and 100
if (y > 100)
y = 100;
if (y < 0)
y = 0;
_previousTemperature = y;
return y;
}
private void buttonAddNaN_Click(object sender, EventArgs e)
{
// set _addNaN to TRUE for 1 sec and then back to FALSE
_addNaN = true;
Delay(1000).ContinueWith(_ => _addNaN = false );
}
/// <summary>
/// The implementation of task delay in C# 4.0 (for .NET 4.5 Task.Delay could be used).
/// Is is a logical delay without blocking the current thread.
/// </summary>
/// <param name="milliseconds"></param>
/// <returns></returns>
private static Task Delay(double milliseconds)
{
var tcs = new TaskCompletionSource<bool>();
System.Timers.Timer timer = new System.Timers.Timer();
timer.Elapsed += (obj, args) =>
{
tcs.TrySetResult(true);
};
timer.Interval = milliseconds;
timer.AutoReset = false;
timer.Start();
return tcs.Task;
}
}
}