.NET 中使用 ScottPlot 实现动态绘图

1,694 阅读4分钟

C#代码进行可视化

这是本文重点介绍的内容,本文的C#代码通过Scottplot进行可视化。

Scottplot简介

ScottPlot 是一个免费的开源绘图库,用于 .NET,可以轻松以交互方式显示大型数据集。

控制台程序可视化

首先我先介绍一下在控制台程序中进行可视化。

首先添加Scottplot包:

image-20240113201207374

将上篇文章中的C#代码修改如下:

using NumSharp;

namespace LinearRegressionDemo
{
    internal class Program
    {    
        static void Main(string[] args)
        {   
            //创建double类型的列表
            List<double> Array = new List<double>();
            List<double> ArgsList = new List<double>();

            // 指定CSV文件的路径
            string filePath = "你的data.csv路径";

            // 调用ReadCsv方法读取CSV文件数据
            Array = ReadCsv(filePath);

            var array = np.array(Array).reshape(100,2);

            double learning_rate = 0.0001;
            double initial_b = 0;
            double initial_m = 0;
            double num_iterations = 10;

            Console.WriteLine($"Starting gradient descent at b = {initial_b}, m = {initial_m}, error = {compute_error_for_line_given_points(initial_b, initial_m, array)}");
            Console.WriteLine("Running...");
            ArgsList = gradient_descent_runner(array, initial_b, initial_m, learning_rate, num_iterations);
            double b = ArgsList[ArgsList.Count - 2];
            double m = ArgsList[ArgsList.Count - 1];
            Console.WriteLine($"After {num_iterations} iterations b = {b}, m = {m}, error = {compute_error_for_line_given_points(b, m, array)}");
            Console.ReadLine();

            var x1 = array[$":", 0];
            var y1 = array[$":", 1];
            var y2 = m * x1 + b;

            ScottPlot.Plot myPlot = new(400, 300);
            myPlot.AddScatterPoints(x1.ToArray<double>(), y1.ToArray<double>(), markerSize: 5);
            myPlot.AddScatter(x1.ToArray<double>(), y2.ToArray<double>(), markerSize: 0);
            myPlot.Title($"y = {m:0.00}x + {b:0.00}");

            myPlot.SaveFig("图片.png");
       
        }

        static List<double> ReadCsv(string filePath)
        {
            List<double> array = new List<double>();
            try
            {
                // 使用File.ReadAllLines读取CSV文件的所有行
                string[] lines = File.ReadAllLines(filePath);             

                // 遍历每一行数据
                foreach (string line in lines)
                {
                    // 使用逗号分隔符拆分每一行的数据
                    string[] values = line.Split(',');

                    // 打印每一行的数据
                    foreach (string value in values)
                    {
                        array.Add(Convert.ToDouble(value));
                    }                  
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("发生错误: " + ex.Message);
            }
            return array;
        }

        public static double compute_error_for_line_given_points(double b,double m,NDArray array)
        {
            double totalError = 0;
            for(int i = 0;i < array.shape[0];i++)
            {
                double x = array[i, 0];
                double y = array[i, 1];
                totalError += Math.Pow((y - (m*x+b)),2);
            }
            return totalError / array.shape[0];
        }

        public static double[] step_gradient(double b_current,double m_current,NDArray array,double learningRate)
        {
            double[] args = new double[2];
            double b_gradient = 0;
            double m_gradient = 0;
            double N = array.shape[0];

            for (int i = 0; i < array.shape[0]; i++)
            {
                double x = array[i, 0];
                double y = array[i, 1];
                b_gradient += -(2 / N) * (y - ((m_current * x) + b_current));
                m_gradient += -(2 / N) * x * (y - ((m_current * x) + b_current));
            }

            double new_b = b_current - (learningRate * b_gradient);
            double new_m = m_current - (learningRate * m_gradient);
            args[0] = new_b;
            args[1] = new_m;

            return args;
        }

        public static List<double> gradient_descent_runner(NDArray array, double starting_b, double starting_m, double learningRate,double num_iterations)
        {
            double[] args = new double[2];
            List<double> argsList = new List<double>();
            args[0] = starting_b;
            args[1] = starting_m;

            for(int i = 0 ; i < num_iterations; i++) 
            {
                args = step_gradient(args[0], args[1], array, learningRate);
                argsList.AddRange(args);
            }

            return argsList;
        }


    }
}

然后得到的图片如下所示:

image-20240113202345301

在以上代码中需要注意的地方:

