Knowledgebase
How to have a Fixed Scrolling Time-Range on the XAxis that works with Modifiers
Posted by Andrew BT on 11 February 2019 05:03 PM

The Problem

We were recently asked the question how to have a Fixed-Size scrolling window, without using the FIFO Series (e.g. do not discard old data):

I am plotting real time data with the x-axis being time and y-axis being a voltage level. The x-axis range must update as I acquire the data. The timespan width for this axis must remain 10 seconds and must initially start at time=0. Once we pass the 10 second time, I need the chart to scroll to show the most recent 10 second time range. I need the scrolling to be continuous and not to discard old data. 

Since this is now an FAQ we thought we would create an article for you!

The Solution

SciChart provides many ways to achieve this, but perhaps the simplest is to update the VisibleRange of the XAxis when you append data. For example:

DateTime latestXValue = DataSeries.XValues.Last();
DateTime tenSecondsAgo = new DateTime(latestXValue.Ticks - TimeSpan.FromSeconds(10).Ticks);

SciChartSurface.XAxis.VisibleRange = new DateTimeRange(tenSecondsAgo, latestXValue);

Where to Apply the Solution?

Where to do this? Well there are two ways. 

  1. You can do this whenever you append data to the chart.
  2. You can use our ViewportManager feature to have full control over axis ranges.

Ok so what was that about the ViewportManager?

There is a property called SciChartSurface.ViewportManager (expects IViewportManager) which is queried on render to get the X and Y axis VisibleRange. See below the implementation of DefaultViewportManager:

using System.Diagnostics;

/// <summary>
/// The DefaultViewportManager performs a naive calculation for X and Y Axis VisibleRange. 
/// On each render of the parent SciChartSurface, either autorange to fit the data (depending on the Axis.AutoRange property value), 
/// or return the original axis range (no change)
/// </summary>
public class DefaultViewportManager : ViewportManagerBase
{
    /// <summary>
    /// Called when the <see cref="IAxis.VisibleRange"/> changes for an axis. Override in derived types to get a notification of this occurring
    /// </summary>
    /// <param name="axis">The <see cref="IAxis"/>instance</param>
    public override void OnVisibleRangeChanged(IAxis axis)
    {
    }

    /// <summary>
    /// Called when the <see cref="ISciChartSurface" /> is rendered.
    /// </summary>
    /// <param name="sciChartSurface">The SciChartSurface instance</param>
    public override void OnParentSurfaceRendered(ISciChartSurface sciChartSurface)
    {
    }

    /// <summary>
    /// Overridden by derived types, called when the parent <see cref="SciChartSurface" /> requests the XAxis VisibleRange.
    /// The Range returned by this method will be applied to the chart on render
    /// </summary>
    /// <param name="xAxis">The XAxis</param>
    /// <returns>
    /// The new VisibleRange for the XAxis
    /// </returns>
    protected override IRange OnCalculateNewXRange(IAxis xAxis)
    {
        // Calculate the VisibleRange of X Axis, depending on AutoRange property
        if (xAxis.AutoRange)
        {
            var newXRange = xAxis.GetMaximumRange();
            if (newXRange != null && newXRange.IsDefined)
                return newXRange;
        }

        return xAxis.VisibleRange;
    }

    /// <summary>
    /// Overridden by derived types, called when the parent <see cref="SciChartSurface" /> requests a YAxis VisibleRange.
    /// The Range returned by this method will be applied to the chart on render
    /// </summary>
    /// <param name="yAxis">The YAxis</param>
    /// <param name="renderPassInfo"></param>
    /// <returns>
    /// The new VisibleRange for the YAxis
    /// </returns>
    protected override IRange OnCalculateNewYRange(IAxis yAxis, RenderPassInfo renderPassInfo)
    {
        if (yAxis.AutoRange && renderPassInfo.PointSeries != null && renderPassInfo.RenderableSeries != null)
        {
            var newYRange = yAxis.CalculateYRange(renderPassInfo);
            if (newYRange != null && newYRange.IsDefined)
            {
                return newYRange;
            }
        }

        return yAxis.VisibleRange;
    }
}

