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

ぃ、小莉子 提交于 2021-01-21 11:13:36

问题


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.


回答1:


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.

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>


来源:https://stackoverflow.com/questions/63138397/livecharts-wpf-slow-with-live-data-improve-livecharts-real-time-plotting-perfor

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!