  var x1 = array[$":", 0];
  var y1 = array[$":", 1];

是在使用NumSharp中的切片,x1表示所有行的第一列,y1表示所有行的第二列。

当然我们不满足于只是保存图片,在控制台应用程序中,再添加一个 ScottPlot.WinForms包:

image-20240113202751162

右键控制台项目选择属性,将目标OS改为Windows:

image-20240113212334704

将上述代码中的

  myPlot.SaveFig("图片.png");

修改为:

 var viewer = new ScottPlot.FormsPlotViewer(myPlot);
 viewer.ShowDialog();

再次运行结果如下:

image-20240113203022718

winform进行可视化

我也想像Python代码中那样画动图,因此做了个winform程序进行演示。

首先创建一个winform,添加ScottPlot.WinForms包,然后从工具箱中添加FormsPlot这个控件:

image-20240113205227384

有两种方法实现,第一种方法用了定时器:

using NumSharp;
namespace WinFormDemo
{
    public partial class Form1 : Form
    {
        System.Windows.Forms.Timer updateTimer = new System.Windows.Forms.Timer();
        int num_iterations;
        int count = 0;
        NDArray? x1, y1, b_each, m_each;
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            StartLinearRegression();
        }

        public void StartLinearRegression()
        {
            //创建double类型的列表
            List<double> Array = new List<double>();
            List<double> ArgsList = new List<double>();

            // 指定CSV文件的路径
            string filePath = "你的data.csv路径";

            // 调用ReadCsv方法读取CSV文件数据
            Array = ReadCsv(filePath);

            var array = np.array(Array).reshape(100, 2);

            double learning_rate = 0.0001;
            double initial_b = 0;
            double initial_m = 0;
            num_iterations = 10;

            ArgsList = gradient_descent_runner(array, initial_b, initial_m, learning_rate, num_iterations);

            x1 = array[$":", 0];
            y1 = array[$":", 1];

            var argsArr = np.array(ArgsList).reshape(num_iterations, 2);
            b_each = argsArr[$":", 0];
            m_each = argsArr[$":", 1];

            double b = b_each[-1];
            double m = m_each[-1];
            var y2 = m * x1 + b;

            formsPlot1.Plot.AddScatterPoints(x1.ToArray<double>(), y1.ToArray<double>(), markerSize: 5);
            //formsPlot1.Plot.AddScatter(x1.ToArray<double>(), y2.ToArray<double>(), markerSize: 0);
            formsPlot1.Render();


        }

