LiveCharts WPF Slow with live data. Improve LiveCharts real-time plotting performance
Asked Answered
R

2

12

I am investigating the use of LiveChart within a WPF application for the purpose of plotting in real time, temperature measurements. I have put together a simple line chart example to read data at 10Hz, and redraw for every sample. However, I am finding that the redraw rate is around 1Hz. This seems very slow for a WPF Live charting tool. My xaml is as follows :

<lvc:CartesianChart x:Name="TemperatureChart" Grid.Row="1" LegendLocation="Right" Hoverable="False" DataTooltip="{x:Null}">
    <lvc:CartesianChart.Series>
        <lvc:LineSeries x:Name="TempDataSeries" Values="{Binding TemperatureData}"></lvc:LineSeries>
    </lvc:CartesianChart.Series>
</lvc:CartesianChart>

And snippets from my view Model is as follows :

ChartValues<ObservableValue> _temperatureData = new ChartValues<ObservableValue>();

public ChartValues<ObservableValue> TemperatureData
{
    get => this._temperatureData;
    set => this._temperatureData = value;
}

void Initialise()
{
    _temperatureMonitor.Subscribe(ProcessTemperatures);
}

void TestStart()
{
    _temperatureMonitor.Start();
}
void TestStop()
{
    _temperatureMonitor.Stop();
}
void ProcessTemperatures(TemperatureData data)
{
    TemperatureData.Add(data.Temperature);
}

I am not working with a large amount of data, and have tested with a limit of 100 values. I am confident that my thread, which reads the data has little overhead, however the redraw plots around 10 points at a time.

Have I implemented the binding correctly? Do I need to add property notifications to force the update? My understanding was that this was handled by ChartValues.

Thanks.

Update. Oxyplot produced the desired results shown below by binding to an ObservableColllection of DataPoints. It would be nice to get the same performance using LiveCharts, as it has really nice aesthetics.

OxyPlot Capture

Roos answered 28/7, 2020 at 16:9 Comment(1)
It has been some time since I used LiveCharts, but AFAIR their high performance pack (Geared) was sold as a premium package. With it I was able to draw around 240 new points per second without any issues. But then it has been 5 years, so I do not remember the details much.Sled
I
13

The library is rather poorly implemented. There is a paid version which advertises itself to be more performant than the free version. I have not tested the paid version. The chart controls of the free version are very slow, especially when dealing with huge data sets.

Apparently, the default CartesianChart.AnimationSpeed is set to 500ms by default. Increasing the plotting rate above 1/450ms in a real-time scenario will result in "lost" frames. "Lost" means the data is finally visible, but not drawn in real-time. The rendering pass of each layout invalidation just takes too long.
Going beyond 450ms will make the plot appear laggy (due to the skipped frames). This is a result of the poor implementation. Animation should be disabled when going beyond the default animation speed of 500ms.

Anyway, there are a few things you can do to improve the overall performance in order to go significantly beyond the 450ms:

  • Use ObservablePoint or ObservableValue or generally let your data type implement INotifyPropertyChanged. You may achieve better results when modifying a fix/immutable set of data items instead of modifying the source collection e.g., by adding/removing items.
  • Remove the graph's actual visual point elements by setting LineSeries.PointGeometry to null. This will remove additional rendering elements. The line stroke itself will remain visible. This will significantly improve performance.
  • Set Chart.Hoverable to false to disable mouse over effects.
  • Set Chart.DataTooltip to {x:Null} to disable creation of tool tip objects.
  • Set Chart.DisableAnimations to true. Disabling animations will significantly improve the rendering performance. Alternatively disable animations selective for each axis by setting Axis.DisableAnimations.
  • Set Axis.MinValue and Axis.MaxValue to disable automatic scaling on each value change. In most scenarios where the x-axis values change you have to adjust both properties in real-time too.
  • Set Axis.Unit also significantly improves appearance on re-rendering.
  • Set UIElement.CacheMode on the chart object. Using a BitmapCache allows to disable pixel snapping and to modify the render scaling. A BitmapCache.RenderAtScale value below 1 increases blurriness, but also rendering performance of the UIElement.

The following example plots a sine graph in real-time by shifting each ObservablePoint value of a fixed set of 360 values to the left. All suggested performance tweaks are applied, which results in an acceptable smoothness at a plotting rate of 1/10ms (100Hz). You can play around with values between 1/50ms and 1/200ms or even go below 1/10ms if this is still acceptable.
Note that the default Windows timer operates at a resolution of 15.6ms. This means values < 1/100ms will result in rendering stalls, when e.g. the mouse is moved. The device input has precedence and will be handled using the same timer. You need to find the plotting rate which leaves enough time for the framework to handle UI input.

It's highly recommended to adjust your sample rate to match the plotting rate to avoid the laggy feel. Alternatively implement the Producer-consumer pattern to avoid loosing/skipping data readings.

plotting rate 1/10ms - gif reduces smoothness

DataModel.cs

public class DataModel : INotifyPropertyChanged
{
  public DataModel()
  {
    this.ChartValues = new ChartValues<ObservablePoint>();
    this.XMax = 360;
    this.XMin = 0;

    // Initialize the sine graph
    for (double x = this.XMin; x <= this.XMax; x++)
    {
      var point = new ObservablePoint() 
      { 
        X = x, 
        Y = Math.Sin(x * Math.PI / 180) 
      };
      this.ChartValues.Add(point);
    }

    // Setup the data mapper
    this.DataMapper = new CartesianMapper<ObservablePoint>()
      .X(point => point.X)
      .Y(point => point.Y)
      .Stroke(point => point.Y > 0.3 ? Brushes.Red : Brushes.LightGreen)
      .Fill(point => point.Y > 0.3 ? Brushes.Red : Brushes.LightGreen);

    // Setup the IProgress<T> instance in order to update the chart (UI thread)
    // from the background thread 
    var progressReporter = new Progress<double>(newValue => ShiftValuesToTheLeft(newValue, CancellationToken.None));

    // Generate the new data points on a background thread 
    // and use the IProgress<T> instance to update the chart on the UI thread
    Task.Run(async () => await StartSineGenerator(progressReporter, CancellationToken.None));
  }

