当前位置:   article > 正文

【ScottPlot】使用ScottPlot创建实时动态图_scatterplotlist

scatterplotlist

首先:简单 介绍下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轴的值。

  1. readonly double[] Values = new double[25];
  2. readonly Stopwatch Stopwatch = Stopwatch.StartNew();
  3. public Form1()
  4. {
  5. InitializeComponent();
  6. UpdateValues();
  7. formsPlot1.Plot.AddSignal(Values);
  8. }
  9. public void UpdateValues()
  10. {
  11. double phase = Stopwatch.Elapsed.TotalSeconds;
  12. double multiplier = 2 * Math.PI / Values.Length;
  13. for (int i = 0; i < Values.Length; i++)
  14. Values[i] = Math.Sin(i * multiplier + phase);
  15. }
  16. private void timer1_Tick(object sender, EventArgs e)
  17. {
  18. UpdateValues();
  19. formsPlot1.Render();
  20. }

这个方案的缺点:

使用固定大小的数组的话,初始化的时候,在没有数据的时候,会显示一条和数组大小相等长度的直线,然后数据上来后,会从数组的最后开始更新数据。所以前面会看到一条直线。

如何解决这个问题:

1. 需要配合IPlottable具体实现类中的MaxRenderIndex来去掉那条线条,例如刚开始初始话的时候,设置:MaxRenderIndex = 0; 

2. 然后在实时值上来时候,更新MaxRenderIndex,并更新Y轴中的值。

3. 如果MaxRenderIndex 的值大于数组的大小了,就让它等于数组的大小。注意这个大小不能超过数组的大小,否则会报数组越界异常。

  1. 部分代码:
  2. private static SignalPlot RealTimeSignalPlot;
  3. RealTimeSignalPlot = RealTimeContentPlot.Plot.AddSignal(LiveData);
  4. RealTimeSignalPlot.MaxRenderIndex = 0
  5. public void UpdateDataDopplerRadar(object currentValue)
  6. {
  7. //要对这个方法进行扩展
  8. // 1. 这里面判断数组实时值的个数是否大于数组大小,如果小于数组大小,就从数组当前大小开始更新。
  9. // 对 RealTimeSignalPlot.MaxRenderIndex 赋值未实时值得个数。
  10. if (ApplicationContext.LiveDataCount < ApplicationContext.LiveDataLength)
  11. {
  12. ApplicationContext.LiveData[ApplicationContext.LiveDataCount] = (double)currentValue;
  13. RealTimeSignalPlot.MaxRenderIndex = ApplicationContext.LiveDataCount;
  14. ApplicationContext.LiveDataCount++;
  15. }
  16. else
  17. {
  18. // 2. 如果实时值的个数等于了数组大小,就执行下面这部分代码,然后对RealTimeSignalPlot.MaxRenderIndex 进行重新赋值。
  19. //"scroll" the whole chart to the left
  20. Array.Copy(ApplicationContext.LiveData, 1, ApplicationContext.LiveData, 0, ApplicationContext.LiveData.Length - 1);
  21. //place the newest data point at the end
  22. ApplicationContext.LiveData[ApplicationContext.LiveData.Length - 1] = (double)currentValue;
  23. }
  24. if (RealTimeSignalPlot.MaxRenderIndex >= ApplicationContext.LiveDataLength)
  25. {
  26. RealTimeSignalPlot.MaxRenderIndex = ApplicationContext.LiveDataLength - 1;
  27. }
  28. RealTimeContentPlot.Refresh();
  29. }

方案二:Growing Data with Partial Array Rendering

 代码自己看,就不解释了。和方案一基本差不多。

  1. readonly double[] Values = new double[100_000];
  2. readonly ScottPlot.Plottable.SignalPlot SignalPlot;
  3. int NextPointIndex = 0;
  4. public Form1()
  5. {
  6. InitializeComponent();
  7. SignalPlot = formsPlot1.Plot.AddSignal(Values);
  8. formsPlot1.Plot.SetAxisLimits(0, 100, -2, 2);
  9. }
  10. // This timer adds data frequently (1000 times / second)
  11. private void timer1_Tick(object sender, EventArgs e)
  12. {
  13. Values[NextPointIndex] = Math.Sin(NextPointIndex * .05);
  14. SignalPlot.MaxRenderIndex = NextPointIndex;
  15. NextPointIndex += 1;
  16. }
  17. // This timer renders infrequently (10 times per second)
  18. private void timer2_Tick(object sender, EventArgs e)
  19. {
  20. // adjust the axis limits only when needed
  21. double currentRightEdge = formsPlot1.Plot.GetAxisLimits().XMax;
  22. if (NextPointIndex > currentRightEdge)
  23. formsPlot1.Plot.SetAxisLimits(xMax: currentRightEdge + 100);
  24. formsPlot1.Render();
  25. }

接下来我们重点来看基于官方代码进行扩展的方案三:

官方给的实时绘图都是基于double数组的。但是留意到官方最后说了一下 ScatterPlotList。

于是自己就去看了下 ScatterPlotList 这个类。

看下这个类的简介:

注意里面提到的 has Add() methods to easily add data.

然后看到类名有个List,那就说明它可以用类似List中Add的方法来给图上的曲线增加一个数据。

  1. /// <summary>
  2. /// A collection of X/Y coordinates that can be displayed as markers and/or connected lines.
  3. /// Unlike the regular ScatterPlot, this plot type has Add() methods to easily add data.
  4. /// </summary>
  5. public class ScatterPlotList<T> : IPlottable

