【ScottPlot】使用ScottPlot创建实时动态图 |
您所在的位置:网站首页 › 实时动态是什么 › 【ScottPlot】使用ScottPlot创建实时动态图 |
首先:简单 介绍下Scottplot这个免费的开源图标组件库。 ScottPlot 是一个 .NET 图表组件, 主要有以下特点: 适用范围广:同时适用于 WinForms, WPF, Avalonia, Console, 支持 .NET Framework 4.6.1 及以上, NET Core 2.0 至 .NET 5。上手简单:只需几行代码即可创建折线图、条形图、饼图、散点图等。性能强悍:千万级数据处理无压力, 媲美 Python Matplotlib。可交互:支持用户和图表数据进行交互, 注入灵魂。开源免费:基于MIT开源协议, 已经开源近5年, 不存在版权和收费问题组件丰富:图表组件非常全面,可满足各种场景下的展示需求。其次:说一下使用的体验。 性能的确非常强悍,亲测百万数据非常流程。重点是这个库是开源的,看官方介绍,说scottplot 5 的版本性能会比4更强劲,期待中。图表种类非常丰富。重点是2D图表库。代码下载下来后,可以使用VS2022进行编译,注意:如果使用VS2019的话,需要支持.net6.【缺点1】:不支持MVVM模式【缺点2】:在图表上标注每个点的数据,没有其他图表库方便。【缺点3】:要绘实时折线图表没有其他图表库方便。 下面介绍使用个人认为比较简单的方法在Scottplot实现实时动态图表的方法。介绍方法前,可以先看下效果,如下图: 下图的数据来自于传感器,通过USB转RS232来读取传感器的值来进行实时显示。 再次:我们先看下官方给的几个方案: Plot Live, Changing Data - ScottPlot FAQ 方案一:Changing Fixed-Length Data 简介:通过一个定时器不断的更新一个固定大小的double数组,来完成实时刷新Y轴的值。 readonly double[] Values = new double[25]; readonly Stopwatch Stopwatch = Stopwatch.StartNew(); public Form1() { InitializeComponent(); UpdateValues(); formsPlot1.Plot.AddSignal(Values); } public void UpdateValues() { double phase = Stopwatch.Elapsed.TotalSeconds; double multiplier = 2 * Math.PI / Values.Length; for (int i = 0; i < Values.Length; i++) Values[i] = Math.Sin(i * multiplier + phase); } private void timer1_Tick(object sender, EventArgs e) { UpdateValues(); formsPlot1.Render(); }这个方案的缺点: 使用固定大小的数组的话,初始化的时候,在没有数据的时候,会显示一条和数组大小相等长度的直线,然后数据上来后,会从数组的最后开始更新数据。所以前面会看到一条直线。 如何解决这个问题: 1. 需要配合IPlottable具体实现类中的MaxRenderIndex来去掉那条线条,例如刚开始初始话的时候,设置:MaxRenderIndex = 0; 2. 然后在实时值上来时候,更新MaxRenderIndex,并更新Y轴中的值。 3. 如果MaxRenderIndex 的值大于数组的大小了,就让它等于数组的大小。注意这个大小不能超过数组的大小,否则会报数组越界异常。 部分代码: private static SignalPlot RealTimeSignalPlot; RealTimeSignalPlot = RealTimeContentPlot.Plot.AddSignal(LiveData); RealTimeSignalPlot.MaxRenderIndex = 0; public void UpdateDataDopplerRadar(object currentValue) { //要对这个方法进行扩展 // 1. 这里面判断数组实时值的个数是否大于数组大小,如果小于数组大小,就从数组当前大小开始更新。 // 对 RealTimeSignalPlot.MaxRenderIndex 赋值未实时值得个数。 if (ApplicationContext.LiveDataCount < ApplicationContext.LiveDataLength) { ApplicationContext.LiveData[ApplicationContext.LiveDataCount] = (double)currentValue; RealTimeSignalPlot.MaxRenderIndex = ApplicationContext.LiveDataCount; ApplicationContext.LiveDataCount++; } else { // 2. 如果实时值的个数等于了数组大小,就执行下面这部分代码,然后对RealTimeSignalPlot.MaxRenderIndex 进行重新赋值。 //"scroll" the whole chart to the left Array.Copy(ApplicationContext.LiveData, 1, ApplicationContext.LiveData, 0, ApplicationContext.LiveData.Length - 1); //place the newest data point at the end ApplicationContext.LiveData[ApplicationContext.LiveData.Length - 1] = (double)currentValue; } if (RealTimeSignalPlot.MaxRenderIndex >= ApplicationContext.LiveDataLength) { RealTimeSignalPlot.MaxRenderIndex = ApplicationContext.LiveDataLength - 1; } RealTimeContentPlot.Refresh(); }方案二:Growing Data with Partial Array Rendering 代码自己看,就不解释了。和方案一基本差不多。 readonly double[] Values = new double[100_000]; readonly ScottPlot.Plottable.SignalPlot SignalPlot; int NextPointIndex = 0; public Form1() { InitializeComponent(); SignalPlot = formsPlot1.Plot.AddSignal(Values); formsPlot1.Plot.SetAxisLimits(0, 100, -2, 2); } // This timer adds data frequently (1000 times / second) private void timer1_Tick(object sender, EventArgs e) { Values[NextPointIndex] = Math.Sin(NextPointIndex * .05); SignalPlot.MaxRenderIndex = NextPointIndex; NextPointIndex += 1; } // This timer renders infrequently (10 times per second) private void timer2_Tick(object sender, EventArgs e) { // adjust the axis limits only when needed double currentRightEdge = formsPlot1.Plot.GetAxisLimits().XMax; if (NextPointIndex > currentRightEdge) formsPlot1.Plot.SetAxisLimits(xMax: currentRightEdge + 100); formsPlot1.Render(); }接下来我们重点来看基于官方代码进行扩展的方案三: 官方给的实时绘图都是基于double数组的。但是留意到官方最后说了一下 ScatterPlotList。 于是自己就去看了下 ScatterPlotList 这个类。 看下这个类的简介: 注意里面提到的 has Add() methods to easily add data. 然后看到类名有个List,那就说明它可以用类似List中Add的方法来给图上的曲线增加一个数据。 /// /// A collection of X/Y coordinates that can be displayed as markers and/or connected lines. /// Unlike the regular ScatterPlot, this plot type has Add() methods to easily add data. /// public class ScatterPlotList : IPlottable看到这里,突然发现,如有有List的方法,那实现动态折线图不是很简单了吗? 接着继续看代码,发现这个类里面就有一个public void Add(T x, T y) 和 public void Clear()方法。 没有类似RemoveAt()和Remove的方法。什么意思? 问题一:不能动态Remove掉Xs里面值。 /// /// Clear the list of points /// public void Clear() { Xs.Clear(); Ys.Clear(); } /// /// Add a single point to the list /// public void Add(T x, T y) { Xs.Add(x); Ys.Add(y); }心不甘,继续看代码: 这个类里面的Xs,Ys都是一个List,而且是 protected的, 问题二:ScatterPlotList 这个类没有提供访问的方法。怎么办? 既然不让我在父类直接访问,那我就直接继承这个类ScatterPlotList来访问Xs和Ys. 不就可以解决了。 protected readonly List Xs = new(); protected readonly List Ys = new();问题三:自己写的继承类,如何生成图表呢? 还得继续看代码: 看下 Plot.AddScatterList() 这个方法怎么实现的。 源代码里面直接new一个ScatterPlotList对象,然后Add里面,就返回了这个对象。 /// /// Scatter plot with Add() and Clear() methods for updating data /// public ScatterPlotList AddScatterList( Color? color = null, float lineWidth = 1, float markerSize = 5, string label = null, MarkerShape markerShape = MarkerShape.filledCircle, LineStyle lineStyle = LineStyle.Solid) { var spl = new ScatterPlotList() { Color = color ?? GetNextColor(), LineWidth = lineWidth, MarkerSize = markerSize, Label = label, MarkerShape = markerShape, LineStyle = lineStyle }; Add(spl); return spl; }问题四:这个Add方法做了什么呢? 源码如下:很简单,而且是个public的。 /// /// Add a plottable to the plot /// /// a plottable the user created public void Add(IPlottable plottable) { settings.Plottables.Add(plottable); }看到这的话,那我想,我自己写个类,继承这个类:public class ScatterPlotList : IPlottable 然后在使用的时候,我new一个自己的这个类,再通过Plot.Add 加进去,不就可以了。 于是有了下面这些代码,来实现文章开头的实时动态折线图的效果: public class ScatterPlotListDouble : ScatterPlotList { public List GetXs() { return Xs; } public List GetYs() { return Ys; } }初始化的时候和串口有数据的时候,调用下面这部分代码: ApplicationContext定义的几个变量 public static double[] LiveData = new double[] { }; public static double[] xs = new double[] { }; public static int LiveDataLength = 1000; public partial class DataView { private static ScatterPlotListDouble RealTimeSignalPlot; public DataView() { RealTimeSignalPlot = RealTimeSignalPlot ?? new ScatterPlotListDouble() { Color = Color.FromArgb(68, 114, 196), MarkerSize = 3, Smooth = false }; if (RealTimeSignalPlot.Count != 0) { ApplicationContext.xs = RealTimeSignalPlot.GetXs().ToArray(); ApplicationContext.LiveData = RealTimeSignalPlot.GetYs().ToArray(); RealTimeSignalPlot.Clear(); } else { RealTimeSignalPlot.Add(DateTime.Now.ToOADate(), 0); } RealTimeContentPlot.Plot.Add(RealTimeSignalPlot); RealTimeSignalPlot.AddRange(ApplicationContext.xs, ApplicationContext.LiveData); RealTimeContentPlot.Plot.XAxis.DateTimeFormat(true); RealTimeContentPlot.Plot.AxisAuto(); RealTimeContentPlot.Refresh(); } // 这个方法是外部接口,每次串口有数据了,就调用这个来更新数据。 public void UpdateData(object currentValue, DateTime now) { RealTimeSignalPlot.Add(now.ToOADate(), (double)currentValue); if (RealTimeSignalPlot.GetXs().Count > ApplicationContext.LiveDataLength) { RealTimeSignalPlot.GetXs().RemoveAt(0); RealTimeSignalPlot.GetYs().RemoveAt(0); } Dispatcher.Invoke(() => { RealTimeContentPlot.Plot.AxisAuto(); RealTimeContentPlot.Refresh(); }); } }完结,通过以上方法,来使用List实现一个实时的动态效果图,比较方便。 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |