Knowledgebase: Tips and Tricks
How to Create a Sweeping ECG Chart
Posted by Andrew BT on 11 July 2014 11:42 AM

A question which gets asked a lot is how to create a sweeping, or wrap-around ECG Chart for signal monitoring. In this type of chart, a signal goes from Left to Right, then once the trace hits the right edge, it restarts at the left edge with a small gap. Something like this:

We have created a video on YouTube showing how we achieved this. For a description of the code see below. We have also attached an example at the footer of this article

 

 

To achieve this in SciChart we need to use a technique of filling two alternate FIFO DataSeries. One to store the current sweep from left to right, another to store the previous sweep from left to right. 

Our XAML is declared like this:

<s:SciChartSurface s:ThemeManager.Theme="Electric" MaxFrameRate="25" x:Name="sciChartSurface">
            
    <s:SciChartSurface.RenderSurface>
        <s3D:Direct3D10RenderSurface/>
    </s:SciChartSurface.RenderSurface>
            
    <s:SciChartSurface.RenderableSeries>
        <s:FastLineRenderableSeries x:Name="traceSeriesA" StrokeThickness="2"/>
        <s:FastLineRenderableSeries x:Name="traceSeriesB" StrokeThickness="2"/>
    </s:SciChartSurface.RenderableSeries>
            
    <s:SciChartSurface.XAxis>
        <!-- Axis is given a range of 10 seconds. We have chosen our FIFO capacity given the sample rate -->
        <!-- to show exactly 10 seconds of data as well -->
        <s:NumericAxis AxisTitle="Seconds (s)" TextFormatting="0.000s" VisibleRange="0, 10" AutoRange="Never"/>
    </s:SciChartSurface.XAxis>
    <s:SciChartSurface.YAxis>
        <s:NumericAxis AxisTitle="Volts (v)" VisibleRange="-0.5, 1.5"/>
    </s:SciChartSurface.YAxis>
            
</s:SciChartSurface>

And our code is declared like this

using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Timers;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Threading;
using Abt.Controls.SciChart.Example.Examples.IWantTo.SeeFeaturedApplication.ECGMonitor;
using Abt.Controls.SciChart.Model.DataSeries;

namespace Abt.Controls.SciChart.Wpf.TestSuite.ExampleSandbox.SweepingEcg
{
    /// 
    /// Interaction logic for SweepingEcg.xaml
    /// 
    [UserExample("Sweeping ECG Trace")]
    public partial class SweepingEcg : Window
    {
        private double[] _sourceData;
        private DispatcherTimer _timer;
        private const int TimerInterval = 20;
        private int _currentIndex;
        private int _totalIndex;
        private TraceAOrB _whichTrace = TraceAOrB.TraceA;
        private XyDataSeries<double, double> _dataSeriesA;
        private XyDataSeries<double, double> _dataSeriesB;

        private enum TraceAOrB
        {
            TraceA, 
            TraceB,
        }

        public SweepingEcg()
        {
            InitializeComponent();  

            this.Loaded += SweepingEcg_Loaded;
        }

        void SweepingEcg_Loaded(object sender, RoutedEventArgs e)
        {
            // Get 10 seconds of data in the dataseries, 4000 samples at 'sample rate' of 400Hz
            _dataSeriesA = new XyDataSeries<double, double>() { FifoCapacity = 3800 };
            _dataSeriesB = new XyDataSeries<double, double>() { FifoCapacity = 3800 };

            // Simulate waveform
            _sourceData = LoadWaveformData("Waveform.txt");

            _timer = new DispatcherTimer();
            _timer.Interval = TimeSpan.FromMilliseconds(TimerInterval);
            _timer.Tick += TimerElapsed;
            _timer.Start();

            traceSeriesA.DataSeries = _dataSeriesA;
            traceSeriesB.DataSeries = _dataSeriesB;
        }

        private void TimerElapsed(object sender, EventArgs e)
        {
            // This constant is just used to calculate the X (time) values from the point index. 
            // The SampleRate, FIFO size are chosen to get exactly '10 seconds' of data in the viewport
            // The XAxis.VisibleRange is also chosen to be 10 seconds. 
            const double sampleRate = 400;

            // As timer cannot tick quicker than ~20ms, we append 10 points
            // per tick to simulate a sampling frequency of 500Hz (e.g. 2ms per sample)
            for (int i = 0; i < 10; i++)
                AppendPoint(sampleRate);
        }

        private void AppendPoint(double sampleRate)
        {
            if (_currentIndex >= _sourceData.Length)
            {
                _currentIndex = 0;
            }

            // Get the next voltage and time, and append to the chart
            double voltage = _sourceData[_currentIndex];
            double time = (_totalIndex / sampleRate) % 10.0;

            if (_whichTrace == TraceAOrB.TraceA)
            {
                _dataSeriesA.Append(time, voltage);
                _dataSeriesB.Append(time, double.NaN);
                traceSeriesA.Opacity = 1.0;
                traceSeriesB.Opacity = 0.5;
            }
            else
            {
                _dataSeriesA.Append(time, double.NaN);
                _dataSeriesB.Append(time, voltage);
                traceSeriesA.Opacity = 0.5;
                traceSeriesB.Opacity = 1.0;
            }            

            _currentIndex++;
            _totalIndex++;

            // Toggle which trace is active
            if (_totalIndex % 4000 == 0)
            {
                _whichTrace = _whichTrace == TraceAOrB.TraceA ? TraceAOrB.TraceB : TraceAOrB.TraceA;
            }
        }

        private double[] LoadWaveformData(string filename)
        {
            var values = new List();

            // Make sure you ahve a reference to Abt.Controls.SciChart.Examples.Wpf EXE as this contains the waveform
            var asm = typeof (ECGMonitorViewModel).Assembly; 
            var resourceString = asm.GetManifestResourceNames().Single(x => x.Contains(filename));

            using (var stream = asm.GetManifestResourceStream(resourceString))
            using (var streamReader = new StreamReader(stream))
            {
                string line = streamReader.ReadLine();
                while (line != null)
                {
                    values.Add(double.Parse(line, NumberFormatInfo.InvariantInfo));
                    line = streamReader.ReadLine();
                }
            }

            return values.ToArray();
        }
    }
}

The example works as follows:

  • Two FIFO DataSeries are declared with a capacity of 3,800 samples.
    • A FIFO series is a circular buffer which automatically discards data once the capacity has been exceeded.
    • They are very memory efficient and can be used if you only care about the latest N samples. 

  • We have chosen our FIFO capacity to be slightly less than 4,000 and our data has a sample rate of 400Hz.
    • So, our FIFO series holds just less than 10seconds of data.
    • Our XAxis has a VisibleRange of 0,10. So, we have chosen parameters well to get 10 seconds in the viewport

  • We append to the XyDataSeries Time as a modulo of 10 (so time is always sweeping from 0...10 then 0..10 again) and Y value as voltage. 

  • We toggle between which data series is Active and Inactive and update opacity. 

 

In the video above we show some advanced techniques to take it further and actually dim the trace the older it is, creating a very nice fading trace effect. Note that the DimTracePaletteProvider in the attached sample will consume a bit more CPU as it has to do a linear search of unsorted data. 

Finally, you can also find the example Attached. This requires SciChart v3.1 to run. 

 



Attachments 
 
 sweepingecg.zip (4.40 KB)
(4 vote(s))
Helpful
Not helpful

CONTACT US

Not sure where to start? Contact us, we are happy to help!


CONTACT US

SciChart Ltd, 16 Beaufort Court, Admirals Way, Docklands, London, E14 9XL. Email: Legal Company Number: 07430048, VAT Number: 101957725