该库实现较差。付费版本声称比免费版本更具性能,但我未测试过付费版本。免费版本的图表控件非常慢,特别是处理大数据集时。
显然,默认的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](https://istack.dev59.com/sRlih.gif)
DataModel.cs
public class DataModel : INotifyPropertyChanged
{
public DataModel()
{
this.ChartValues = new ChartValues<ObservablePoint>();
this.XMax = 360;
this.XMin = 0;
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);
}
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);
var progressReporter = new Progress<double>(newValue => ShiftValuesToTheLeft(newValue, CancellationToken.None));
Task.Run(async () => await StartSineGenerator(progressReporter, CancellationToken.None));
}
private void ShiftValuesToTheLeft(double newValue, CancellationToken cancellationToken)
{
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;
}
ObservablePoint newPoint = this.ChartValues[this.ChartValues.Count - 1];
newPoint.X = newValue;
newPoint.Y = Math.Sin(newValue * Math.PI / 180);
this.XMax = newValue;
this.XMin = this.ChartValues[0].X;
}
private async Task StartSineGenerator(IProgress<double> progressReporter, CancellationToken cancellationToken)
{
while (true)
{
ObservablePoint newPoint = this.ChartValues[this.ChartValues.Count - 1];
double newXValue = newPoint.X + 1;
progressReporter.Report(newXValue);
cancellationToken.ThrowIfCancellationRequested();
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>