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.
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.