This is the latest working repository used in our demo video for the Maxim to display temperature readings on Bluetooth

Dependencies:   USBDevice

hspguisourcev301/HspGuiSourceV301/HSPGui/MedicalChartHelper.cs

Committer:
darienf
Date:
2021-05-02
Revision:
5:bc128a16232f
Parent:
3:36de8b9e4b1a

File content as of revision 5:bc128a16232f:

//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