Live Chart (Streamed Data) Update Using Oxyplot

In this post, we will look into stream the data over the Oxyplot. In other words, the new data should be always displayed and the old data moves out of screen. These are useful when you want to observe live data, say coming from a sensor. Consider the following screenshot, this is the behavior we would like to emulate.

Live Chart Using Oxyplot

We will begin by setting the stage - a demo code to generate random data at regular intervals.

public BindableCollection < SensorData > SensorData {
    get;
    set;
}
private DispatcherTimer ? _timer;
private Random _randomGenerator;
public void StartAcquisition() {
    if (_timer is null) {
        _timer = new DispatcherTimer {
            Interval = TimeSpan.FromMilliseconds(500),
        };
        _timer.Tick += MockSensorRecievedData;
    }
    _timer.Start();
    NotifyOfPropertyChange(nameof(CanStartAcquisition));
    NotifyOfPropertyChange(nameof(CanStopAcquisition));
}
private void MockSensorRecievedData(object ? sender, EventArgs e) {
    SensorData.Add(new() {
        TimeStamp = DateTime.Now,
            Data = _randomGenerator.NextDouble()
    });
}

Where SensorData is defined as

public class SensorData {
    public DateTime TimeStamp {
        get;
        set;
    }
    public double Data {
        get;
        set;
    }
}

We are using a DispatchTimer to schedule data generation at regular intervals. Nothing fancy so far, as we haven't added our Chart control yet. So let us now go ahead and add our Oxyplot Chart control in our View now.

<oxy:PlotView Grid.Column="0" Model="{Binding SensorPlotModel}"></oxy:PlotView>

As seen in the view, the PlotView is bound to SensorPlotModel. We can now go back to our ViewModel and add/configure our PlotModel.

public PlotModel SensorPlotModel {
    get;
    set;
}
private
const int MaxSecondsToShow = 20;
public void InitializePlotModel() {
    SensorPlotModel = new() {
        Title = "Demo Live Tracking",
    };
    SensorPlotModel.Axes.Add(new DateTimeAxis {
        Title = "TimeStamp",
            Position = AxisPosition.Bottom,
            StringFormat = "HH:mm:ss",
            IntervalLength = 60,
            Minimum = DateTimeAxis.ToDouble(DateTime.Now),
            Maximum = DateTimeAxis.ToDouble(DateTime.Now.AddSeconds(MaxSecondsToShow)),
            IsPanEnabled = true,
            IsZoomEnabled = true,
            IntervalType = DateTimeIntervalType.Seconds,
            MajorGridlineStyle = LineStyle.Solid,
            MinorGridlineStyle = LineStyle.Solid,
    });
    SensorPlotModel.Axes.Add(new LinearAxis {
        Title = "Data Value",
            Position = AxisPosition.Left,
            IsPanEnabled = true,
            IsZoomEnabled = true,
            Minimum = 0,
            Maximum = 1
    });
    SensorPlotModel.Series.Add(new LineSeries() {
        MarkerType = MarkerType.Circle,
    });
}

We have defined two axis for our PlotModel - a Linear Axis and a DateTime Axis, reflecting the Data and TimeStamp values in our SensorData. Notice we have also set the Minimum and Maximum values for each axis. We want to restrict the amount of data that can be viewed in the graph at a time. We will, as the new data comes in, ensure this range is maintained.

This can be done by observing the changes in our Observable Collection, SensorData.

private void SensorData_CollectionChanged(object ? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) {
    if (e.NewItems is null) return;
    var series = SensorPlotModel.Series.OfType < LineSeries > ().First();
    var dateTimeAxis = SensorPlotModel.Axes.OfType < DateTimeAxis > ().First();
    if (!series.Points.Any()) {
        dateTimeAxis.Minimum = DateTimeAxis.ToDouble(DateTime.Now);
        dateTimeAxis.Maximum = DateTimeAxis.ToDouble(DateTime.Now.AddSeconds(MaxSecondsToShow));
    }
    foreach(var newItem in e.NewItems) {
        if (newItem is SensorData sensorData) {
            series.Points.Add(new DataPoint(DateTimeAxis.ToDouble(sensorData.TimeStamp), sensorData.Data));
        }
    }
    if (DateTimeAxis.ToDateTime(series.Points.Last().X) > DateTimeAxis.ToDateTime(dateTimeAxis.Maximum)) {
        dateTimeAxis.Minimum = DateTimeAxis.ToDouble(DateTime.Now.AddSeconds(-1 * MaxSecondsToShow));
        dateTimeAxis.Maximum = DateTimeAxis.ToDouble(DateTime.Now);
        dateTimeAxis.Reset();
    }
    SensorPlotModel.InvalidatePlot(true);
    NotifyOfPropertyChange(nameof(SensorPlotModel));
}

Let us go through the code here. Each time new data is added to SensorData we are adding points to the only series (in our case, a LinearSeries) in the PlotModel. More importantly, we do something else. We check if the new DateTime (associated with the last point) exceeds the Maximum associated with the DateTimeAxis. If so, we reset the Axis to include the new data, and excluding some of the old data.

if (DateTimeAxis.ToDateTime(series.Points.Last().X) > DateTimeAxis.ToDateTime(dateTimeAxis.Maximum)) {
    dateTimeAxis.Minimum = DateTimeAxis.ToDouble(DateTime.Now.AddSeconds(-1 * MaxSecondsToShow));
    dateTimeAxis.Maximum = DateTimeAxis.ToDouble(DateTime.Now);
    dateTimeAxis.Reset();
}

Do note that ToDateTime() conversion here is not really needed. You could have compared with the Double values.

if (series.Points.Last().X > dateTimeAxis.Maximum)

But, conversion to DateTime at least to me makes it more readable.

In either case, that's the last bit magic we needed to ensure our chart control streams data. If you are interested in the complete code in this example, you can access the same at my Github.