How to handle daylight saving time shift

Need help in implementing some specific function to your LightningChart Ultimate powered application? Post a question and get code snippets from other LightningChart Ultimate community members.

Moderator: Queue Moderators

Post Reply

Was this example helpful?

No
0
No votes
Yes, somewhat
0
No votes
Yes, very good
0
No votes
 
Total votes: 0

User avatar
ArctionPasi
Posts: 1367
Joined: Tue Mar 26, 2013 10:57 pm
Location: Finland
Contact:

How to handle daylight saving time shift

Post by ArctionPasi » 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.

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;
        }
    }
}

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.
Before DST event
Before DST event
BeforeDSTMar26_2017_3AM.jpg (574.3 KiB) Viewed 13222 times
After DST event
After DST event
AfterDSTMar26_2017_3AM.jpg (582.12 KiB) Viewed 13222 times
LightningChart Support Team, PT

Post Reply