草庐IT

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

Foxalien 2023-09-04 原文

首先:简单 介绍下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的方法来给图上的曲线增加一个数据。

    /// <summary>
    /// 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.
    /// </summary>
    public class ScatterPlotList<T> : IPlottable

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

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

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

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

        /// <summary>
        /// Clear the list of points
        /// </summary>
        public void Clear()
        {
            Xs.Clear();
            Ys.Clear();
        }

        /// <summary>
        /// Add a single point to the list
        /// </summary>
        public void Add(T x, T y)
        {
            Xs.Add(x);
            Ys.Add(y);
        }

心不甘,继续看代码:

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

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

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

不就可以解决了。


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

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

还得继续看代码:

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

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

        /// <summary>
        /// Scatter plot with Add() and Clear() methods for updating data
        /// </summary>
        public ScatterPlotList<double> 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<double>()
            {
                Color = color ?? GetNextColor(),
                LineWidth = lineWidth,
                MarkerSize = markerSize,
                Label = label,
                MarkerShape = markerShape,
                LineStyle = lineStyle
            };

            Add(spl);
            return spl;
        }

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

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

        /// <summary>
        /// Add a plottable to the plot
        /// </summary>
        /// <param name="plottable">a plottable the user created</param>
        public void Add(IPlottable plottable)
        {
            settings.Plottables.Add(plottable);
        }

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

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

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

 public class ScatterPlotListDouble<T> : ScatterPlotList<T>
    {
        public List<T> GetXs()
        {
            return Xs;
        }

        public List<T> 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<double> RealTimeSignalPlot;

    public DataView()
    {
            RealTimeSignalPlot = RealTimeSignalPlot ?? new ScatterPlotListDouble<double>()
            {
                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实现一个实时的动态效果图,比较方便。

有关【ScottPlot】使用ScottPlot创建实时动态图的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  4. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  5. ruby - 在 Ruby 中使用匿名模块 - 2

    假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

  6. ruby - 如何在 Ruby 中顺序创建 PI - 2

    出于纯粹的兴趣,我很好奇如何按顺序创建PI,而不是在过程结果之后生成数字,而是让数字在过程本身生成时显示。如果是这种情况,那么数字可以自行产生,我可以对以前看到的数字实现垃圾收集,从而创建一个无限系列。结果只是在Pi系列之后每秒生成一个数字。这是我通过互联网筛选的结果:这是流行的计算机友好算法,类机器算法:defarccot(x,unity)xpow=unity/xn=1sign=1sum=0loopdoterm=xpow/nbreakifterm==0sum+=sign*(xpow/n)xpow/=x*xn+=2sign=-signendsumenddefcalc_pi(digits

  7. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

    我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

  8. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  9. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  10. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

随机推荐