If you create a class like the above and change the behaviour of OnCalculateNewYRange to return a VisibleRange with a fixed time window, then you you can effectively do anything with the behaviour of the chart.

That's great! But now the Zoom and Pan Modifiers don't work!

Well gosh don't you want everything? :P Just kidding. Yes, your custom ViewportManager will now override the zoom and pan modifiers applied to the chart. Mouse input will do nothing. How can we allow both? 

Well the Real Time Ticking Stock Charts example handles this use-case very well. Try it out before proceeding - 

How to Implement Scrolling Behaviour which is compatible with Zoom and Pan?

Well, its really simple. The example has this one rule that does the scrolling only if the latest XValue is inside the viewport, otherwise, it allows the user to interact with the chart. 

/// INSIDE CreateRealtimeTickingStockChartsViewModel.OnNewPrice method

IOhlcDataSeries<DateTime, double> dataSeries; // Already declared data series
int latestXIndex = dataSeries.Count; 

// If the latest appending point is inside the viewport (i.e. not off the edge of the screen)
// then scroll the viewport 1 bar, to keep the latest bar at the same place
if (XVisibleRange.Max > latestXIndex)
{
    var existingRange = _xVisibleRange;
    var newRange = new IndexRange(existingRange.Min + 1, existingRange.Max + 1);
    XVisibleRange = newRange;
}

You can try something like this in your ViewportManager.OnCalculateNewYRange() method, or, just in code or a ViewModel. It should work!

For DateTimeAxis you will need to use actual date values not indices. Code to scroll a fixed-time window of 10 seconds would look like this:

public void CallThisWhenYouAppendData()
{
    IXyDataSeries<DateTime, double> dataSeries; // Already declared and filled data series
    DateTime latestXValue = dataSeries.XValues.Last(); 

    // If the last point in the DataSeries is in the viewport
    if (SciChartSurface.XAxis.VisibleRange.Max > latestXValue)
    { 
        // Scroll the viewport to show the last ten seconds, else allow user interaction
        DateTime latestXValue = DataSeries.XValues.Last();
        DateTime tenSecondsAgo = new DateTIme(latestXValue.Ticks - TimeSpan.FromSeconds(10).Ticks);

        SciChartSurface.XAxis.VisibleRange = new DateTimeRange(tenSecondsAgo, latestXValue);
    }
}

Notes:

  1. Remember to use TimeSpanRange for TimeSpanAxisDateRange for DateTimeAxis, IndexRange for CategoryDateTimeAxis and DoubleRange for NumericAxis.
  2. You don't have to use a ViewportManager, you can manipulate the SciChartSurface.XAxis.VisibleRange directly.
  3. You can do all the above in MVVM, either by binding SciChartSurface.ViewportManager to a ViewportManager property in a ViewModel, or by binding SciChartSurface.XAxis.VisibleRange to an IRange property in your ViewModel.
  4. Still stuck? Contact us and say you found this article unhelpful and we will do our best to assist.

Further Reading

Please find a Tutorial that goes into more details on this topic in our documentation Tutorial 06 - Adding Realtime Updates.

For further info on various SciChart APIs please refer to the following documentation articles:

Finally, the example can be found in SciChart Examples Suite at Create Stock Charts -> Realtime Ticking Stock Charts. Full source code of the example is available online at this link.

(5 vote(s))
Helpful
Not helpful

Comments (11)
Gary Arnold
12 April 2014 04:34 PM
Thanks for this explanation. Since this is an often needed feature, it would be great if the next release implemented this behavior as an option without going through all this suggested extra coding. I would suggest having a mode that is "continuous scrolling" and has an initial x-axis time range and that time delta is then maintained. Maybe there is an optional parameter that specifies the scrolling "time jump" for when the data exceeds the range - not sure if that makes sense. Anyway, it would be great if it just did this right out of the box. Just about anyone that is doing real time charting could use this feature and it would make the purchase so much more attractive since,in other regards, the package is excellent.
Andrew Burnett-Thompson
08 May 2014 08:08 AM
Hi Gary, good point, but there are some very complex interactions going on here. By encapsulating the API users will lose flexibility. Can you suggest a few boilerplate ways that we could do this without interfering too much with modifiers, zooming panning etc?
Andrew Burnett-Thompson
08 May 2014 08:17 AM
Hi Gary, good point, but there are some very complex interactions going on here. By encapsulating the API users will lose flexibility. Can you suggest a few boilerplate ways that we could do this without interfering too much with modifiers, zooming panning etc?
Bharat Mallapur
07 July 2015 12:52 PM
Hi,

