LiveCharts在实时数据下速度缓慢。如何提高LiveCharts的实时绘图性能?

11

我正在调查在WPF应用程序中使用LiveChart来实时绘制温度测量数据的用途。 我已经编写了一个简单的线性图表示例,以10Hz的频率读取数据,并为每个样本重绘。然而,我发现重新绘制速率约为1Hz。对于一款WPF实时图表工具来说,这似乎非常缓慢。 我的XAML如下:

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

而我的视图模型中的片段如下:

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);
}

我没有处理大量数据,测试了最多100个值。我相信我的线程读取数据时开销很小,然而每次重新绘制图表大约涉及10个点。

我是否正确实现了绑定?我需要添加属性通知来强制更新吗?我理解的是这由ChartValues处理。

谢谢。

更新。通过绑定到DataPoints的ObservableColllection,Oxyplot产生了所需的结果,如下图所示。希望使用LiveCharts获得相同的性能,因为它有非常漂亮的外观。

OxyPlot Capture


我已经有一段时间没有使用LiveCharts了,但据我所知,他们的高性能包(Geared)是作为高级套餐销售的。有了它,我能够每秒绘制大约240个新点而不会出现任何问题。但那时已经过去5年了,所以我记不太清楚细节了。 - mcy
2个回答

12
该库实现较差。付费版本声称比免费版本更具性能,但我未测试过付费版本。免费版本的图表控件非常慢,特别是处理大数据集时。
显然,默认的CartesianChart.AnimationSpeed默认设置为500毫秒。在实时场景中将绘图速率提高到1/450毫秒以上将导致“丢失”帧。 “丢失”意味着数据最终可见,但不是实时绘制的。每个布局无效的渲染传递时间太长了。超过450ms会使绘图看起来很卡(由于跳帧)。这是实现不佳的结果。当超过默认动画速度500ms时,应禁用动画。
无论如何,有几件事情可以做以提高整体性能,从而显着超过450ms:
- 使用ObservablePoint或ObservableValue或通常让数据类型实现INotifyPropertyChanged。修改固定/不可变数据项集合而不是通过添加/删除项目等修改源集合可能会取得更好的结果。 - 通过将LineSeries.PointGeometry设置为null来删除图形的实际视觉点元素。这将删除其他渲染元素。线条描边本身仍将可见。这将显著提高性能。 - 将Chart.Hoverable设置为false以禁用鼠标悬停效果。 - 将Chart.DataTooltip设置为{x:Null}以禁用工具提示对象的创建。 - 将Chart.DisableAnimations设置为true。禁用动画将显著提高渲染性能。或者通过设置Axis.DisableAnimations来选择性地禁用每个轴的动画。 - 设置Axis.MinValue和Axis.MaxValue以在每个值更改时禁用自动缩放。在大多数x轴值更改的情况下,您也必须实时调整两个属性。 - 设置Axis.Unit还可以显着改善重新渲染时的外观。 - 在图表对象上设置UIElement.CacheMode。使用BitmapCache允许禁用像素捕捉并修改渲染缩放。 BitmapCache.RenderAtScale值低于1会增加模糊度,但也会提高UIElement的渲染性能。
下面的示例通过将一组固定的360个ObservablePoint值向左移动来实时绘制正弦图。应用了所有建议的性能优化,结果在1/10毫秒(100Hz)的绘制速率下具有可接受的平滑度。您可以尝试在1/50毫秒和1/200毫秒之间或甚至低于1/10毫秒的值。
请注意,默认的Windows计时器的分辨率为15.6毫秒。这意味着小于1/100毫秒的值会导致渲染停顿,例如移动鼠标时。设备输入具有优先权,并将使用相同的计时器进行处理。您需要找到绘图速率,以留出足够的时间让框架处理UI输入。

强烈建议将采样率调整为与绘图速率匹配,以避免感觉卡顿。或者实现生产者-消费者模式,以避免丢失/跳过数据读取。

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>

1
非常感谢您的回答。这让我受益匪浅。我认为根据我的需求,我需要使用不同的图表工具,因为我只需要一个基本的2D图表来绘制10Hz的数据,这应该不会太过苛刻。Oxyplot非常适合我的需求。LiveChart外观很漂亮,但要实现合理的性能需要购买付费版本。我相信有更好的付费工具可以用于实时性能,例如SciChart。 - user5265160
10Hz意味着每100毫秒绘制一次数据。LiveCharts可以处理这个问题(取决于您需要多么平滑)。您可以复制上面的代码并将延迟设置为100毫秒:await Task.Delay(TimeSpan.FromMilliseconds(100));,看看是否令人满意。我同意图表看起来很棒。但是处理方式相当糟糕。从WPF MVVM的角度来看,显然是由具有MVC背景的开发人员实现的。虽然已经过去了几年,但是在使用OxyPlot时,我发现它甚至更糟。处理感觉像是WinForms风格设计的控件或库,强制编写糟糕的WPF代码。也许现在已经改变了。 - BionicCode
非常有用,遗憾的是升级过程不够顺畅。 - Mark Eckdahl
@user5265160 Scichart真的很不错,但与Livecharts相比非常昂贵。 - mcy
1
谢谢这个示例。我已经尝试过了,不幸的是它似乎存在巨大的内存泄漏问题。 - Daniel Ellis
这并不让我感到惊讶。你为什么认为你有泄漏? - BionicCode

2

对于我来说,Chart.DisableAnimations = true很有效。即使启用了工具提示和其他参数,也请尝试此方法,这可以大大提高性能。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接