        static List<double> ReadCsv(string filePath)
        {
            List<double> array = new List<double>();
            try
            {
                // 使用File.ReadAllLines读取CSV文件的所有行
                string[] lines = File.ReadAllLines(filePath);

                // 遍历每一行数据
                foreach (string line in lines)
                {
                    // 使用逗号分隔符拆分每一行的数据
                    string[] values = line.Split(',');

                    // 打印每一行的数据
                    foreach (string value in values)
                    {
                        array.Add(Convert.ToDouble(value));
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("发生错误: " + ex.Message);
            }
            return array;
        }

        public static double compute_error_for_line_given_points(double b, double m, NDArray array)
        {
            double totalError = 0;
            for (int i = 0; i < array.shape[0]; i++)
            {
                double x = array[i, 0];
                double y = array[i, 1];
                totalError += Math.Pow((y - (m * x + b)), 2);
            }
            return totalError / array.shape[0];
        }

        public static double[] step_gradient(double b_current, double m_current, NDArray array, double learningRate)
        {
            double[] args = new double[2];
            double b_gradient = 0;
            double m_gradient = 0;
            double N = array.shape[0];

            for (int i = 0; i < array.shape[0]; i++)
            {
                double x = array[i, 0];
                double y = array[i, 1];
                b_gradient += -(2 / N) * (y - ((m_current * x) + b_current));
                m_gradient += -(2 / N) * x * (y - ((m_current * x) + b_current));
            }

            double new_b = b_current - (learningRate * b_gradient);
            double new_m = m_current - (learningRate * m_gradient);
            args[0] = new_b;
            args[1] = new_m;

            return args;
        }

        public static List<double> gradient_descent_runner(NDArray array, double starting_b, double starting_m, double learningRate, double num_iterations)
        {
            double[] args = new double[2];
            List<double> argsList = new List<double>();
            args[0] = starting_b;
            args[1] = starting_m;

            for (int i = 0; i < num_iterations; i++)
            {
                args = step_gradient(args[0], args[1], array, learningRate);
                argsList.AddRange(args);
            }

            return argsList;
        }

        private void button2_Click(object sender, EventArgs e)
        {
            // 初始化定时器
            updateTimer.Interval = 1000; // 设置定时器触发间隔(毫秒)
            updateTimer.Tick += UpdateTimer_Tick;
            updateTimer.Start();
        }

        private void UpdateTimer_Tick(object? sender, EventArgs e)
        {
            if (count >= num_iterations)
            {
                updateTimer.Stop();
            }
            else
            {
                UpdatePlot(count);
            }

            count++;
        }

        public void UpdatePlot(int count)
        {

            double b = b_each?[count];
            double m = m_each?[count];

            var y2 = m * x1 + b;

            formsPlot1.Plot.Clear();
            formsPlot1.Plot.AddScatterPoints(x1?.ToArray<double>(), y1?.ToArray<double>(), markerSize: 5);
            formsPlot1.Plot.AddScatter(x1?.ToArray<double>(), y2.ToArray<double>(), markerSize: 0);
            formsPlot1.Plot.Title($"第{count + 1}次迭代:y = {m:0.00}x + {b:0.00}");
            formsPlot1.Render();
        }

        private void button3_Click(object sender, EventArgs e)
        {
            updateTimer.Stop();
        }

        private void Form1_Load(object sender, EventArgs e)
        {

        }
    }
}

简单介绍一下思路,首先创建List<double> argsList用来保存每次迭代生成的参数b、m,然后用

           var argsArr = np.array(ArgsList).reshape(num_iterations, 2);  

argsList通过np.array()方法转化为NDArray,然后再调用reshape方法,转化成行数等于迭代次数,列数为2,即每一行对应一组参数值b、m。

            b_each = argsArr[$":", 0];
            m_each = argsArr[$":", 1];

argsArr[$":", 0]表示每一行中第一列的值,也就是每一个b,argsArr[$":", 1]表示每一行中第二列的值。

            double b = b_each[-1];
            double m = m_each[-1];

b_each[-1]用了NumSharp的功能表示b_each最后一个元素。

实现效果如下所示:

winform绘图效果1

image-20240113205549690

另一种方法可以通过异步实现:

using NumSharp;

namespace WinFormDemo
{
    public partial class Form2 : Form
    {      
        int num_iterations;
        NDArray? x1, y1, b_each, m_each;
        public Form2()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            StartLinearRegression();
        }

        public void StartLinearRegression()
        {
            //创建double类型的列表
            List<double> Array = new List<double>();
            List<double> ArgsList = new List<double>();

            // 指定CSV文件的路径
            string filePath = "你的data.csv路径";

            // 调用ReadCsv方法读取CSV文件数据
            Array = ReadCsv(filePath);

            var array = np.array(Array).reshape(100, 2);

            double learning_rate = 0.0001;
            double initial_b = 0;
            double initial_m = 0;
            num_iterations = 10;

            ArgsList = gradient_descent_runner(array, initial_b, initial_m, learning_rate, num_iterations);

            x1 = array[$":", 0];
            y1 = array[$":", 1];

            var argsArr = np.array(ArgsList).reshape(num_iterations, 2);
            b_each = argsArr[$":", 0];
            m_each = argsArr[$":", 1];

            double b = b_each[-1];
            double m = m_each[-1];
            var y2 = m * x1 + b;

            formsPlot1.Plot.AddScatterPoints(x1.ToArray<double>(), y1.ToArray<double>(), markerSize: 5);      
            formsPlot1.Render();
        }

