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! ... Strip Chart RequirementsA 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:
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 CodeUsing 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.csusing 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
Adding the Ability to Zoom and PanSo 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?
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 LabelsYou 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 RelativeTimeLabelProviderCreate 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();
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?
... Let us know your comments & feedback!
| |
|