Knowledgebase
How to Create a Scrolling Strip Chart in SciChart?
Posted by Andrew BT on 04 February 2019 12:31 PM

As you may know, our FIFO DataSeries type allows you to implement a Circular Buffer scrolling chart very easily, however, the drawback of FIFO (or advantage, depending on your use case!) is that old data older than the FifoCapacity  is discarded, permanently.

Sometimes our users want to be able to show a window of the latest 10 seconds of a Line Series, but also allow the user to scroll back in time. Our AutoRange feature is also out of the question as this will always lock the chart to showing the entire DataSeries at once.

Q: So how do we achieve this?

A: With a little hacks and magic! ...

ScIChart Scrolling Strip Chart

Strip Chart Requirements

Strip chart recorder has a long strip of paper that is ejected out of the recorder and is usually found in Polygraph, EEG type applications.

 

The requirements of a strip chart are:

  • As new samples are appended to the chart, show the new sample.
  • All data should be preserved on the chart
  • Should be able to go back in time to view older data
  • User Zooming / Panning should not interfere with auto-scrolling scrolling as new samples appended.

FIFO Buffers & AutoRange

For those of you that know SciChart, you will know we have two features which allow scrolling, and auto zooming. These are FIFO DataSeries and AutoRange.

However, you may also know that FIFO DataSeries deliberately discard data older than the buffer size (cannot go back in time) and AutoRange prevents zooming and panning with modifiers (AutoRange means yes, always range to fit the data).

So we need to write some code so that we can scroll, but also allow zooming. Let's begin ...

The Strip Chart Example Code

Using SciChart v5.x, create a new WPF Application and add references to all SciChart v5.x dlls. You will also need to ensure .NET Framework 4.5 or above is set.

Next, create a MainWindow and add the following code:

MainWindow.xaml

<Window x:Class="StripChartExample.StripChartExampleWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:s="http://schemas.abtsoftware.co.uk/scichart"
        Title="StripChartExample" Height="450" Width="800">
    <Grid>
        <s:SciChartSurface x:Name="sciChartSurface" Padding="30,20,20,20" ChartTitle="Strip Chart Example" s:ThemeManager.Theme="BrightSpark">

            <s:SciChartSurface.RenderSurface>
                <!-- High Quality subpixel rendering -->
                <s:HighQualityRenderSurface/>
            </s:SciChartSurface.RenderSurface>

            <s:SciChartSurface.RenderableSeries>
                <s:FastLineRenderableSeries x:Name="lineSeries" StrokeThickness="2" Stroke="#FF3333"/>
            </s:SciChartSurface.RenderableSeries>

            <s:SciChartSurface.XAxis>
                <s:NumericAxis x:Name="xAxis" AxisTitle="TimeStamp (s)" VisibleRange="-10, 0">
                    <s:NumericAxis.Scrollbar>
                        <s:SciChartScrollbar Height="16"/>
                    </s:NumericAxis.Scrollbar>
                </s:NumericAxis>
            </s:SciChartSurface.XAxis>

            <s:SciChartSurface.YAxis>
                <s:NumericAxis x:Name="yAxis" VisibleRange="0,1" AxisTitle="Awesomeness (A)"/>
            </s:SciChartSurface.YAxis>

            <s:SciChartSurface.ChartModifier>
                <s:ModifierGroup>
                    <s:RubberBandXyZoomModifier/>
                    <s:ZoomPanModifier ExecuteOn="MouseMiddleButton"/>
                </s:ModifierGroup>
            </s:SciChartSurface.ChartModifier>

        </s:SciChartSurface>
    </Grid>
</Window>

MainWindow.xaml.cs

using System;
using System.Windows;
using System.Windows.Threading;
using SciChart.Charting.Model.DataSeries;
using SciChart.Data.Model;

namespace StripChartExample
{
    public partial class StripChartExampleWindow : Window
    {
        private XyDataSeries<double, double> _dataSeries;
        private Random _random;
        private DateTime? _startTime;

        private double _windowSize = 10;
        private double _timeNow = 0;
        private bool _showLatestWindow = true;
        private bool _thatWasADoubleClick;
        private RelativeTimeLabelProvider _labelProvider;

        public StripChartExampleWindow()
        {
            InitializeComponent();

            this.Loaded += OnLoaded;
        }

        private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
        {
            // (2): Create a DataSeries and assign to FastLineRenderableSeries
            _dataSeries = new XyDataSeries<double, double>();
            _random = new Random();
            lineSeries.DataSeries = _dataSeries;

            // (6): We subscribe to PreviewMouseDown to set a flag to prevent scrolling calculation in (5)
            sciChartSurface.PreviewMouseDown += (s, arg) =>
            {
                // On mouse down (but not double click), freeze our last N seconds window 
                if (!_thatWasADoubleClick) _showLatestWindow = false;

                _thatWasADoubleClick = false;
            };

            // (7): Subscribe to PreviewMouseDoubleClick to re-enable the auto scrolling window
            sciChartSurface.PreviewMouseDoubleClick += (s, arg) =>
            {
                _showLatestWindow = true;
                _thatWasADoubleClick = true; // (8): Prevent contention between double click and single click event

                // Restore our last N seconds window on double click
                yAxis.AnimateVisibleRangeTo(new DoubleRange(0, 1), TimeSpan.FromMilliseconds(200));
                xAxis.AnimateVisibleRangeTo(new DoubleRange(_timeNow - _windowSize, _timeNow), TimeSpan.FromMilliseconds(200));
            };

            // (3): Create a timer to tick new data 
            var timer = new DispatcherTimer(DispatcherPriority.Render);
            timer.Interval = TimeSpan.FromSeconds(1);
            timer.Tick += TimerOnTick;
            timer.Start();
        }

