问题
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
orObservableValue
or generally let your data type implementINotifyPropertyChanged
. 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
tonull
. This will remove additional rendering elements. The line stroke itself will remain visible. This will significantly improve performance. - Set
Chart.Hoverable
tofalse
to disable mouse over effects. - Set
Chart.DataTooltip
to{x:Null}
to disable creation of tool tip objects. - Set
Chart.DisableAnimations
totrue
. Disabling animations will significantly improve the rendering performance. Alternatively disable animations selective for each axis by settingAxis.DisableAnimations
. - Set
Axis.MinValue
andAxis.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 aBitmapCache
allows to disable pixel snapping and to modify the render scaling. ABitmapCache.RenderAtScale
value below1
increases blurriness, but also rendering performance of theUIElement
.
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