- 数据可视化
- 图表库
- 跨平台
【免费下载链接】LiveCharts2
Beautiful, interactive charts, maps, and gauges. One API for every .NET UI framework.
导读
实时刷新是数据可视化中最常见的需求之一:传感器数据、行情曲线、监控指标每秒都在变化,图表需要在不卡顿、不闪烁的前提下持续追加新数据并滚动显示。本篇以 LiveCharts2 仓库中的General/RealTime示例为核心,完整讲解其数据生产端(后台任务 +ObservableCollection)、时间轴刻度管理(CustomSeparators+DateFormatter+Interval)、跨线程更新(SyncContext)以及各平台(Avalonia、WPF、MAUI、Blazor、Eto)的视图写法,并结合src/LiveChartsCore下的源码揭示底层实现原理,读完后你可以直接在自己的 .NET 项目中复刻一套流畅的实时曲线。
本文对应的示例文档由仓库文档系统从共享模板与示例源码自动渲染生成,模板机制见 docs/readme.md,共享模板见 docs/shared/genericSampleJustGif.md。
示例效果:每一秒都在滚动的曲线
该示例的运行结果是一张 624×281 的折线图:X 轴以秒为刻度滚动前进,Y 轴在 0~10 之间随机波动,图上始终保留最近约 250 个数据点,最新的刻度标签显示为 "now",其余刻度显示为 "Xs ago":
要实现这样的效果,需要同时解决三个问题:数据从哪里来、时间轴如何跟着走、跨线程更新如何保证线程安全。下面逐一展开。
一、数据生产端:ViewModel 中的后台数据循环
示例的 ViewModel 位于 samples/ViewModelsSamples/General/RealTime/ViewModel.cs,它同时被各平台视图复用(这也是 LiveCharts2 各平台共享同一套 ViewModel 的设计),完整代码如下:
using System; using System.Collections.ObjectModel; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using LiveChartsCore.Defaults; namespace ViewModelsSamples.General.RealTime; public partial class ViewModel : ObservableObject { public ViewModel() { _ = ReadData(); } [ObservableProperty] public partial double[] Separators { get; set; } = []; public ObservableCollection<DateTimePoint> Values { get; set; } = []; public Func<DateTime, string> LabelsFormatter { get; } = Formatter; public object Sync { get; } = new object(); public bool IsReading { get; set; } = true; private async Task ReadData() { var random = new Random(); // to keep this sample simple, we run the next infinite loop // in a real application you should stop the loop/task when the view is disposed while (IsReading) { await Task.Delay(100); // Because we are updating the chart from a different thread // we need to use a lock to access the chart data. // this is not necessary if your changes are made on the UI thread. lock (Sync) { Values.Add(new DateTimePoint(DateTime.Now, random.Next(0, 10))); if (Values.Count > 250) Values.RemoveAt(0); // we need to update the separators every time we add a new point Separators = GetSeparators(); } } } private static double[] GetSeparators() { var now = DateTime.Now; return [ now.AddSeconds(-25).Ticks, now.AddSeconds(-20).Ticks, now.AddSeconds(-15).Ticks, now.AddSeconds(-10).Ticks, now.AddSeconds(-5).Ticks, now.Ticks ]; } private static string Formatter(DateTime date) { var secsAgo = (DateTime.Now - date).TotalSeconds; return secsAgo < 1 ? "now" : $"{secsAgo:N0}s ago"; } }1. 数据载体:DateTimePoint
Values的类型是ObservableCollection<DateTimePoint>,其中DateTimePoint定义在 src/LiveChartsCore/Defaults/DateTimePoint.cs:
- 它实现了
IChartEntity与INotifyPropertyChanged,坐标由DateTime.Ticks(X 轴)和Value(Y 轴)组成; - 属性变更时
OnPropertyChanged()会同步重建Coordinate,因此当单个点被修改时图表能感知变化并自动重绘; - 构造时若
Value为null,Coordinate会置为Coordinate.Empty,这与示例中random.Next(0, 10)的取值(始终非空)对应。
2. 生产节奏:100ms 循环 + 250 点滑动窗口
ReadData()是一个async Task循环:每 100msawait Task.Delay(100)追加一个新点;当Values.Count > 250时从头部RemoveAt(0),从而形成一个"先进先出"的滑动窗口——这保证了图表始终只渲染最近 250 个点,避免了无限增长导致的内存与绘制压力。示例以 100ms 为节奏,实际应用中你可以按业务需要调整Task.Delay的值,或改为从真实数据源(Socket、事件总线、数据库轮询等)推送。
3. 生命周期说明
while (IsReading)通过布尔开关控制循环,注释明确提醒:真实应用中应在视图销毁时停止循环/任务(如实现IDisposable或在页面OnNavigatedFrom中置IsReading = false),示例为保持简单未做资源清理。
二、时间轴管理:让刻度跟着"现在"走
实时曲线的难点不在画线,而在 X 轴:数据点在不断追加,时间窗口不断前移,刻度、标签、分隔线都必须同步刷新,否则会出现刻度滞留、标签错位的问题。示例通过四个配置协同解决。
1.Interval="0:0:1":固定秒级步长
在 XAML 视图中 X 轴设置Interval="0:0:1"(即 1 秒)。从源码 src/LiveChartsCore/Generators/XamlGeneration.cs 可以看到,XAML 侧的Interval最终被映射为轴的两个底层属性:
public static void OnAxisIntervalChanged( IXamlWrapper<ICartesianAxis> axis, TimeSpan oldValue, TimeSpan newValue) { axis.WrappedObject.UnitWidth = newValue.Ticks; axis.WrappedObject.MinStep = newValue.Ticks; }即UnitWidth = newValue.Ticks(单位宽度)与MinStep = newValue.Ticks(最小步长),用于告诉图表引擎"相邻刻度之间的理想间隔是 1 秒"。
2.DateFormatter:时间标签格式化
DateFormatter="{Binding LabelsFormatter}"绑定到 ViewModel 中的Func<DateTime, string>。在 XamlGeneration.cs 中,该委托被桥接到核心轴的Labeler:
public static void OnDateTimeAxisDateFormatterChanged( IXamlWrapper<ICartesianAxis> axis, Func<DateTime, string> oldValue, Func<DateTime, string> newValue) => axis.WrappedObject.Labeler = value => newValue(value.AsDate());即每个刻度值(内部为double,实际是DateTime.Ticks)都会被转换为DateTime后交给你的格式化函数。示例的Formatter以"距当前时间"为语义输出:
private static string Formatter(DateTime date) { var secsAgo = (DateTime.Now - date).TotalSeconds; return secsAgo < 1 ? "now" : $"{secsAgo:N0}s ago"; }效果是:最新刻度显示 "now",往前依次显示 "1s ago"、"2s ago"……这比打印绝对时间更直观,也是实时监控场景的常用做法。
3.CustomSeparators:动态接管刻度集合
这是实时场景的关键配置。默认情况下轴会根据数据范围与MinStep自动枚举刻度;但在滑动窗口场景下,自动枚举可能产生非整秒的刻度位置。示例改为完全自定义:
private static double[] GetSeparators() { var now = DateTime.Now; return [ now.AddSeconds(-25).Ticks, now.AddSeconds(-20).Ticks, now.AddSeconds(-15).Ticks, now.AddSeconds(-10).Ticks, now.AddSeconds(-5).Ticks, now.Ticks ]; }它返回以"当前时刻"为终点的 6 个整秒刻度(-25s、-20s、-15s、-10s、-5s、0s),且每次追加数据后都会重新计算并赋值给Separators([ObservableProperty]保证属性变更自动通知 X 轴)。于是刻度集合每一秒都整体前移一格,曲线看起来就像"匀速滚动"。
底层逻辑在 src/LiveChartsCore/CoreAxis.cs 的EnumerateSeparators中:
private IEnumerable<double> EnumerateSeparators(double start, double end, double step) { var custom = CustomSeparators; if (custom is not null) { foreach (var s in custom) yield return s; yield break; } // ... 否则按 start/end/step 自动枚举 }可以看到:一旦设置了CustomSeparators,引擎会直接采用你提供的刻度序列,不再执行自动步长枚举(yield break)。这正是实时场景下刻度可控、稳定的来源。CustomSeparators属性定义于 src/LiveChartsCore/CoreAxis.cs,并作为接口约定存在于 IPlane.cs。
4.AnimationsSpeed="0":关闭刻度动画
实时更新时,如果刻度仍参与动画插值,轴标签会"追赶"数据造成拖影。示例将AnimationsSpeed设为 0(TimeSpan.Zero),表示轴刻度不做缓动、立即到位。同理在 Blazor/Eto 等纯代码写法中对应AnimationsSpeed = TimeSpan.FromMilliseconds(0)。
5.SeparatorsPaint:刻度的视觉样式
SeparatorsPaint="{lvc:SolidColorPaint Color='64B4B4B4'}"为分隔线(网格线)指定了半透明灰色画笔。在 src/LiveChartsCore/CoreAxis.cs 中SeparatorsPaint定义了分隔线画笔;绘制逻辑(CoreAxis.cs)会将其挂到CanvasZone.DrawMargin区域。64B4B4B4中的64是 8 位十六进制 Alpha(约 39% 不透明度),B4B4B4为 RGB。
三、跨线程更新:SyncContext与lock
示例的数据循环运行在后台线程(async Task中的Task.Delay之后继续执行的上下文),而图表绘制运行在 UI 线程,因此需要两重保障:
数据访问锁:所有对
Values与Separators的读写都包在lock (Sync)中,避免集合在 UI 线程遍历(如绘制读取)与后台线程追加(Add/RemoveAt)并发时抛出InvalidOperationException。注意ObservableCollection本身不是线程安全的,这正是锁存在的意义。图表的
SyncContext:在视图层,CartesianChart绑定SyncContext="{Binding Sync}",把 ViewModel 中同一个object Sync实例交给图表。从 src/skiasharp/_Shared/SourceGenChart.sgp.cs 可以看到:
static void OnSyncContextChanged(SGChart chart, object oldValue, object newValue) { chart.CoreCanvas.Sync = newValue; chart.CoreChart.Update(); }SyncContext最终被设置为画布(CoreCanvas.Sync)的同步对象。LiveCharts2 的绘制引擎以同一把锁串行化"数据变更 → 画布失效 → 重绘"的完整链路:只要数据修改和图表都使用同一个Sync对象,后台线程写数据与渲染线程读数据就不会互相踩踏。SyncContext属性也作为跨图表类型的统一约定定义在 IChartView.cs。
若你的更新发生在 UI 线程(例如点击事件里追加数据),示例注释指出锁并非必需;但后台任务更新的场景下,
lock+SyncContext缺一不可。
四、各平台视图写法对照
示例的 ViewModel 被所有平台复用,各平台只差视图层的绑定语法。
Avalonia(XAML)
见 samples/AvaloniaSample/General/RealTime/View.axaml,命名空间为lvc="using:LiveChartsCore.SkiaSharpView.Avalonia":
<UserControl ... xmlns:lvc="using:LiveChartsCore.SkiaSharpView.Avalonia" xmlns:vms="using:ViewModelsSamples.General.RealTime" x:DataType="vms:ViewModel"> <UserControl.DataContext> <vms:ViewModel/> </UserControl.DataContext> <lvc:CartesianChart SyncContext="{Binding Sync}"> <lvc:CartesianChart.Series> <lvc:XamlLineSeries Values="{Binding Values}" Fill="{x:Null}" GeometryFill="{x:Null}" GeometryStroke="{x:Null}"/> </lvc:CartesianChart.Series> <lvc:CartesianChart.XAxes> <lvc:XamlDateTimeAxis Interval="0:0:1" DateFormatter="{Binding LabelsFormatter}" AnimationsSpeed="0" SeparatorsPaint="{lvc:SolidColorPaint Color='64B4B4B4'}" CustomSeparators="{Binding Separators}"/> </lvc:CartesianChart.XAxes> </lvc:CartesianChart> </UserControl>WPF 与 MAUI
结构完全一致,仅命名空间不同:WPF 见 samples/WPFSample/General/RealTime/View.xaml(xmlns:lvc="clr-namespace:LiveChartsCore.SkiaSharpView.WPF;assembly=LiveChartsCore.SkiaSharpView.WPF",序列需包裹在lvc:SeriesCollection与lvc:AxesCollection中);MAUI 见 samples/MauiSample/General/RealTime/View.xaml(根元素为ContentPage)。三者均复用同一 ViewModel。
Blazor(Razor 代码块)
Blazor 无法用 XAML,因此改用纯 C# 构建系列与轴,见 samples/BlazorSample/Pages/General/RealTime/View.razor:
series = new ISeries[] { new LineSeries<DateTimePoint> { Values = _values, Fill = null, GeometryFill = null, GeometryStroke = null } }; var xAxis = new Axis { Labeler = value => Formatter(new DateTime((long)value)), AnimationsSpeed = TimeSpan.FromMilliseconds(0), SeparatorsPaint = new SolidColorPaint(SKColors.Gray), CustomSeparators = _separators };注意两点差异:
- 核心轴使用
Labeler(Func<double, string>,需要手动把double刻度转回DateTime),而 XAML 侧的DateFormatter是它的"DateTime 友好"封装; - 每次循环更新刻度时,除了给
_separators赋值,还要显式回写xAxis.CustomSeparators = _separators;(见该文件中ReadData方法),确保轴感知新刻度集合。
Eto.Forms(纯代码)
见 samples/EtoFormsSample/General/RealTime/View.cs,使用CartesianChart的Series与XAxes属性直接赋ISeries[]和Axis[],模式与 Blazor 相同。WinForms 对应实现见 samples/WinFormsSample/General/RealTime/View.cs。
五、要点小结与落地清单
在自有项目中实现实时曲线,按以下清单核对即可:
- 数据模型:使用
DateTimePoint(或实现IChartEntity/INotifyPropertyChanged的自定义模型)承载(DateTime, double)数据; - 数据窗口:用
ObservableCollection+ 定长滑动窗口(满 250 即RemoveAt(0)),防止无限增长; - 后台循环:
async Task+Task.Delay控制刷新节奏,用IsReading开关并确保视图销毁时停止任务; - 线程安全:数据读写包
lock (Sync),并将同一Sync对象绑定到CartesianChart.SyncContext; - 时间轴:
Interval固定步长、CustomSeparators每次追加点后重新生成(以当前时间为终点的整秒刻度)、DateFormatter/Labeler输出相对时间标签、AnimationsSpeed="0"关闭刻度动画; - 视觉:用
SeparatorsPaint控制分隔线颜色与透明度(如64B4B4B4),关闭系列Fill/GeometryFill/GeometryStroke可得到纯线条的监控风格。
以上每一步都能在本仓库的示例与源码中找到对应实现,可作为你排查实时图表闪烁、卡顿或刻度错乱问题的直接参考。
- 数据可视化
- 图表库
- 跨平台
【免费下载链接】LiveCharts2
Beautiful, interactive charts, maps, and gauges. One API for every .NET UI framework.
相关推荐
LiveCharts2 折线图自动更新实战:基于 ObservableCollection 与 ObservableValue 实现数据驱动的实时刷新
LiveCharts2 折线图自动更新实战:基于 ObservableCollection 与 ObservableValue 实现数据驱动的实时刷新 导读 L
数据可视化图表库跨平台LiveCharts2 日期时间轴(DateTime Scaled Axes)实战指南:用 DateTimeAxis 与 DateTimePoint 绘制时间序列图表
LiveCharts2 日期时间轴(DateTime Scaled Axes)实战指南:用 DateTimeAxis 与 DateTimePoint 绘制时间序
数据可视化图表库跨平台基于实时数据更新与 timeCat 时间轴的 F2 动态折线图实现指南
基于实时数据更新与 timeCat 时间轴的 F2 动态折线图实现指南 导读 :动态折线图是移动端图表库 F2 中展示"数据随时间实时变化"的核心场景之一,广泛
数据可视化前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考