首页 > 解决方案 > LiveCharts WPF 使用实时数据慢。提高 LiveCharts 实时绘图性能

问题描述

我正在研究在 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 获得相同的性能会很好,因为它具有非常好的美学。

氧绘图捕获

标签: c#wpfperformanceredrawlivecharts

解决方案


该库实施得相当差。有一个付费版本宣传自己比免费版本更高效。我还没有测试过付费版本。免费版的图表控件非常慢,尤其是在处理庞大的数据集时。

显然,默认情况下默认CartesianChart.AnimationSpeed设置为 500ms。在实时场景中将绘图速率提高到 1/450 毫秒以上将导致“丢失”帧。“丢失”意味着数据最终可见,但不是实时绘制的。每个布局失效的渲染过程只需要太长时间。
超过 450 毫秒会使情节显得滞后(由于跳过的帧)。这是执行不力的结果。超过默认动画速度 500 毫秒时应禁用动画。

无论如何,您可以采取一些措施来提高整体性能,以显着超过 450 毫秒:

  • 使用ObservablePointorObservableValue或者一般让你的数据类型实现INotifyPropertyChanged。当修改一组固定/不可变的数据项而不是修改源集合时,您可能会获得更好的结果,例如,通过添加/删除项。
  • LineSeries.PointGeometry通过设置为删除图形的实际视觉点元素null。这将删除额外的渲染元素。线条笔触本身将保持可见。这将显着提高性能。
  • 设置Chart.Hoverablefalse禁用鼠标悬停效果。
  • 设置Chart.DataTooltip{x:Null}禁用工具提示对象的创建。
  • 设置Chart.DisableAnimationstrue。禁用动画将显着提高渲染性能。或者通过设置禁用每个轴的选择性动画Axis.DisableAnimations
  • 设置Axis.MinValueAxis.MaxValue禁用每个值更改时的自动缩放。在 x 轴值发生变化的大多数情况下,您也必须实时调整这两个属性。
  • SetAxis.Unit还显着改善了重新渲染时的外观。
  • 设置UIElement.CacheMode在图表对象上。使用 aBitmapCache允许禁用像素捕捉并修改渲染缩放。BitmapCache.RenderAtScale低于的值1会增加模糊度,但也会增加UIElement.

ObservablePoint以下示例通过将一组固定的 360 值中的每个值向左移动来实时绘制正弦图。应用了所有建议的性能调整,从而在 1/10ms (100Hz) 的绘图速率下产生可接受的平滑度。您可以使用 1/50ms 和 1/200ms 之间的值,如果这仍然可以接受,甚至可以低于 1/10ms。
请注意,默认的 Windows 计时器以 15.6 毫秒的分辨率运行。这意味着值 < 1/100ms 将导致渲染停止,例如当鼠标移动时。设备输入具有优先权,将使用相同的计时器进行处理。您需要找到为框架处理 UI 输入留出足够时间的绘图速率。

强烈建议调整您的采样率以匹配绘图率以避免滞后感。或者实现生产者-消费者模式以避免丢失/跳过数据读数。

绘图速率 1/10ms - gif 降低平滑度

数据模型.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));
}

主窗口.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>

推荐阅读