  // Dynamically add new data
  private void ShiftValuesToTheLeft(double newValue, CancellationToken cancellationToken)
  {
    // Shift item data (and not the items) to the left
    for (var index = 0; index < this.ChartValues.Count - 1; index++)
    {
      cancellationToken.ThrowIfCancellationRequested();

      ObservablePoint currentPoint = this.ChartValues[index];
      ObservablePoint nextPoint = this.ChartValues[index + 1];
      currentPoint.X = nextPoint.X;
      currentPoint.Y = nextPoint.Y;
    }

    // Add the new reading
    ObservablePoint newPoint = this.ChartValues[this.ChartValues.Count - 1];
    newPoint.X = newValue;
    newPoint.Y = Math.Sin(newValue * Math.PI / 180);

    // Update axis min/max
    this.XMax = newValue;
    this.XMin = this.ChartValues[0].X;
  }

  private async Task StartSineGenerator(IProgress<double> progressReporter, CancellationToken cancellationToken)
  {
    while (true)
    {
      // Add the new reading by posting the callback to the UI thread
      ObservablePoint newPoint = this.ChartValues[this.ChartValues.Count - 1];
      double newXValue = newPoint.X + 1;
      progressReporter.Report(newXValue);

      // Check if CancellationToken.Cancel() was called 
      cancellationToken.ThrowIfCancellationRequested();

      // Plot at 1/10ms
      await Task.Delay(TimeSpan.FromMilliseconds(10), cancellationToken);
    }
  }

  private double xMax;
  public double XMax
  {
    get => this.xMax;
    set
    {
      this.xMax = value;
      OnPropertyChanged();
    }
  }

  private double xMin;
  public double XMin
  {
    get => this.xMin;
    set
    {
      this.xMin = value;
      OnPropertyChanged();
    }
  }

  private object dataMapper;   
  public object DataMapper
  {
    get => this.dataMapper;
    set 
    { 
      this.dataMapper = value; 
      OnPropertyChanged();
    }
  }

  public ChartValues<ObservablePoint> ChartValues { get; set; }
  public Func<double, string> LabelFormatter => value => value.ToString("F");

  public event PropertyChangedEventHandler PropertyChanged;
  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

MainWIndow.xaml

<Window>
  <Window.DataContext>
    <DataModel />
  </Window.DataContext>

  <CartesianChart Height="500" 
                  Zoom="None"  
                  Hoverable="False" 
                  DataTooltip="{x:Null}" 
                  DisableAnimations="True">
    <wpf:CartesianChart.Series>
      <wpf:LineSeries PointGeometry="{x:Null}"
                      Title="Sine Graph"
                      Values="{Binding ChartValues}"
                      Configuration="{Binding DataMapper}"/>
    </wpf:CartesianChart.Series>

    <CartesianChart.CacheMode>
      <BitmapCache EnableClearType="False" 
                   RenderAtScale="1"
                   SnapsToDevicePixels="False" />
    </CartesianChart.CacheMode>

    <CartesianChart.AxisY>
      <Axis Title="Sin(X)"
            FontSize="14" 
            Unit="1"
            MaxValue="1.1"
            MinValue="-1.1" 
            DisableAnimations="True"
            LabelFormatter="{Binding LabelFormatter}"
            Foreground="PaleVioletRed" />
    </CartesianChart.AxisY>

    <CartesianChart.AxisX>
      <Axis Title="X" 
            DisableAnimations="True" 
            FontSize="14" 
            Unit="1"
            MaxValue="{Binding XMax}"
            MinValue="{Binding XMin}"
            Foreground="PaleVioletRed" />
    </CartesianChart.AxisX>
  </CartesianChart>
</Window>
Isla answered 1/8, 2020 at 16:14 Comment(6)
Thanks very much for your answer. This tells me a lot. I think for my needs I need to use a different chart tool since I only want a basic 2D chart to plot data at 10Hz, which shouldn't be too demanding. Oxyplot works well for my needs. LiveChart has nice cosmetics but to achieve reasonable performance requires the paid version. I'm sure there are much better paid tools for live performance such as SciChart.Roos
10Hz would mean to plot data every 100ms. LiveCharts can handle this (dependeing on how smooth you need it). You can copy the above code and set the delay to 100ms: await Task.Delay(TimeSpan.FromMilliseconds(100)); and see if it is satisfying. I agree that the charts look great. But the handling is rather bad. From a WPF MVVM point of view it is obviously implemented by developers with a MVC background. It's years ago, but when using OxyPlot I found it to be even worse. The handling felt like a WinForms style designed control or library forcing to write bad WPF code. Maybe it has changed.Isla
Very informative, and unfortunate it is not easier to get a smooth update.Cosmism
@Roos Scichart is really nice but sadly very expensive compared to livechartsSled
Thanks for this sample. I have tried it and unfortunately it seems LiveCharts has a huge memory leak problem.Vigilante
It wouldn't surprise me. Why do you think you have a leak?Isla
T
2

Chart.DisableAnimations = true works for me. Try this, even if tooltip and other parameters are enabled this improves performance a lot.

Theo answered 27/9, 2021 at 10:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.