This is the latest working repository used in our demo video for the Maxim to display temperature readings on Bluetooth
Diff: hspguisourcev301/HspGuiSourceV301/HSPGui/MedicalChartHelper.cs
- Revision:
- 3:36de8b9e4b1a
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hspguisourcev301/HspGuiSourceV301/HSPGui/MedicalChartHelper.cs Sat Apr 10 03:05:42 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 (<= _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