看到这里,突然发现,如有有List的方法,那实现动态折线图不是很简单了吗?

接着继续看代码,发现这个类里面就有一个public void Add(T x, T y) 和 public void Clear()方法。

没有类似RemoveAt()和Remove的方法。什么意思?

问题一:不能动态Remove掉Xs里面值。

  1. /// <summary>
  2. /// Clear the list of points
  3. /// </summary>
  4. public void Clear()
  5. {
  6. Xs.Clear();
  7. Ys.Clear();
  8. }
  9. /// <summary>
  10. /// Add a single point to the list
  11. /// </summary>
  12. public void Add(T x, T y)
  13. {
  14. Xs.Add(x);
  15. Ys.Add(y);
  16. }

心不甘,继续看代码:

这个类里面的Xs,Ys都是一个List,而且是 protected的,

问题二:ScatterPlotList<T> 这个类没有提供访问的方法。怎么办?

既然不让我在父类直接访问,那我就直接继承这个类ScatterPlotList<T>来访问Xs和Ys.

不就可以解决了。

  1. protected readonly List<T> Xs = new();
  2. protected readonly List<T> Ys = new();

问题三:自己写的继承类,如何生成图表呢?

还得继续看代码:

看下 Plot.AddScatterList<double>() 这个方法怎么实现的。

源代码里面直接new一个ScatterPlotList对象,然后Add里面,就返回了这个对象。

  1. /// <summary>
  2. /// Scatter plot with Add() and Clear() methods for updating data
  3. /// </summary>
  4. public ScatterPlotList<double> AddScatterList(
  5. Color? color = null,
  6. float lineWidth = 1,
  7. float markerSize = 5,
  8. string label = null,
  9. MarkerShape markerShape = MarkerShape.filledCircle,
  10. LineStyle lineStyle = LineStyle.Solid)
  11. {
  12. var spl = new ScatterPlotList<double>()
  13. {
  14. Color = color ?? GetNextColor(),
  15. LineWidth = lineWidth,
  16. MarkerSize = markerSize,
  17. Label = label,
  18. MarkerShape = markerShape,
  19. LineStyle = lineStyle
  20. };
  21. Add(spl);
  22. return spl;
  23. }

问题四:这个Add方法做了什么呢?

源码如下:很简单,而且是个public的。

  1. /// <summary>
  2. /// Add a plottable to the plot
  3. /// </summary>
  4. /// <param name="plottable">a plottable the user created</param>
  5. public void Add(IPlottable plottable)
  6. {
  7. settings.Plottables.Add(plottable);
  8. }

看到这的话,那我想,我自己写个类,继承这个类:public class ScatterPlotList<T> : IPlottable

然后在使用的时候,我new一个自己的这个类,再通过Plot.Add 加进去,不就可以了。

于是有了下面这些代码,来实现文章开头的实时动态折线图的效果:

  1. public class ScatterPlotListDouble<T> : ScatterPlotList<T>
  2. {
  3. public List<T> GetXs()
  4. {
  5. return Xs;
  6. }
  7. public List<T> GetYs()
  8. {
  9. return Ys;
  10. }
  11. }

初始化的时候和串口有数据的时候,调用下面这部分代码:

  1. ApplicationContext定义的几个变量
  2. public static double[] LiveData = new double[] { };
  3. public static double[] xs = new double[] { };
  4. public static int LiveDataLength = 1000
  5. public partial class DataView
  6. {
  7. private static ScatterPlotListDouble<double> RealTimeSignalPlot;
  8. public DataView()
  9. {
  10. RealTimeSignalPlot = RealTimeSignalPlot ?? new ScatterPlotListDouble<double>()
  11. {
  12. Color = Color.FromArgb(68, 114, 196),
  13. MarkerSize = 3,
  14. Smooth = false
  15. };
  16. if (RealTimeSignalPlot.Count != 0)
  17. {
  18. ApplicationContext.xs = RealTimeSignalPlot.GetXs().ToArray();
  19. ApplicationContext.LiveData = RealTimeSignalPlot.GetYs().ToArray();
  20. RealTimeSignalPlot.Clear();
  21. }
  22. else
  23. {
  24. RealTimeSignalPlot.Add(DateTime.Now.ToOADate(), 0);
  25. }
  26. RealTimeContentPlot.Plot.Add(RealTimeSignalPlot);
  27. RealTimeSignalPlot.AddRange(ApplicationContext.xs, ApplicationContext.LiveData);
  28. RealTimeContentPlot.Plot.XAxis.DateTimeFormat(true);
  29. RealTimeContentPlot.Plot.AxisAuto();
  30. RealTimeContentPlot.Refresh();
  31. }
  32. // 这个方法是外部接口,每次串口有数据了,就调用这个来更新数据。
  33. public void UpdateData(object currentValue, DateTime now)
  34. {
  35. RealTimeSignalPlot.Add(now.ToOADate(), (double)currentValue);
  36. if (RealTimeSignalPlot.GetXs().Count > ApplicationContext.LiveDataLength)
  37. {
  38. RealTimeSignalPlot.GetXs().RemoveAt(0);
  39. RealTimeSignalPlot.GetYs().RemoveAt(0);
  40. }
  41. Dispatcher.Invoke(() =>
  42. {
  43. RealTimeContentPlot.Plot.AxisAuto();
  44. RealTimeContentPlot.Refresh();
  45. });
  46. }
  47. }

完结,通过以上方法,来使用List实现一个实时的动态效果图,比较方便。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/blog/article/detail/97900
推荐阅读
相关标签
  

闽ICP备14008679号