repo time

Dependencies:   mbed MAX14720 MAX30205 USBDevice

Revision:
20:6d2af70c92ab
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/HspGuiSourceV301/HSPGui/MedicalChartHelper.cs	Tue Apr 06 06:41:40 2021 +0000
@@ -0,0 +1,705 @@
+//using System;
+//using System.Collections;
+//using System.Collections.Generic;
+//using System.ComponentModel;
+//using System.Linq;
+//using System.Text;
+//using System.Windows.Forms;
+//using Microsoft.Win32.SafeHandles;
+
+namespace Maxim.MAX30101
+{
+#pragma warning disable 1574
+    /// <summary>
+    /// MedicalChartHelper class, an invisible "helper" class, 
+    /// helps manage an existing standard chart object.
+    /// 
+    /// Initializes the standard 
+    /// System.Windows.Forms.DataVisualization.Charting.Chart chart
+    /// with a maximum of 3 chart areas vertically aligned with each other.
+    ///
+    /// When data is appended to the chart, the chart series data
+    /// is initially aligned with the left. Once the X axis reaches the
+    /// right side of the screen, the chart begins scrolling so that
+    /// newest data is aligned to the right edge.
+    ///
+    /// </summary>
+#pragma warning restore 1574
+    public class MedicalChartHelper
+    {
+        private System.Windows.Forms.DataVisualization.Charting.Chart _chart;
+        private string _chartArea1Name;
+        private string _chartArea2Name;
+        private string _chartArea3Name;
+        private string _series1Name;
+        private string _series2Name;
+        private string _series3Name;
+        private string _chartArea1AxisYTitle;
+        private string _chartArea2AxisYTitle;
+        private string _chartArea3AxisYTitle;
+        
+        /// <summary>
+        /// Constructor 
+        /// </summary>
+        /// <param name="chart"></param>
+        /// <param name="chartArea1Name"></param>
+        /// <param name="series1Name"></param>
+        /// <param name="chartArea1AxisYTitle"></param>
+        /// <param name="chartArea2Name"></param>
+        /// <param name="series2Name"></param>
+        /// <param name="chartArea2AxisYTitle"></param>
+        /// <param name="chartArea3Name"></param>
+        /// <param name="series3Name"></param>
+        /// <param name="chartArea3AxisYTitle"></param>
+        public MedicalChartHelper(System.Windows.Forms.DataVisualization.Charting.Chart chart,
+            string chartArea1Name = "ChartArea1Red", string series1Name = "SeriesRed", string chartArea1AxisYTitle = "Red ADC Code",
+            string chartArea2Name = "ChartArea2IR", string series2Name = "SeriesIR", string chartArea2AxisYTitle = "IR ADC Code",
+            string chartArea3Name = "ChartArea3Green", string series3Name = "SeriesGreen", string chartArea3AxisYTitle = "Green ADC Code",
+            DataFormats dataFormat = DataFormats.FormatUnsigned
+        )
+        {
+            // implementation: see InitCharting()
+            // default X axis length should be 10 seconds of data
+            _chart = chart;
+            _chartArea1Name = chartArea1Name;
+            _chartArea2Name = chartArea2Name;
+            _chartArea3Name = chartArea3Name;
+            _series1Name = series1Name;
+            _series2Name = series2Name;
+            _series3Name = series3Name;
+            _chartArea1AxisYTitle = chartArea1AxisYTitle;
+            _chartArea2AxisYTitle = chartArea2AxisYTitle;
+            _chartArea3AxisYTitle = chartArea3AxisYTitle;
+
+            DataFormat = dataFormat;
+
+            // VERIFY: OS24EVK-75 replace _xCount with _xCount1, _xCount2, _xCount3
+            //_xCount = 0;
+            _xCount1 = 0;
+            _xCount2 = 0;
+            _xCount3 = 0;
+
+            _chart.Titles[0].Visible = false;
+
+            _chart.ChartAreas[_chartArea2Name].AlignWithChartArea = _chartArea1Name;
+            _chart.ChartAreas[_chartArea3Name].AlignWithChartArea = _chartArea1Name;
+
+            _chart.ChartAreas[_chartArea1Name].AxisY.Title = chartArea1AxisYTitle;
+            _chart.ChartAreas[_chartArea2Name].AxisY.Title = chartArea2AxisYTitle;
+            _chart.ChartAreas[_chartArea3Name].AxisY.Title = chartArea3AxisYTitle;
+
+            //_chart.ChartAreas[_chartArea1Name].AxisX.LabelStyle.Enabled = !_plotXAxisNoLabels;
+            //_chart.ChartAreas[_chartArea2Name].AxisX.LabelStyle.Enabled = !_plotXAxisNoLabels;
+            //_chart.ChartAreas[_chartArea3Name].AxisX.LabelStyle.Enabled = !_plotXAxisNoLabels;
+
+            _chart.ChartAreas[_chartArea1Name].Visible = true; // valid_Red;
+            _chart.ChartAreas[_chartArea2Name].Visible = true; // valid_IR;
+            _chart.ChartAreas[_chartArea3Name].Visible = true; // valid_Green;
+
+            _chart.Series[_series1Name].MarkerSize = 1; // Charting.Series.MarkerSize=0 makes no points visible
+            _chart.Series[_series1Name].BorderWidth = 1; // Charting.Series.BorderWidth is actually the line thickness
+            _chart.Series[_series1Name].XValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.UInt32;
+            _chart.Series[_series1Name].YValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.Int32;
+
+            _chart.Series[_series2Name].MarkerSize = 1; // Charting.Series.MarkerSize=0 makes no points visible
+            _chart.Series[_series2Name].BorderWidth = 1; // Charting.Series.BorderWidth is actually the line thickness
+            _chart.Series[_series2Name].XValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.UInt32;
+            _chart.Series[_series2Name].YValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.Int32;
+
+            _chart.Series[_series3Name].MarkerSize = 1; // Charting.Series.MarkerSize=0 makes no points visible
+            _chart.Series[_series3Name].BorderWidth = 1; // Charting.Series.BorderWidth is actually the line thickness
+            _chart.Series[_series3Name].XValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.UInt32;
+            _chart.Series[_series3Name].YValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.Int32;
+        }
+
+        /// <summary>
+        /// Clear chart data, reset _xCount, reset _plotPointsToSkip
+        /// </summary>
+        public void Clear()
+        {
+            _chart.Series[_series1Name].Points.Clear();
+            _chart.Series[_series2Name].Points.Clear();
+            _chart.Series[_series3Name].Points.Clear();
+            // VERIFY: OS24EVK-75 replace _xCount with _xCount1, _xCount2, _xCount3
+            //_xCount = 0;
+            _xCount1 = 0;
+            _xCount2 = 0;
+            _xCount3 = 0;
+            _plotPointsToSkip = 0;
+            // https://jira.maxim-ic.com/browse/OS24EVK-32 replace numPlotTime with menu item Options | Plot Time
+            _plotPoints = (int)_plotWindowTime * _SampleRate_Hz / SampleAverage_n;
+            if (_plotPoints > _maxPlotPoints)
+            {
+                _plotPointsToSkip = _plotPoints / _maxPlotPoints - 1;
+                _plotPoints = _maxPlotPoints;
+            }
+        }
+
+        /// <summary>
+        /// Replacement for plotXAxisNoLabelsToolStripMenuItem.Checked
+        /// </summary>
+        public bool plotXAxisNoLabels { 
+            get { return _plotXAxisNoLabels; }
+            set
+            {
+                _plotXAxisNoLabels = value;
+                _chart.ChartAreas[_chartArea1Name].AxisX.LabelStyle.Enabled = !_plotXAxisNoLabels;
+                _chart.ChartAreas[_chartArea2Name].AxisX.LabelStyle.Enabled = !_plotXAxisNoLabels;
+                _chart.ChartAreas[_chartArea3Name].AxisX.LabelStyle.Enabled = !_plotXAxisNoLabels;
+            }
+        }
+        private bool _plotXAxisNoLabels = true; // VERIFY: default plotXAxisNoLabelsToolStripMenuItem.Checked
+
+        /// <summary>
+        /// Replacement for plotXAxisTimeToolStripMenuItem.Checked
+        /// </summary>
+        public bool plotXAxisTime
+        {
+            get { return _plotXAxisTime; }
+            set
+            {
+                _plotXAxisTime = value;
+            }
+        }
+        private bool _plotXAxisTime = true; // VERIFY: default plotXAxisTimeToolStripMenuItem.Checked
+
+        private int _SampleRate_Hz = 100;
+        /// <summary>
+        /// Sample rate. Replacement for int.Parse(cboSampleRate.Text)
+        /// </summary>
+        public int SampleRate_Hz
+        {
+            get { return _SampleRate_Hz; } 
+            set 
+            { 
+                _SampleRate_Hz = value;
+
+                //_AutoScaleEveryNSamples = 4 * _SampleRate_Hz; // 4 seconds assuming sample rate int.Parse(cboSampleRate.Text) == 100 samples per second
+
+                // VERIFY: OS24EVK-73 Sample Avg 2 breaks chart scrolling
+                // update _plotPoints which depends on myMAX30101.SampleAverage_n
+                _plotPoints = (int)_plotWindowTime * _SampleRate_Hz / SampleAverage_n;
+                if (_plotPoints > _maxPlotPoints)
+                {
+                    _plotPointsToSkip = _plotPoints / _maxPlotPoints - 1;
+                    _plotPoints = _maxPlotPoints;
+                }
+            } 
+        }
+
+        private int _SampleAverage_n = 1;
+        /// <summary>
+        /// Replacement for myMAX30101.SampleAverage_n
+        /// </summary>
+        public int SampleAverage_n
+        {
+            get { return _SampleAverage_n; }
+            set
+            {
+                _SampleAverage_n = value;
+                // VERIFY: OS24EVK-73 Sample Avg 2 breaks chart scrolling
+                // update _plotPoints which depends on myMAX30101.SampleAverage_n
+                _plotPoints = (int)_plotWindowTime * _SampleRate_Hz / SampleAverage_n;
+                if (_plotPoints > _maxPlotPoints)
+                {
+                    _plotPointsToSkip = _plotPoints / _maxPlotPoints - 1;
+                    _plotPoints = _maxPlotPoints;
+                }
+            }
+        }
+
+        // VERIFY: IMPLEMENT OS24EVK-70 Maxim.MAX30101.MedicalChartHelper._xCount int instead of double?
+        // VERIFY: OS24EVK-75 replace _xCount with _xCount1, _xCount2, _xCount3
+        //public double _xCount = 0;
+        public int _xCount1 = 0;
+        public int _xCount2 = 0;
+        public int _xCount3 = 0;
+
+        /// <summary>
+        /// The actual number of points on the plot (&lt;= _maxPlotPoints). The actual number of points may be less than _maxPlotPoints in order to maintain ~numPlotTime.Value of plotted data
+        /// </summary>
+        public int _plotPoints = 500;
+
+        private int _plotWindowTime = 5;    // plot window in seconds
+        public int plotWindowTime 
+        {
+            get 
+            {
+                return _plotWindowTime;
+            }
+            set 
+            {
+                _plotWindowTime = value;
+                _plotPoints = (int)_plotWindowTime * _SampleRate_Hz / SampleAverage_n;
+                if (_plotPoints > _maxPlotPoints)
+                {
+                    _plotPointsToSkip = _plotPoints / _maxPlotPoints - 1;
+                    _plotPoints = _maxPlotPoints;
+                }
+            }
+        }
+
+        private int _maxPlotPoints = 2000;   // maximum allowed number of points that can be plotted per series. Higher Fs will have more than 500 pts for a given numPlotTime.Value, so the number of samples per point will need to be increased accordingly to give maximum _maxPlotPoints
+
+        public int _plotPointsToSkip = 0;
+
+        // https://jira.maxim-ic.com/browse/OS24EVK-34 Autoscale tuning parameters
+        // https://jira.maxim-ic.com/browse/OS24EVK-34 Change _AutoScaleTimeInterval to _AutoScaleEveryNSamples and use sampleNumber to measure time
+        // https://jira.maxim-ic.com/browse/OS24EVK-42 Autoscale tuning (Larry 2014-11-24) _AutoScaleEveryNSamples
+        // - Slow the Autoscale update rate every 4 seconds
+        //private int _AutoScaleEveryNSamples = 400; // 4 seconds assuming sample rate int.Parse(cboSampleRate.Text) == 100 samples per second
+
+        // private double _AutoScaleMarginXLeft; // ignore old data at left of graph
+        // private double _AutoScaleMarginXRight; // ignore new data at right of graph? probably 0 by default
+        
+        /// <summary>
+        /// chartMax empty area above dataMax
+        /// </summary>
+        private static double _AutoScaleMarginYTop = 0.125;
+        
+        /// <summary>
+        /// chartMin empty area below dataMin
+        /// </summary>
+        private static double _AutoScaleMarginYBottom = 0.125;
+        
+        /// <summary>
+        /// Minimum Y span: allow up to 10 counts per division (i.e. 50 LSBs)
+        /// 
+        /// (5 * _AutoScaleYmultiples) minimum allowed span of chartMax-chartMin, 
+        /// to avoid focusing on LSB noise
+        /// </summary>
+        private static double _AutoScaleMinSpanY = 500;
+
+        /// <summary>
+        /// Y axis: No decimals. Round to nearest multiple of 100.
+        /// </summary>
+        private static double _AutoScaleYmultiples = 100;
+
+        // Calculated initial default chart limits _AutoscaleInitialChartMaxY .. _AutoscaleInitialChartMinY
+        int _AutoscaleInitialChartMaxY =
+            (
+                (int)
+                    ((_AutoScaleMinSpanY * (1.0 + _AutoScaleMarginYTop + _AutoScaleMarginYBottom)) / _AutoScaleYmultiples)
+            )
+            * (int)(_AutoScaleYmultiples);
+        int _AutoscaleInitialChartMinY = 0;
+
+        /// <summary>
+        /// Append data to chart area 1 series 1 (Optical: Red. Accelerometer: X)
+        ///
+        /// @post _xCount1 is updated
+        /// </summary>
+        /// <param name="rawIntArrayYData"></param>
+        /// <param name="firstNewXIndex"></param>
+        /// <param name="lastXIndex"></param>
+        public void AppendDataChartArea1(int[] rawIntArrayYData, int firstNewXIndex, int lastXIndex)
+        {
+            // AppendDataChartArea(_chartArea1Name, _series1Name, rawIntArrayYData, firstNewXIndex, lastXIndex);
+            string chartAreaName = _chartArea1Name;
+            string seriesName = _series1Name;
+            
+            for (int index = firstNewXIndex; index <= lastXIndex; index++)
+            {
+                int yData = rawIntArrayYData[index];
+                // VERIFY: OS24EVK-75 replace AutoScaleEvaluate dataFormatIs16bit2sComplement with if (DataFormat == DataFormats.Format16bit2sComplement) 
+                if (DataFormat == DataFormats.Format16bit2sComplement)
+                {
+                    // VERIFY: OS24EVK-57 interpret rawX rawY rawZ as 16-bit 2's complement
+                    if (yData > 0x8000)
+                    {
+                        yData = yData - 0x10000;
+                    }
+                }
+                int count = _chart.Series[seriesName].Points.Count;
+                // VERIFY: OS24EVK-75 replace _xCount with _xCount1, _xCount2, _xCount3
+                double xCoord = _xCount1 * (_plotPointsToSkip + 1) * SampleAverage_n / (plotXAxisTime ? SampleRate_Hz : 1);
+                
+                //_chart.Series[seriesName].Points.AddXY(xCoord, yData);
+                _chart.Series[seriesName].Points.AddY(yData);
+                _chart.ResetAutoValues();
+
+                while (count > _plotPoints)
+                {
+                    _chart.Series[seriesName].Points.RemoveAt(0);
+                    count = _chart.Series[seriesName].Points.Count;
+                }
+                // VERIFY: OS24EVK-75 replace _xCount with _xCount1, _xCount2, _xCount3
+                _xCount1++;
+            }
+            //AutoScaleEvaluate(e.rawRedData, e.sampleNumberOffset, numSamples - 1, _chart, chartAreaName);
+        }
+
+        /// <summary>
+        /// Append data to chart area 2 series 2 (Optical: IR. Accelerometer: Y)
+        ///
+        /// @post _xCount2 is updated
+        /// </summary>
+        /// <param name="rawIntArrayYData"></param>
+        /// <param name="firstNewXIndex"></param>
+        /// <param name="lastXIndex"></param>
+        public void AppendDataChartArea2(int[] rawIntArrayYData, int firstNewXIndex, int lastXIndex)
+        {
+            // AppendDataChartArea(_chartArea2Name, _series2Name, rawIntArrayYData, firstNewXIndex, lastXIndex);
+            string chartAreaName = _chartArea2Name;
+            string seriesName = _series2Name;
+            
+            for (int index = firstNewXIndex; index <= lastXIndex; index++)
+            {
+                int yData = rawIntArrayYData[index];
+                // VERIFY: OS24EVK-75 replace AutoScaleEvaluate dataFormatIs16bit2sComplement with if (DataFormat == DataFormats.Format16bit2sComplement) 
+                if (DataFormat == DataFormats.Format16bit2sComplement)
+                {
+                    // VERIFY: OS24EVK-57 interpret rawX rawY rawZ as 16-bit 2's complement
+                    if (yData > 0x8000)
+                    {
+                        yData = yData - 0x10000;
+                    }
+                }
+                int count = _chart.Series[seriesName].Points.Count;
+                // VERIFY: OS24EVK-75 replace _xCount with _xCount1, _xCount2, _xCount3
+                double xCoord = _xCount2 * (_plotPointsToSkip + 1) * SampleAverage_n / (plotXAxisTime ? SampleRate_Hz : 1);
+                
+                _chart.Series[seriesName].Points.AddXY(xCoord, yData);
+                _chart.ResetAutoValues();
+
+                while (count > _plotPoints)
+                {
+                    _chart.Series[seriesName].Points.RemoveAt(0);
+                    count = _chart.Series[seriesName].Points.Count;
+                }
+                // VERIFY: OS24EVK-75 replace _xCount with _xCount1, _xCount2, _xCount3
+                _xCount2++;
+            }
+            //AutoScaleEvaluate(e.rawRedData, e.sampleNumberOffset, numSamples - 1, _chart, chartAreaName);
+        }
+
+        /// <summary>
+        /// Append data to chart area 3 series 3 (Optical: Green. Accelerometer: Z)
+        ///
+        /// @post _xCount3 is updated
+        /// </summary>
+        /// <param name="rawIntArrayYData"></param>
+        /// <param name="firstNewXIndex"></param>
+        /// <param name="lastXIndex"></param>
+        public void AppendDataChartArea3(int[] rawIntArrayYData, int firstNewXIndex, int lastXIndex)
+        {
+            // AppendDataChartArea(_chartArea3Name, _series3Name, rawIntArrayYData, firstNewXIndex, lastXIndex);
+            string chartAreaName = _chartArea3Name;
+            string seriesName = _series3Name;
+            
+            for (int index = firstNewXIndex; index <= lastXIndex; index++)
+            {
+                int yData = rawIntArrayYData[index];
+                // VERIFY: OS24EVK-75 replace AutoScaleEvaluate dataFormatIs16bit2sComplement with if (DataFormat == DataFormats.Format16bit2sComplement) 
+                if (DataFormat == DataFormats.Format16bit2sComplement)
+                {
+                    // VERIFY: OS24EVK-57 interpret rawX rawY rawZ as 16-bit 2's complement
+                    if (yData > 0x8000)
+                    {
+                        yData = yData - 0x10000;
+                    }
+                }
+                int count = _chart.Series[seriesName].Points.Count;
+                // VERIFY: OS24EVK-75 replace _xCount with _xCount1, _xCount2, _xCount3
+                double xCoord = _xCount3 * (_plotPointsToSkip + 1) * SampleAverage_n / (plotXAxisTime ? SampleRate_Hz : 1);
+                
+                _chart.Series[seriesName].Points.AddXY(xCoord, yData);
+                _chart.ResetAutoValues();
+
+                while (count > _plotPoints)
+                {
+                    _chart.Series[seriesName].Points.RemoveAt(0);
+                    count = _chart.Series[seriesName].Points.Count;
+                }
+                // VERIFY: OS24EVK-75 replace _xCount with _xCount1, _xCount2, _xCount3
+                _xCount3++;
+            }
+            //AutoScaleEvaluate(e.rawRedData, e.sampleNumberOffset, numSamples - 1, _chart, chartAreaName);
+        }
+
+#if PROFILER
+        //// TODO1: OS24EVK-54 profiling: capture max interval, number of intervals, cumulative interval
+        System.Diagnostics.Stopwatch _profilingStopwatchAutoScaleEvaluate = new System.Diagnostics.Stopwatch(); // cumulative timing
+        int _profilingStopwatchAutoScaleEvaluate_NumIntervals;
+#endif // PROFILER
+
+        public void AutoScaleEvaluate1(int[] rawIntArrayYData, int firstNewXIndex, int lastXIndex)
+        {
+            AutoScaleEvaluate(rawIntArrayYData, firstNewXIndex, lastXIndex, _chartArea1Name);
+        }
+
+        public void AutoScaleEvaluate2(int[] rawIntArrayYData, int firstNewXIndex, int lastXIndex)
+        {
+            AutoScaleEvaluate(rawIntArrayYData, firstNewXIndex, lastXIndex, _chartArea2Name);
+        }
+
+        public void AutoScaleEvaluate3(int[] rawIntArrayYData, int firstNewXIndex, int lastXIndex)
+        {
+            AutoScaleEvaluate(rawIntArrayYData, firstNewXIndex, lastXIndex, _chartArea3Name);
+        }
+
+        // TODO1: OS24EVK-57 OS24EVK-54 AutoScaleEvaluate(int[] rawIntArrayYData,...) Calculate autoscale min/max/SampleVariance from the raw integer data. This should be much faster as it doesn't involve arbitrarily converting into floating-point numbers or excessive indexing.
+        public void AutoScaleEvaluate(int[] rawIntArrayYData,
+                int firstNewXIndex,
+                int lastXIndex,
+                /*Chart chart,*/ string chartAreaName
+                // TODO1: OS24EVK-75 replace AutoScaleEvaluate dataFormatIs16bit2sComplement with if (DataFormat == DataFormats.Format16bit2sComplement) 
+                //bool dataFormatIs16bit2sComplement = false
+            //string seriesName, 
+            //int sampleNumber, 
+            //ref int sampleNumberPreviousAutoscale
+            )
+        {
+#if PROFILER
+            // TODO1: OS24EVK-54 profiling: capture max interval, number of intervals, cumulative interval
+            _profilingStopwatchAutoScaleEvaluate.Start();
+            _profilingStopwatchAutoScaleEvaluate_NumIntervals = _profilingStopwatchAutoScaleEvaluate_NumIntervals + 1;
+#endif // PROFILER
+
+            // TODO1: OS24EVK-57 OS24EVK-54 Calculate autoscale min/max/SampleVariance from the raw integer data. This should be much faster as it doesn't involve arbitrarily converting into floating-point numbers or excessive indexing.
+
+            // https://jira.maxim-ic.com/browse/OS24EVK-34 AutoScaleEvaluate() use _AutoScaleMarginXLeft; // ignore old data at left of graph
+            // https://jira.maxim-ic.com/browse/OS24EVK-34 AutoScaleEvaluate() use _AutoScaleMarginXRight; // ignore new data at right of graph? probably 0 by default
+            //const int firstPointIndex = 0;
+            // advance the left and right X limits
+            // https://jira.maxim-ic.com/browse/OS24EVK-48 Graph X axis - Replace hidden chkPlotTime with a menu option plotXAxisTimeToolStripMenuItem.Checked
+            // https://jira.maxim-ic.com/browse/OS24EVK-47 Graph scrolling needs to be smooth progression like a paper tape chart even when (plotXAxisTimeToolStripMenuItem.Checked is true) 
+            // per Larry Skrenes 2014-11-24: graph scrolling needs to be smooth progression like a paper tape chart.
+            // When (plotXAxisTimeToolStripMenuItem.Checked is false) graph scrolling is already smooth.
+            // When (plotXAxisTimeToolStripMenuItem.Checked is true) graph scrolling moves in staccato 1-second steps. Larry hates this.
+            int xWidth = _plotPoints * (_plotPointsToSkip + 1) * SampleAverage_n / (plotXAxisTime ? SampleRate_Hz : 1);
+            // VERIFY: OS24EVK-73 Sample Avg 2 breaks chart scrolling -- xLastXIndex = lastXIndex * myMAX30101.SampleAverage_n
+            int xLastXIndex = lastXIndex * SampleAverage_n / (plotXAxisTime ? SampleRate_Hz : 1);
+            //int xWidth = _plotPoints * (_plotPointsToSkip + 1) * _cboSampleAvg / (false ? int.Parse(cboSampleRate.Text) : 1);
+            //int xWidth = _plotPoints * (_plotPointsToSkip + 1) * _cboSampleAvg / (1);
+            //int xWidth = _plotPoints * (_plotPointsToSkip + 1) * _cboSampleAvg;
+            // https://jira.maxim-ic.com/browse/OS24EVK-15 Fix plot X axis jumping around (_plotWindowTime)
+            // 0.0 --> 0  1  2  3  4  5
+            // 0.0 --> 0_ 1  2  3  4  5
+            // 0.0 --> 0__1  2  3  4  5
+            // 0.0 --> 0__1_ 2  3  4  5
+            // 0.0 --> 0__1__2  3  4  5
+            // 0.0 --> 0__1__2_ 3  4  5
+            // 0.0 --> 0__1__2__3  4  5
+            // 0.0 --> 0__1__2__3_ 4  5
+            // 0.0 --> 0__1__2__3__4  5
+            // 0.0 --> 0__1__2__3__4_ 5
+            // 0.0 --> 0__1__2__3__4__5
+            // 0.5 -->    1__2__3__4__5_ 6
+            // 1.0 -->    1__2__3__4__5__6
+            // 1.5 -->       2__3__4__5__6_ 7
+            // 2.0 -->       2__3__4__5__6__7
+            // https://jira.maxim-ic.com/browse/OS24EVK-47 Graph scrolling needs to be smooth progression like a paper tape chart even when (plotXAxisTimeToolStripMenuItem.Checked is true) 
+            // per Larry Skrenes 2014-11-24: graph scrolling needs to be smooth progression like a paper tape chart.
+            // When (plotXAxisTimeToolStripMenuItem.Checked is false) graph scrolling is already smooth.
+            // When (plotXAxisTimeToolStripMenuItem.Checked is true) graph scrolling moves in staccato 1-second steps. Larry hates this.
+            // VERIFY: OS24EVK-76 Autoscale delay time is affected by Sample Average - firstVisiblePointIndex
+            //int firstVisiblePointIndex = lastXIndex - (_plotPoints * (_plotPointsToSkip + 1) * SampleAverage_n);
+            int firstVisiblePointIndex = lastXIndex - (_plotPoints * (_plotPointsToSkip + 1));
+            if (firstVisiblePointIndex < 0) { firstVisiblePointIndex = 0; }
+            //
+            // VERIFY: OS24EVK-73 Sample Avg 2 breaks chart scrolling -- xLastXIndex instead of lastXIndex
+            int firstVisiblePointX = xLastXIndex - xWidth; // (int)chart.Series[0].Points[firstPointIndex].XValue;
+            if (firstVisiblePointX < 0) { firstVisiblePointX = 0; }
+            //int firstPointX = (int)System.Math.Ceiling(chart.Series[seriesName].Points[firstPointIndex].XValue);
+            //if ((chart.Series[seriesName].Points[firstPointIndex].XValue) < 0.1)
+            //{
+            //    firstPointX = (int)System.Math.Floor(chart.Series[seriesName].Points[firstPointIndex].XValue);
+            //}
+            _chart.ChartAreas[chartAreaName].AxisX.Minimum = firstVisiblePointX;
+            // https://jira.maxim-ic.com/browse/OS24EVK-47 Graph scrolling needs to be smooth progression like a paper tape chart even when (plotXAxisTimeToolStripMenuItem.Checked is true) 
+            //chart.ChartAreas[0].AxisX.Maximum = (int)System.Math.Ceiling((double)firstPointX + xWidth);
+            _chart.ChartAreas[chartAreaName].AxisX.Maximum = firstVisiblePointX + xWidth;
+
+            if (lastXIndex /* chart.Series[seriesName].Points.Count */ < 1)
+            {
+                //int iMaxY_chart_default = (_AutoScaleMinSpanY * (1.0 + _AutoScaleMarginYTop + _AutoScaleMarginYBottom)) / _AutoScaleYmultiples;
+                //int _AutoscaleInitialChartMaxY = iMaxY_chart_default * _AutoScaleYmultiples;
+                //int _AutoscaleInitialChartMaxY =
+                //    (
+                //        (int)
+                //            ((_AutoScaleMinSpanY * (1.0 + _AutoScaleMarginYTop + _AutoScaleMarginYBottom)) / _AutoScaleYmultiples)
+                //    )
+                //    * (int)(_AutoScaleYmultiples);
+                //int _AutoscaleInitialChartMinY = 0;
+                _chart.ChartAreas[chartAreaName].AxisY.Maximum = _AutoscaleInitialChartMaxY;
+                _chart.ChartAreas[chartAreaName].AxisY.Minimum = _AutoscaleInitialChartMinY;
+#if PROFILER
+                // TODO1: OS24EVK-54 profiling: capture max interval, number of intervals, cumulative interval
+                _profilingStopwatchAutoScaleEvaluate.Stop();
+#endif // PROFILER
+                return;
+            }
+
+            //            // TODO1: OS24EVK-57 OS24EVK-54 scan new data rawIntArrayYData[firstNewIndex .. lastIndex]. As is this code only triggers if the last point is extreme.
+            //            int newestPointIndex = lastXIndex; // chart.Series[seriesName].Points.Count - 1;
+            //            int newestPointY = rawIntArrayYData[lastXIndex]; // (int)chart.Series[seriesName].Points[newestPointIndex].YValues[0];
+            //            if ((newestPointY < chart.ChartAreas[chartAreaName].AxisY.Maximum)
+            //                && (newestPointY > chart.ChartAreas[chartAreaName].AxisY.Minimum)
+            //                )
+            //            {
+            //                // https://jira.maxim-ic.com/browse/OS24EVK-34 always re-evaluate chart when new data exceeds Minimum or Maximum
+            //                // But if we just return at this point, then autoscale will never contract to fit smaller data series.
+            //                //
+            //                // https://jira.maxim-ic.com/browse/OS24EVK-34 Change _AutoScaleTimeInterval to _AutoScaleEveryNSamples and use sampleNumber to measure time
+            //                // https://jira.maxim-ic.com/browse/OS24EVK-34 AutoScaleEvaluate() after every _AutoScaleEveryNSamples, re-scan data and fit chart to data
+            //                // if ( interval has been less than _AutoScaleEveryNSamples ) { return; }
+            //                if ((sampleNumber - sampleNumberPreviousAutoscale) < _AutoScaleEveryNSamples)
+            //                {
+            //#if PROFILER
+            //                    // TODO1: OS24EVK-54 profiling: capture max interval, number of intervals, cumulative interval
+            //                    _profilingStopwatchAutoScaleEvaluate.Stop();
+            //#endif // PROFILER
+            //                    return;
+            //                }
+            //            }
+
+            int chartMinYValue = (int)_chart.ChartAreas[chartAreaName].AxisY.Minimum; // rawIntArrayYData[firstVisiblePointX] - 1;
+            int chartMaxYValue = (int)_chart.ChartAreas[chartAreaName].AxisY.Maximum; // rawIntArrayYData[firstVisiblePointX] + 1;
+            // VERIFY: OS24EVK-73 Sample Avg 2 breaks chart scrolling - firstVisiblePointIndex instead of firstVisiblePointX
+            int dataMinYValue = rawIntArrayYData[firstVisiblePointIndex] - 1;
+            int dataMaxYValue = rawIntArrayYData[firstVisiblePointIndex] + 1;
+            for (int index = firstVisiblePointIndex; index <= lastXIndex; index++)
+            {
+                int Y = rawIntArrayYData[index];
+                // VERIFY: OS24EVK-57 OS24EVK-54 improve chart throughput: AutoScaleEvaluate() dataFormatIs16bit2sComplement=true for Accelerometer X Y Z data
+                // VERIFY: OS24EVK-75 replace AutoScaleEvaluate dataFormatIs16bit2sComplement with if (DataFormat == DataFormats.Format16bit2sComplement) 
+                if (DataFormat == DataFormats.Format16bit2sComplement)
+                {
+                    // VERIFY: OS24EVK-57 interpret rawX rawY rawZ as 16-bit 2's complement
+                    if (Y > 0x8000)
+                    {
+                        Y = Y - 0x10000;
+                    }
+                }
+                //else
+                //{
+                //    Y = Y + 0; // debug breakpoint for code path verification
+                //}
+                if (dataMinYValue > Y)
+                {
+                    dataMinYValue = Y;
+                }
+                if (dataMaxYValue < Y)
+                {
+                    dataMaxYValue = Y;
+                }
+            }
+            // rawIntArrayYData[firstIndex..lastIndex] spans the range from dataMinYValue .. dataMaxYValue
+            double dataSpanY = (dataMaxYValue - dataMinYValue);
+            double dataCenterY = (dataMaxYValue + dataMinYValue) / 2;
+
+            bool isAllDataVisible = (
+                (chartMinYValue < dataMaxYValue) && (dataMaxYValue < chartMaxYValue)
+                &&
+                (chartMinYValue < dataMinYValue) && (dataMinYValue < chartMaxYValue)
+                );
+            if (isAllDataVisible)
+            {
+                // all data is within the chart limits, but is the chart sufficiently zoomed in?
+                double relativeDataSpan = (double)dataSpanY / (chartMaxYValue - chartMinYValue);
+                if (relativeDataSpan > 0.65)
+                {
+                    // the current chart scale is good enough, do not update
+#if PROFILER
+                    // TODO1: OS24EVK-54 profiling: capture max interval, number of intervals, cumulative interval
+                    _profilingStopwatchAutoScaleEvaluate.Stop();
+#endif // PROFILER
+                    return;
+                }
+            }
+            // https://jira.maxim-ic.com/browse/OS24EVK-34 AutoScaleEvaluate() use _AutoScaleMinSpanY; // minimum allowed span of chartMax-chartMin, to avoid focusing on LSB noise
+            if (dataSpanY < _AutoScaleMinSpanY)
+            {
+                // https://jira.maxim-ic.com/browse/OS24EVK-34 Autoscale - Minimum span (in case dataMax-dataMin is too small) to avoid focusing on LSB noise
+                dataSpanY = _AutoScaleMinSpanY;
+            }
+            else
+            {
+                // https://jira.maxim-ic.com/browse/OS24EVK-34 Autoscale - Apply a Top margin - Bottom margin = 12% .. 88% when mapping Chart max-min to Data max-min
+                // https://jira.maxim-ic.com/browse/OS24EVK-34 AutoScaleEvaluate() use _AutoScaleMarginYTop; // chartMax empty area above dataMax
+                // https://jira.maxim-ic.com/browse/OS24EVK-34 AutoScaleEvaluate() use _AutoScaleMarginYBottom; // chartMin empty area below dataMin
+                // dataSpanY = dataSpanY * 1.25;
+                // dataSpanY = dataSpanY * (1.0 + 0.125 + 0.125);
+                dataSpanY = dataSpanY * (1.0 + _AutoScaleMarginYTop + _AutoScaleMarginYBottom);
+
+                // https://jira.maxim-ic.com/browse/OS24EVK-42 Autoscale tuning (Larry 2014-11-24) Round to nearest multiple of 100.
+                // - Y axis: No decimals. Round to nearest multiple of 100.
+                // private double _AutoScaleYmultiples = 100;
+                //int tempdataSpanY = (int)(dataSpanY / _AutoScaleYmultiples);
+                //dataSpanY = tempdataSpanY * _AutoScaleYmultiples;
+                //dataSpanY = (double)((int)dataSpanY / _AutoScaleYmultiples) * _AutoScaleYmultiples;
+                //dataCenterY = (double)((int)dataCenterY / _AutoScaleYmultiples) * _AutoScaleYmultiples;
+            }
+            // https://jira.maxim-ic.com/browse/OS24EVK-34 Autoscale - There are 5 vertical divisions; keep the minor axis tick values integer
+            //int YaxisDivisions = (int)System.Math.Ceiling(dataSpanY / 6.0);
+            //dataSpanY = YaxisDivisions * 6;
+            // https://jira.maxim-ic.com/browse/OS24EVK-42 Autoscale tuning (Larry 2014-11-24) Round to nearest multiple of 100.
+            // - Y axis: No decimals. Round to nearest multiple of 100.
+            // private double _AutoScaleYmultiples = 100;
+            //int tempdataCenterY = (int)(dataCenterY / (_AutoScaleYmultiples*2));
+            //dataCenterY = tempdataCenterY * (_AutoScaleYmultiples*2);
+
+            // chart.ChartAreas[0].AxisY.Maximum = dataMaxYValue;
+            // chart.ChartAreas[0].AxisY.Minimum = dataMinYValue;
+            //chart.ChartAreas[0].AxisY.Maximum = System.Math.Ceiling(dataCenterY + (dataSpanY / 2)); // dataMaxYValue;
+            //chart.ChartAreas[0].AxisY.Minimum = System.Math.Floor(dataCenterY - (dataSpanY / 2)); // dataMinYValue;
+            // https://jira.maxim-ic.com/browse/OS24EVK-42 Autoscale tuning (Larry 2014-11-24) Round to nearest multiple of 100.
+            // - Y axis: No decimals. Round to nearest multiple of 100.
+            // private double _AutoScaleYmultiples = 100;
+            int iMaxY_target = (int)((dataCenterY + (dataSpanY / 2)) / _AutoScaleYmultiples);
+            int iMinY_target = (int)((dataCenterY - (dataSpanY / 2)) / _AutoScaleYmultiples);
+
+            double dampingConstantK = 0.2;
+            int iMaxY_damped = iMaxY_target;
+            int iMinY_damped = iMinY_target;
+            if (double.IsNaN(_chart.ChartAreas[chartAreaName].AxisY.Maximum) == false)
+            {
+                int iMaxY_chart = (int)(_chart.ChartAreas[chartAreaName].AxisY.Maximum / _AutoScaleYmultiples);
+                int iMinY_chart = (int)(_chart.ChartAreas[chartAreaName].AxisY.Minimum / _AutoScaleYmultiples);
+                iMaxY_damped = (int)((dampingConstantK * (double)iMaxY_target) + ((1 - dampingConstantK) * (double)iMaxY_chart) + 0.5);
+                iMinY_damped = (int)((dampingConstantK * (double)iMinY_target) + ((1 - dampingConstantK) * (double)iMinY_chart));
+            }
+            double maxY = iMaxY_target * _AutoScaleYmultiples;
+            double minY = iMinY_target * _AutoScaleYmultiples;
+            _chart.ChartAreas[chartAreaName].AxisY.Maximum = maxY;
+            _chart.ChartAreas[chartAreaName].AxisY.Minimum = minY;
+            // if chart.ChartAreas[chartAreaName].AxisY.Maximum  is NaN then just assign maxY
+            //if (double.IsNaN(chart.ChartAreas[chartAreaName].AxisY.Maximum))
+            //{
+            //    chart.ChartAreas[chartAreaName].AxisY.Maximum = maxY;
+            //    chart.ChartAreas[chartAreaName].AxisY.Minimum = minY;
+            //}
+            //else
+            //{
+            //    chart.ChartAreas[chartAreaName].AxisY.Maximum = (dampingConstantK * maxY) + ((1 - dampingConstantK) * chart.ChartAreas[chartAreaName].AxisY.Maximum);
+            //    chart.ChartAreas[chartAreaName].AxisY.Minimum = (dampingConstantK * minY) + ((1 - dampingConstantK) * chart.ChartAreas[chartAreaName].AxisY.Minimum);
+            //}
+
+#if PROFILER
+            // TODO1: OS24EVK-54 profiling: capture max interval, number of intervals, cumulative interval
+            _profilingStopwatchAutoScaleEvaluate.Stop();
+#endif // PROFILER
+        }
+
+        /// <summary>
+        /// Raw data format
+        /// </summary>
+        public DataFormats DataFormat = DataFormats.FormatUnsigned;
+
+        public enum DataFormats
+        {
+            /// <summary>
+            /// Interpret raw data as unsigned values
+            /// </summary>
+            FormatUnsigned,
+
+            /// <summary>
+            /// Interpret raw data as 16-bit, signed 2's complement values
+            /// </summary>
+            Format16bit2sComplement,
+        }
+
+    } // public class MedicalChartHelper
+    
+} // namespace Maxim.MAX30101