LiveCharts WPF 实时数据慢。改进 LiveCharts 实时绘图性能
LiveCharts WPF Slow with live data. Improve LiveCharts real-time plotting performance
我正在研究在 WPF 应用程序中使用 LiveChart 以实时绘制温度测量值。
我整理了一个简单的折线图示例,以 10Hz 的频率读取数据,并为每个样本重新绘制。但是,我发现重绘率约为 1Hz。这对于 WPF Live 图表工具来说似乎非常慢。
我的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 处理的。
谢谢。
更新。 Oxyplot 通过绑定到 DataPoints 的 ObservableCollection 产生了如下所示的预期结果。使用 LiveCharts 获得相同的性能会很好,因为它具有非常好的美学。
该库实现得相当差。有一个付费版本宣传自己比免费版本更高效。我还没有测试付费版本。免费版的图表控件非常慢,尤其是在处理庞大的数据集时。
显然,默认CartesianChart.AnimationSpeed
默认设置为500ms。在实时场景中将绘图速率提高到 1/450 毫秒以上将导致“丢失”帧。 “丢失”意味着数据最终可见,但不是实时绘制的。每个布局失效的渲染过程花费的时间太长。
超过 450 毫秒将使情节显得滞后(由于跳帧)。这是执行不力的结果。超过 500 毫秒的默认动画速度时应禁用动画。
无论如何,为了显着超过 450 毫秒,您可以采取一些措施来提高整体性能:
- 使用
ObservablePoint
或 ObservableValue
或者通常让您的数据类型实现 INotifyPropertyChanged
。修改 fix/immutable 组数据项而不是修改源集合(例如,通过 adding/removing 项。 时,您可能会获得更好的结果
- 通过将
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
允许禁用像素捕捉并修改渲染比例。低于 1
的 BitmapCache.RenderAtScale
值会增加模糊度,但也会增加 UIElement
. 的渲染性能
以下示例通过将一组固定的 360 个值中的每个 ObservablePoint
值向左移动来实时绘制正弦图。应用了所有建议的性能调整,从而在 1/10 毫秒(100 赫兹)的绘图速率下产生了可接受的平滑度。您可以使用 1/50 毫秒到 1/200 毫秒之间的值,如果仍然可以接受,甚至低于 1/10 毫秒。
请注意,默认的 Windows 计时器以 15.6 毫秒的分辨率运行。这意味着 < 1/100ms 的值将导致渲染停顿,例如鼠标被移动。设备输入具有优先权,将使用相同的计时器处理。您需要找到为框架留出足够时间来处理 UI 输入的绘图速率。
强烈建议调整您的采样率以匹配绘图率以避免滞后感。或者实施生产者-消费者模式以避免 loosing/skipping 数据读取。
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>
Chart.DisableAnimations = true 对我有用。试试这个,即使启用了工具提示和其他参数,这也会大大提高性能。
我正在研究在 WPF 应用程序中使用 LiveChart 以实时绘制温度测量值。 我整理了一个简单的折线图示例,以 10Hz 的频率读取数据,并为每个样本重新绘制。但是,我发现重绘率约为 1Hz。这对于 WPF Live 图表工具来说似乎非常慢。 我的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 处理的。
谢谢。
更新。 Oxyplot 通过绑定到 DataPoints 的 ObservableCollection 产生了如下所示的预期结果。使用 LiveCharts 获得相同的性能会很好,因为它具有非常好的美学。
该库实现得相当差。有一个付费版本宣传自己比免费版本更高效。我还没有测试付费版本。免费版的图表控件非常慢,尤其是在处理庞大的数据集时。
显然,默认CartesianChart.AnimationSpeed
默认设置为500ms。在实时场景中将绘图速率提高到 1/450 毫秒以上将导致“丢失”帧。 “丢失”意味着数据最终可见,但不是实时绘制的。每个布局失效的渲染过程花费的时间太长。
超过 450 毫秒将使情节显得滞后(由于跳帧)。这是执行不力的结果。超过 500 毫秒的默认动画速度时应禁用动画。
无论如何,为了显着超过 450 毫秒,您可以采取一些措施来提高整体性能:
- 使用
ObservablePoint
或ObservableValue
或者通常让您的数据类型实现INotifyPropertyChanged
。修改 fix/immutable 组数据项而不是修改源集合(例如,通过 adding/removing 项。 时,您可能会获得更好的结果
- 通过将
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
允许禁用像素捕捉并修改渲染比例。低于1
的BitmapCache.RenderAtScale
值会增加模糊度,但也会增加UIElement
. 的渲染性能
以下示例通过将一组固定的 360 个值中的每个 ObservablePoint
值向左移动来实时绘制正弦图。应用了所有建议的性能调整,从而在 1/10 毫秒(100 赫兹)的绘图速率下产生了可接受的平滑度。您可以使用 1/50 毫秒到 1/200 毫秒之间的值,如果仍然可以接受,甚至低于 1/10 毫秒。
请注意,默认的 Windows 计时器以 15.6 毫秒的分辨率运行。这意味着 < 1/100ms 的值将导致渲染停顿,例如鼠标被移动。设备输入具有优先权,将使用相同的计时器处理。您需要找到为框架留出足够时间来处理 UI 输入的绘图速率。
强烈建议调整您的采样率以匹配绘图率以避免滞后感。或者实施生产者-消费者模式以避免 loosing/skipping 数据读取。
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>
Chart.DisableAnimations = true 对我有用。试试这个,即使启用了工具提示和其他参数,这也会大大提高性能。