        static List<double> ReadCsv(string filePath)
        {
            List<double> array = new List<double>();
            try
            {
                // 使用File.ReadAllLines读取CSV文件的所有行
                string[] lines = File.ReadAllLines(filePath);

                // 遍历每一行数据
                foreach (string line in lines)
                {
                    // 使用逗号分隔符拆分每一行的数据
                    string[] values = line.Split(',');

                    // 打印每一行的数据
                    foreach (string value in values)
                    {
                        array.Add(Convert.ToDouble(value));
                    }
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("发生错误: " + ex.Message);
            }
            return array;
        }

        public static double compute_error_for_line_given_points(double b, double m, NDArray array)
        {
            double totalError = 0;
            for (int i = 0; i < array.shape[0]; i++)
            {
                double x = array[i, 0];
                double y = array[i, 1];
                totalError += Math.Pow((y - (m * x + b)), 2);
            }
            return totalError / array.shape[0];
        }

        public static double[] step_gradient(double b_current, double m_current, NDArray array, double learningRate)
        {
            double[] args = new double[2];
            double b_gradient = 0;
            double m_gradient = 0;
            double N = array.shape[0];

            for (int i = 0; i < array.shape[0]; i++)
            {
                double x = array[i, 0];
                double y = array[i, 1];
                b_gradient += -(2 / N) * (y - ((m_current * x) + b_current));
                m_gradient += -(2 / N) * x * (y - ((m_current * x) + b_current));
            }

            double new_b = b_current - (learningRate * b_gradient);
            double new_m = m_current - (learningRate * m_gradient);
            args[0] = new_b;
            args[1] = new_m;

            return args;
        }

        public static List<double> gradient_descent_runner(NDArray array, double starting_b, double starting_m, double learningRate, double num_iterations)
        {
            double[] args = new double[2];
            List<double> argsList = new List<double>();
            args[0] = starting_b;
            args[1] = starting_m;

            for (int i = 0; i < num_iterations; i++)
            {
                args = step_gradient(args[0], args[1], array, learningRate);
                argsList.AddRange(args);
            }

            return argsList;
        }

        private void Form2_Load(object sender, EventArgs e)
        {

        }

        public async Task UpdateGraph()
        {
            for (int i = 0; i < num_iterations; i++)
            {
                double b = b_each?[i];
                double m = m_each?[i];
                var y2 = m * x1 + b;

                formsPlot1.Plot.Clear();
                formsPlot1.Plot.AddScatterPoints(x1?.ToArray<double>(), y1?.ToArray<double>(), markerSize: 5);
                formsPlot1.Plot.AddScatter(x1?.ToArray<double>(), y2.ToArray<double>(), markerSize: 0);
                formsPlot1.Plot.Title($"第{i + 1}次迭代:y = {m:0.00}x + {b:0.00}");
                formsPlot1.Render();
           
                await Task.Delay(1000);
            }


        }

        private async void button2_Click(object sender, EventArgs e)
        {
            await UpdateGraph();
        }
    }
}

点击更新按钮开始执行异步任务:

 private async void button2_Click(object sender, EventArgs e)
        {
            await UpdateGraph();
        }
 public async Task UpdateGraph()
        {
            for (int i = 0; i < num_iterations; i++)
            {
                double b = b_each?[i];
                double m = m_each?[i];
                var y2 = m * x1 + b;

                formsPlot1.Plot.Clear();
                formsPlot1.Plot.AddScatterPoints(x1?.ToArray<double>(), y1?.ToArray<double>(), markerSize: 5);
                formsPlot1.Plot.AddScatter(x1?.ToArray<double>(), y2.ToArray<double>(), markerSize: 0);
                formsPlot1.Plot.Title($"第{i + 1}次迭代:y = {m:0.00}x + {b:0.00}");
                formsPlot1.Render();
           
                await Task.Delay(1000);
            }

实现效果如下:

总结

本文以一个控制台应用与一个winform程序为例向大家介绍了C#如何基于ScottPlot进行数据可视化,并介绍了实现动态绘图的两种方式,一种是使用定时器,另一种是使用异步操作,希望对你有所帮助。

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!

作者:mingupupup

出处:cnblogs.com/mingupupu/p/17963079

声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!