I am a total newcomer to SciChart and am evaluating this for integrationg into our product, and have the exact same requirement that is mentioned here by the user.

I'm not able to adapt your suggestions into my code properly. Could you post a link to a sample (with source code) for the above requirement?
Andrew Burnett-Thompson
08 July 2015 05:47 PM
Hi there, our RealTimeTickingStockCharts example (see here http://www.scichart.com/Abt.Controls.SciChart.SL.ExampleTestPage.html#/Abt.Controls.SciChart.Example;component/Examples/IWantTo/CreateStockCharts/RealtimeMvvm/CreateRealTimeTickingStockChart.xaml) demonstrates this feature.

It's very simple. After updating the data, you calculate the XAxis.VisibleRange you want to show, and show it.

If the user scrolls back in time, you stop updating the XAxis.VisibleRange.

Hope this helps!
Matthew Becker
18 February 2016 04:01 PM
I don't know if any better functionality for this has been added in 4.0, but if not, how about adding an AutoRange value of "IfAtExtents". Then that Axis would apply AutoRange only when it is Zoomed to Extents. Perhaps this would require adding a flag so that the Axis would know that it is at its extents?
Matthew Becker
18 February 2016 04:01 PM
I don't know if any better functionality for this has been added in 4.0, but if not, how about adding an AutoRange value of "IfAtExtents". Then that Axis would apply AutoRange only when it is Zoomed to Extents. Perhaps this would require adding a flag so that the Axis would know that it is at its extents?
Matthew Becker
18 February 2016 04:07 PM
I don't know if any better functionality for this has been added in 4.0, but if not, how about adding an AutoRange value of "IfAtExtents". Then that Axis would apply AutoRange only when it is Zoomed to Extents. Perhaps this would require adding a flag so that the Axis would know that it is at its extents?
Matthew Becker
18 February 2016 04:08 PM
I don't know if any better functionality for this has been added in 4.0, but if not, how about adding an AutoRange value of "IfAtExtents". Then that Axis would apply AutoRange only when it is Zoomed to Extents. Perhaps this would require adding a flag so that the Axis would know that it is at its extents?
Jonas Larsson
12 July 2016 09:48 AM
We have live data coming in continously. We want to look at the N last values, so we are using a FIFO on the series.
Sometimes the operator wants to freeze the graph and inspect the data - zoom and pan.
The live data should continue to be added to the series, but should not be seen in the graph.
When the operator unfreezes the graph, he should see the latest values (added while freezed) and all new values will be added "live".

I have tried to achieve this. I can freeze the data by using SuspendUpdates. But then I can't zoom and pan. Do you know how this could be done?

If I don't freeze the graph, I just set the AutoRange to Never and it's possible to zoom and pan. But it's not possible when I use SuspendUpdates (of course).

Is there another way to "freeze" the update?
Is there another way to zoom and pan while SuspendUpdates is on?
Jonas Larsson
12 July 2016 09:52 AM
We have live data coming in continously. We want to look at the N last values, so we are using a FIFO on the series.
Sometimes the operator wants to freeze the graph and inspect the data - zoom and pan.
The live data should continue to be added to the series, but should not be seen in the graph.
When the operator unfreezes the graph, he should see the latest values (added while freezed) and all new values will be added "live".

I have tried to achieve this. I can freeze the data by using SuspendUpdates. But then I can't zoom and pan. Do you know how this could be done?

If I don't freeze the graph, I just set the AutoRange to Never and it's possible to zoom and pan. But it's not possible when I use SuspendUpdates (of course).

Is there another way to "freeze" the update?
Is there another way to zoom and pan while SuspendUpdates is on?

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