How to create real-time chart (RadCartesianChart with Spline Series) scrolled or panned in time?

11 posts, 0 answers
  1. Yaroslav
    Yaroslav avatar
    123 posts
    Member since:
    Jan 2016

    Posted 31 Oct Link to this post

    Hello. I need a real-time RadCartesianChart with Spline Series which could contain a large number of points and could be scrolled (or panned) in time. Suppose the program works and generates a chart for an hour. The chart moves from right to left. Every second the program polls the external device (flowmeter) and receives data to form a new point on the chart. In the chart window only last 30 points are seen. The others are hidden behind the left border of the chart window. To see the hidden part of the chart the user has to do one of the two floowing options:

    - Scroll the chart. I.e., the user moves the mouse cursor to the scroll bar thumb and sets the mouse cursor over it, presses the right mouse button and while holding it, tows scroll bar thumb from right to left until the chart will be scrolled until the required distance.

    Or

    -Pan the chart. I.e., the user sets the mouse cursor over the chart, presses the right mouse button and while holding it, drags the chart from right to left until the chart will be moved until the required distance.

    Below, I present to you View and ViewModel of how the chart is now done. Please see the View:

    <telerik:RadCartesianChart Grid.Row="0">
        <!--Turn off scrollbars on the chart-->
        <telerik:RadCartesianChart.Resources>
            <Style TargetType="telerik:PanZoomBar">
                <Setter Property="Visibility" Value="Collapsed"/>
            </Style>
        </telerik:RadCartesianChart.Resources>
        <telerik:SplineSeries CategoryBinding="Category" ValueBinding="Value" ItemsSource="{Binding Data}"/>
        <telerik:RadCartesianChart.HorizontalAxis>
            <telerik:DateTimeContinuousAxis MajorStepUnit="Second" LabelInterval="5" LabelFormat="hh:mm:ss" FontFamily="Segoe UI" PlotMode="OnTicks" TickOrigin="{Binding AlignmentDate}"/>
        </telerik:RadCartesianChart.HorizontalAxis>
        <telerik:RadCartesianChart.VerticalAxis>
            <telerik:LinearAxis FontFamily="Segoe UI" Title="Meters per second [m/s]"/>
        </telerik:RadCartesianChart.VerticalAxis>
        <!--Coordinate grid-->
        <telerik:RadCartesianChart.Grid>
            <telerik:CartesianChartGrid MajorLinesVisibility="XY" MajorXLineDashArray="3,4" MajorYLineDashArray="3,4"/>
        </telerik:RadCartesianChart.Grid>
        <!--Changing of scale and panning of the chart---->
        <telerik:RadCartesianChart.Behaviors>
            <telerik:ChartPanAndZoomBehavior DragMode="Pan" ZoomMode="Both"  PanMode="Both"/>
        </telerik:RadCartesianChart.Behaviors>
    </telerik:RadCartesianChart>

    Please see the ViewModel

    public class t_SoundVelocityViewModel : BindableBase
    {
        #region Fields
     
        #region Constant Fields
        /// <summary>
        /// Outer device polling timer period.
        /// </summary>
        private const int TIMER_INTERVAL = 1000;
        . . . . . . . . . . . . . . . . . . . . . . . . .
        #endregion
     
        #region Common Fields  
        . . . . . . . . . . . . . . . . . . . . . . . . .
        /// <summary>
        /// Chart points collection.
        /// </summary>
        private RadObservableCollection<ChartPoint> _data;
        /// <summary>
        /// Outer device polling timer.
        /// </summary>
        private DispatcherTimer _timer;
        /// <summary>
        ///
        /// </summary>
        private DateTime _lastDate;
        /// <summary>
        ///
        /// </summary>
        private DateTime? _alignmentDate;
        /// <summary>
        ///
        /// </summary>
        private bool _useAlignmentDate;
        /// <summary>
        /// Текущее значение скорости звука.
        /// </summary>
        private double _soundVelocityValue = 0;
        #endregion
     
        #endregion
     
        #region Constructors
     
        /// <summary>
        /// Конструктор.
        /// </summary>
        public t_SoundVelocityViewModel(IEventAggregator eventAggregator)
        {
            . . . . . . . . . . . . . . . . . . . . . . . .
     
            this.FillData();
     
            this._useAlignmentDate = true;
     
            // Create timer.
            this._timer = new DispatcherTimer();
            this._timer.Interval = TimeSpan.FromMilliseconds(TIMER_INTERVAL);
            this._timer.Tick += this.onTimer;
            // Запустить таймер опроса.
            this._timer.Start();
        }
     
        #endregion
     
        #region Properties
     
        /// <summary>
        /// Gets or sets chart points collection.
        /// </summary>
        public RadObservableCollection<ChartPoint> Data
        {
            get { return this._data; }
            set { this.SetProperty(ref this._data, value); }
        }
        public DateTime? AlignmentDate
        {
            get { return this._alignmentDate; }
            set { this.SetProperty(ref this._alignmentDate, value); }
        }
        public bool UseAlignmentDate
        {
            get { return this._useAlignmentDate; }
            set
            {
                if (this.SetProperty(ref this._useAlignmentDate, value))
                    this.updateAlignmentDate();
            }
        }
        . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
     
        #endregion
     
        #region Methods
     
        . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
        /// <summary>
        /// Polling timer tick's handler.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void onTimer(object sender, EventArgs e)
        {
            try
            {
                this._lastDate = this._lastDate.AddMilliseconds(TIMER_INTERVAL);
                // Turn off collection changed notifications when very old chart point is removed
                // from chart points collection and a new chart point add there.
                this.Data.SuspendNotifications();
                this.Data.RemoveAt(0);
                this.Data.Add(this.CreateBusinessObject());
                // Turn on collection changed notifications after new chart point was added there.
                this.Data.ResumeNotifications();
     
                . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
            }
            catch (Exception ex)
            {
                if (ex.InnerException != null)
                    raiseNotification(ex.InnerException.Message, "Ошибка");
                else
                    raiseNotification(ex.Message, "Ошибка");
            }
        }
     
        /// <summary>
        /// Creates chart point.
        /// </summary>
        /// <returns></returns>
        private ChartPoint CreateBusinessObject()
        {
            this._soundVelocityValue = GlobalStaticMembers.SoundVelocityBuffer;
            ChartPoint point = new ChartPoint();
            point.Value = this._soundVelocityValue;
            point.Category = this._lastDate;
     
            return point;
        }
     
        /// <summary>
        ///
        /// </summary>
        private void FillData()
        {
            RadObservableCollection<ChartPoint> collection = new RadObservableCollection<ChartPoint>();
            this._lastDate = DateTime.Now;
            this._alignmentDate = this._lastDate;
            for (int i = 0; i < 31; i++)
            {
                this._lastDate = DateTime.Now;
                collection.Add(this.CreateBusinessObject());
            }
            this.Data = collection;
        }
     
        /// <summary>
        /// Starts polling timer.
        /// </summary>
        public void startTimer(bool param)
        {
            if (!this._timer.IsEnabled)
                this._timer.Start();
        }
     
        /// <summary>
        /// Stops polling timer.
        /// </summary>
        public void stopTimer(bool param)
        {
            this._timer.Stop();
        }
     
        /// <summary>
        /// Updates polling timer interval.
        /// </summary>
        /// <param name="interval"></param>
        public void UpdateTimer(double interval)
        {
            this._timer.Interval = TimeSpan.FromMilliseconds(interval);
        }
     
        /// <summary>
        ///
        /// </summary>
        private void updateAlignmentDate()
        {
            this.AlignmentDate = this._useAlignmentDate ? (DateTime?)this._lastDate : null;
        }
        . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
     
        #endregion
    }
    As you can see only 30 last point are shown in the chart now. But I'm in need of scrolled or panned chart as I write above. I've seen documentation for ChartView and posts in forum of ChartView WPF but I found nothing significant about scrolling (or panning) real-time chart with very large number of points in time. If you give me an example of such RadCartesianChart with Spline Series (as I describe above) I will be very grateful to you. Please help me if this is possible ofcource.
  2. Yaroslav
    Yaroslav avatar
    123 posts
    Member since:
    Jan 2016

    Posted 31 Oct in reply to Yaroslav Link to this post

    I bag your pardon. In the case of panning of the chart the user sets the mouse cursor over the chart, presses the right mouse button and while holding it, drags the chart from LEFT to RIGHT (but not from right to left as I wrote with mistake above) until the chart will be moved until the required distance. I bag you pardon for this mistake.
  3. UI for WPF is Visual Studio 2017 Ready
  4. Yaroslav
    Yaroslav avatar
    123 posts
    Member since:
    Jan 2016

    Posted 01 Nov in reply to Yaroslav Link to this post

    And I get error again: for panning or scroling the chart user must press LEFT mouse button, not right one!
  5. Yaroslav
    Yaroslav avatar
    123 posts
    Member since:
    Jan 2016

    Posted 03 Nov in reply to Yaroslav Link to this post

    I bag your pardon, but with your permission, I will mention here two more notes:

    1) If the chart is displaying the latest data, when new data come in, the viewport will be updated to continue displaying the latest data.  In other words, the chart will scroll when new data arrives.

    2) If the chart has been scrolled back to display older data, when new data come in, the viewport will be updated so that the chart will remain unchanged. This is to allow the user to view older data without the chart constantly moving.

    Now, I'll make an attempt to create such a chart by myself, but, so far, it has not met with great success. If you give me the example, it'll be great!
  6. Martin
    Admin
    Martin avatar
    1101 posts

    Posted 03 Nov Link to this post

    Hi Yaroslav,

    To allow panning and zooming in the chart you can use its ChartPanAndZoomBehavior. This way you will easily achieve your main goal. However, to view older data without updating the chart you will need to implement additional code. You can see such approach demonstrated in the attached project. Note that this is not a feature supported by the chart out of the box and the provided code is merely a suggestion how to achieve your requirement. I hope it helps.

    Regards,
    Martin
    Telerik by Progress
    Do you need help with upgrading your WPF project? Try the Telerik API Analyzer and share your thoughts!
  7. Yaroslav
    Yaroslav avatar
    123 posts
    Member since:
    Jan 2016

    Posted 03 Nov in reply to Martin Link to this post

    Hello, Martin. Thank you very much for your example. Now I'm parsing its source code. I'm not closing the post yet. Thank you very much for your help and support again.
  8. Yaroslav
    Yaroslav avatar
    123 posts
    Member since:
    Jan 2016

    Posted 07 Nov in reply to Yaroslav Link to this post

    Hello, Martin. I'm not clear about the next lines in ChartVM.UpdateAxisMinMax method:

    if (this.visibleMax.Ticks - this.visibleMin.Ticks < 5 * TimeSpan.TicksPerSecond)
    {
         this.Max = this.visibleMin.AddSeconds(5);
    }

    Why do you compair this.visibleMin.Ticks with 50000000 ? Below I place this your method entirely.

    private void UpdateAxisMinMax()
    {
        if (this.mainVM.Data.Count != 0)
        {
            this.Min = this.mainVM.Data[0].Date;
            this.Max = this.mainVM.Data[this.mainVM.Data.Count - 1].Date;
     
            if (this.visibleMin < this.min)
            {
                this.VisibleMin = this.min;
            }
            // I don't understand just the following lines - in 'if' conditional staement and in the body of this statement.
            if (this.visibleMax.Ticks - this.visibleMin.Ticks < 5 * TimeSpan.TicksPerSecond)
            {
                this.Max = this.visibleMin.AddSeconds(5);
            }
     
            if (this.isPlaying)
            {
                this.Play();
            }
        }
    }

    Please help me understand.

  9. Martin
    Admin
    Martin avatar
    1101 posts

    Posted 07 Nov Link to this post

    Hello Yaroslav,

    This part of the code ensures that the range of the axis will always be at least 5 seconds long.

    The "visibleMax" and "visibleMin" DateTime objects hold information about the visible range in the chart. It is a bit easier to work with their Ticks so we are using them to get the visible time range (this.visibleMax.Ticks - this.visibleMin.Ticks). After this we check if the range's Ticks value is lower then 5000 ticks, which is five seconds (5 * TimeSpan.TicksPerSecond). And if this condition is met (the axis range is lower than 5 seconds), we set the maximum of the axis to be equal to its visible minimum value + 5 seconds. 

    Regards,
    Martin
    Telerik by Progress
    Do you need help with upgrading your WPF project? Try the Telerik API Analyzer and share your thoughts!
  10. Yaroslav
    Yaroslav avatar
    123 posts
    Member since:
    Jan 2016

    Posted 07 Nov in reply to Martin Link to this post

    So, the width of the viewing area of the chart is bound by 5 seconds ?
  11. Martin
    Admin
    Martin avatar
    1101 posts

    Posted 07 Nov Link to this post

    Hi Yaroslav,

    The axis range minimum is 5 seconds, not the viewing area of the chart. The range of the chart's horizontal axis (the DateTime axis) is bound to the min and max properties of the view model. And when the data points collection is changed (add or remove data points), the minimum gets the first data item in the ItemsSource collection of the series and set its Date to be the minimum of the axis. Then it does the same for the last data item, and sets the maximum of the axis. In other words, the range of the horizontal axis (the viewing area if you like) is determined by the first and the last data item in the collection. If, for some reason the DateTime range between the first and the last data item is less than 5 seconds, we ensure that we set it up to 5 seconds. So, we have a minimum range of 5 seconds. This is an application logic and you can change it if you like.

    Regards,
    Martin
    Telerik by Progress
    Do you need help with upgrading your WPF project? Try the Telerik API Analyzer and share your thoughts!
  12. Yaroslav
    Yaroslav avatar
    123 posts
    Member since:
    Jan 2016

    Posted 23 Nov in reply to Martin Link to this post

    Hello, Martin. I bag your pardon fo delays in my answering on your replys due to many tasks which I have in my job. Lets put aside for a while yet our topic about displaying of historical data in RadCartesianChart because I'm occupying now in the other tasks. When I'll have time for displaying of historical data in RadCartesianChart I'll let you know.
Back to Top
UI for WPF is Visual Studio 2017 Ready