        private void TimerOnTick(object sender, EventArgs eventArgs)
        {
            _timeNow++;

            // (4): Append next sample            
            _dataSeries.Append(_timeNow, _random.NextDouble());

            // (5): Update visible range if we are in the mode to show the latest window of N seconds
            if (_showLatestWindow)
            {
                xAxis.AnimateVisibleRangeTo(new DoubleRange(_timeNow - _windowSize, _timeNow), TimeSpan.FromMilliseconds(280));
            }
        }
    }
}

At this point your application should build and run. Breaking this down ...

Breaking the Example Down

  1. We create a SciChartSurface with XAxis, YAxis, single FastLineRenderableSeries and some ChartModifiers, in this case RubberBandXyZoomModifier and ZoomPanModifier to zoom and pan the chart.
  2. In code behind, we create a DataSeries and assign to the FastLineRenderableSeries.
  3. We start a DispatcherTimer with an interval of 1 second.
  4. In the DispatcherTimer.Tick handler, we append a new point to the DataSeries. Note the timestamp
  5. To implement the scrolling, we calculate a new VisibleRange for the XAxis which is N seconds long, where N is the window size. Note this VisibleRange always shows the latest N seconds of the waveform. Older data is stored in the DataSeries but not shown.

Adding the Ability to Zoom and Pan

So if you use the application you will notice we can zoom back in time while the VisibleRange calculation in (5) is only applied while user zooming is not occurring. How do we do this?

  1. We subscribe to PreviewMouseDown to set a flag, _showLatestWindow, to enable or disable the visiblerange calculation in step (5). What we want is as soon as the user mouse-downs on the surface (begin zoom operation), stop scrolling. It's as simple as that!
  2. To resume scrolling we subscribe to PreviewMouseDoubleClick and set the flag, _showLatestWindow, back to true.
  3. Oh, we also set a flag _thatWasADoubleClick because in WPF, PreviewMouseDown is fired after PreviewMouseDoubleClick (?...)

That's it! Our application is complete! We have an awesome, high-quality scrolling strip chart with user-zooming panning.

Extra Credit - Adding Relative Time Labels

You will notice in our sample, we show relative time labels. Rather than showing the absolute timestamp on the XAxis, we are showing the seconds ago since the current time, e.g. T-0 (right side, latest sample), T-1, T-2 ... T-10 (left side).

This is an often requested feature and is achievable easily using our LabelProvider feature.

Creating the RelativeTimeLabelProvider

Create a new class which inherits NumericLabelProvider. We are going to format our labels to include a relative time.

using System;
using SciChart.Charting.Visuals.Axes.LabelProviders;

namespace StripChartExample
{
    public class RelativeTimeLabelProvider : NumericLabelProvider
    {
        // Update me with current latest time every time you append data!
        public double CurrentTime { get; set; }

        public override string FormatCursorLabel(IComparable dataValue)
        {
            // dataValue is the actual axis label value, e.g. comes from DataSeries.XValues
            var value = (double)dataValue;
            var relative = (CurrentTime - value);

            return $"t-{relative:0.0}";
        }

        public override string FormatLabel(IComparable dataValue)
        {
            // dataValue is the actual axis label value, e.g. comes from DataSeries.XValues
            var value = (double)dataValue;
            var relative = (CurrentTime - value);

            return $"t-{relative:0.0}";
        }
    }
}

We simply apply the label provider to the SciChartSurface.XAxis.LabelProvider property as follows: 

// In MainWindow.OnLoaded, add this code
_labelProvider = new RelativeTimeLabelProvider();
xAxis.LabelProvider = _labelProvider;
sciChartSurface.InvalidateElement();


Finally, modify our DispatcherTimer.Tick handler to update the labelprovider CurrentTime property, used to calculate relative times. 

private void TimerOnTick(object sender, EventArgs eventArgs)
{
    _timeNow++;
            
    // Append next sample            
    _dataSeries.Append(_timeNow, _random.NextDouble());

    // Update currentTime in LabelProvider to calculate relative labels
    _labelProvider.CurrentTime = _timeNow;

    // Update visible range if we are in the mode to show the latest window of N seconds
    if (_showLatestWindow)
    {
        xAxis.AnimateVisibleRangeTo(new DoubleRange(_timeNow - _windowSize, _timeNow), TimeSpan.FromMilliseconds(280));
    }
}

That's it! Our sample is complete!

 

What else could we do with this? 

  • Well, apart from the myriad EEG, ECG, Polygraph style applications we could monitor network traffic, CPU usage, Process Statistics etc... Anywhere you need to have a live dashboard of frequently updating data.
  • This application could be extended to discard data older than N, either by supplying a DataSeries.FifoCapacity of suitably large number of samples (much larger than Window Size) to automatically discard data, or using DataSeries.Remove to clear out old data.

  • You could use our experimental SplineLineRenderableSeries to smooth the lines and make it look even more awesome.

  • Also, you might want to experiment with using the SciChartOverview control, not just the scrollbar, which shows the entire data series behind the scrollbar allowing for a good visual cue of where you are in the overall series. 

...

Let us know your comments & feedback! 

 

(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