以下是一个基于 ScottPlot 5.0.55 的详细 C# 示例代码,展示如何使用 ScottPlot.WinForms 绘制电压曲线,结合 ResamplerMgr 类进行重采样,确保负值电压区域的曲线平滑连续。示例包括模拟电压数据(正弦波 + 噪声,包含负值)、重采样处理、绘制原始数据(散点)和重采样数据(平滑曲线),并提供交互功能(如鼠标显示坐标)。代码使用 Windows Forms 平台,包含清晰的中文注释,解释每个部分的功能、参数和逻辑,并针对 ScottPlot 5.0.55 的 API 进行优化。


环境准备

  1. NuGet 包安装:

    • 安装特定版本的 ScottPlot.WinForms 和 MathNet.Numerics:

      bash

      dotnet add package ScottPlot.WinForms --version 5.0.55
      dotnet add package MathNet.Numerics --version 5.0.0
    • 确保项目文件中包含:

      xml

      <PackageReference Include="ScottPlot.WinForms" Version="5.0.55" />
      <PackageReference Include="MathNet.Numerics" Version="5.0.0" />
  2. 项目类型:

    • Windows Forms 应用程序(.NET 8.0 或 .NET Framework 4.8 均可)。

    • 示例使用 FormsPlot 控件,适合 Windows 环境。

  3. 功能概述:

    • 模拟电压数据:正弦波(±5V)加随机噪声,包含负值。

    • 使用 ResamplerMgr 类重采样,生成平滑曲线。

    • 使用 ScottPlot 5.0.55 的 FormsPlot 绘制:

      • 蓝色散点:原始数据。

      • 红色折线:重采样数据。

    • 添加交互功能:鼠标移动时显示坐标。

    • 保存图像为 PNG 文件。


ScottPlot 5.0.55 特性

ScottPlot 5.0.55 是 ScottPlot 5.x 系列的一个稳定版本,改进了性能和 API 设计。以下是本示例中用到的关键特性:

  • FormsPlot 控件:用于 Windows Forms,替代旧版 PlotView,提供高效渲染。

  • Plot API:

    • Add.Scatter:绘制散点图或折线图,支持自定义颜色、线宽、标记大小。

    • Title, XLabel, YLabel:设置图表标题和轴标签。

    • Legend:显示图例。

    • AutoScale:自动调整轴范围,适合负值数据。

    • SavePng:保存图表为 PNG 文件。

  • 交互支持:

    • 通过 MouseMove 事件获取鼠标坐标,显示实时数据点信息。

  • 性能优化:

    • 支持大数据集(本例数据较小,使用 Scatter 即可;若数据量大,可用 Signal)。


完整示例代码

csharp

using MathNet.Numerics;
using MathNet.Numerics.Interpolation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using ScottPlot.WinForms; // ScottPlot 5.0.55 的 Windows Forms 命名空间

namespace ResamplerLib
{
    /// <summary>
    /// 重采样管理类,用于对电压数据进行插值重采样,确保负值区域平滑连续。
    /// 使用 MathNet.Numerics 的样条插值,支持均匀采样和指定点采样。
    /// </summary>
    public static class ResamplerMgr
    {
        /// <summary>
        /// 均匀重采样,生成指定数量的采样点。
        /// 使用 Cubic Spline 插值,确保负值电压曲线平滑。
        /// </summary>
        /// <param name="sampleCount">目标采样点数量,必须大于 0</param>
        /// <param name="x">输入自变量列表(时间),需严格递增</param>
        /// <param name="y">输入因变量列表(电压),与 x 长度相同</param>
        /// <param name="xout">输出重采样的自变量列表</param>
        /// <param name="yout">输出重采样的因变量列表</param>
        public static void Resample(int sampleCount, List<double> x, List<double> y, out List<double> xout, out List<double> yout)
        {
            // 初始化输出列表
            xout = new List<double>();
            yout = new List<double>();

            // 验证输入数据
            if (!ValidateInput(x, y, sampleCount))
                return;

            try
            {
                // 创建样条插值对象,优化负值区域平滑性
                IInterpolation ip = Interpolate.CubicSpline(x.ToArray(), y.ToArray());

                // 获取时间范围
                double start = x[0];
                double stop = x[x.Count - 1];

                // 处理特殊情况:单点或时间范围为 0
                if (sampleCount == 1 || Math.Abs(stop - start) < 1e-10)
                {
                    xout.Add(start);
                    yout.Add(y[0]);
                    return;
                }

                // 计算步长,确保采样点均匀分布
                double step = (stop - start) / (sampleCount - 1);

                // 逐点插值生成重采样数据
                for (double sx = start; sx <= stop + 1e-10; sx += step)
                {
                    double sy = ip.Interpolate(sx);
                    // 处理插值异常(NaN 或 Infinity)
                    if (double.IsNaN(sy) || double.IsInfinity(sy))
                    {
                        sy = sx <= x[0] ? y[0] : y[y.Count - 1];
                        Console.WriteLine($"警告:sx={sx:F3} 处插值异常,使用边界值 {sy:F3}");
                    }
                    xout.Add(sx);
                    yout.Add(sy);
                }
            }
            catch (Exception ex)
            {
                // 异常处理:返回原始数据并记录错误
                Console.WriteLine($"重采样失败:{ex.Message}");
                xout = new List<double>(x);
                yout = new List<double>(y);
            }
        }

        /// <summary>
        /// 根据指定的自变量列表进行重采样。
        /// </summary>
        /// <param name="sampleCount">目标采样点数量(占位参数,不实际使用)</param>
        /// <param name="x">输入自变量列表(时间),需严格递增</param>
        /// <param name="y">输入因变量列表(电压),与 x 长度相同</param>
        /// <param name="xx">目标自变量列表,用于插值</param>
        /// <param name="yy">输出重采样的因变量列表</param>
        public static void Resample(int sampleCount, List<double> x, List<double> y, List<double> xx, out List<double> yy)
        {
            // 初始化输出列表
            yy = new List<double>();

            // 验证输入数据
            if (!ValidateInput(x, y, 1) || xx == null || xx.Count == 0)
            {
                Console.WriteLine("无效输入:xx 必须非空且有效");
                return;
            }

            try
            {
                // 创建样条插值对象
                IInterpolation ip = Interpolate.CubicSpline(x.ToArray(), y.ToArray());

                // 对每个目标点进行插值
                foreach (double sx in xx)
                {
                    double sy = ip.Interpolate(sx);
                    // 处理插值异常
                    if (double.IsNaN(sy) || double.IsInfinity(sy))
                    {
                        sy = sx <= x[0] ? y[0] : y[y.Count - 1];
                        Console.WriteLine($"警告:sx={sx:F3} 处插值异常,使用边界值 {sy:F3}");
                    }
                    yy.Add(sy);
                }
            }
            catch (Exception ex)
            {
                // 异常处理:记录错误
                Console.WriteLine($"重采样失败:{ex.Message}");
            }
        }

        /// <summary>
        /// 两次重采样以提高曲线平滑性。
        /// </summary>
        /// <param name="sampleCount">目标采样点数量</param>
        /// <param name="x">输入自变量列表(时间)</param>
        /// <param name="y">输入因变量列表(电压)</param>
        /// <param name="xout">输出重采样的自变量列表</param>
        /// <param name="yout">输出重采样的因变量列表</param>
        public static void Resample2(int sampleCount, List<double> x, List<double> y, out List<double> xout, out List<double> yout)
        {
            // 初始化输出列表
            xout = new List<double>();
            yout = new List<double>();

            // 第一次重采样:使用两倍采样点
            Resample(2 * sampleCount, x, y, out List<double> xTemp, out List<double> yTemp);

            // 合并原始数据和第一次重采样结果
            List<double> x2 = new List<double>(x);
            List<double> y2 = new List<double>(y);
            x2.AddRange(xTemp);
            y2.AddRange(yTemp);

            // 第二次重采样
            Resample(sampleCount, x2, y2, out xout, out yout);
        }

        /// <summary>
        /// 验证输入数据的有效性。
        /// </summary>
        /// <param name="x">自变量列表(时间)</param>
        /// <param name="y">因变量列表(电压)</param>
        /// <param name="sampleCount">采样点数量</param>
        /// <returns>是否有效</returns>
        private static bool ValidateInput(List<double> x, List<double> y, int sampleCount)
        {
            // 检查输入是否为空或长度不一致
            if (x == null || y == null || x.Count != y.Count || x.Count < 2 || sampleCount <= 0)
            {
                Console.WriteLine("无效输入:x 和 y 必须非空、长度相等、至少 2 个点,且 sampleCount > 0");
                return false;
            }

            // 检查是否包含 NaN 或 Infinity
            if (x.Any(double.IsNaN) || y.Any(double.IsNaN) || x.Any(double.IsInfinity) || y.Any(double.IsInfinity))
            {
                Console.WriteLine("无效输入:x 和 y 不能包含 NaN 或 Infinity");
                return false;
            }

            // 检查 x 是否严格递增
            for (int i = 1; i < x.Count; i++)
            {
                if (x[i] <= x[i - 1])
                {
                    Console.WriteLine("无效输入:x 必须严格递增");
                    return false;
                }
            }

            return true;
        }
    }

    /// <summary>
    /// 主程序:模拟电压数据,执行重采样,使用 ScottPlot 5.0.55 的 FormsPlot 绘制曲线。
    /// 支持鼠标交互,显示坐标,保存图像为 PNG。
    /// </summary>
    class Program
    {
        [STAThread]
        static void Main()
        {
            // 启用 Windows Forms 视觉样式
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            // 创建主窗体
            var form = new Form
            {
                Text = "电压曲线展示 - ScottPlot 5.0.55",
                Width = 800,
                Height = 600
            };

            // 创建 ScottPlot FormsPlot 控件
            var formsPlot = new FormsPlot
            {
                Dock = DockStyle.Fill // 填充整个窗体
            };
            form.Controls.Add(formsPlot);

            // 模拟电压数据:正弦波 + 随机噪声,包含负值
            List<double> x = new List<double>(); // 时间 (秒)
            List<double> y = new List<double>(); // 电压 (伏特)
            Random rand = new Random(42); // 固定种子,确保结果可重复
            for (double t = -5.0; t <= 5.0; t += 0.5) // 时间范围 [-5, 5]
            {
                x.Add(t);
                // 电压 = 5 * sin(t) + 噪声 (±0.25V)
                double voltage = 5.0 * Math.Sin(t) + (rand.NextDouble() - 0.5) * 0.5;
                y.Add(voltage);
            }

            // 进行重采样
            int sampleCount = 100; // 生成 100 个采样点
            ResamplerMgr.Resample(sampleCount, x, y, out List<double> xout, out List<double> yout);

            // 配置 ScottPlot 图表
            var plt = formsPlot.Plot;
            // 添加原始数据(蓝色散点)
            plt.Add.Scatter(x.ToArray(), y.ToArray(),
                color: System.Drawing.Color.Blue,
                markerSize: 5,
                label: "原始数据");
            // 添加重采样数据(红色平滑曲线)
            plt.Add.Scatter(xout.ToArray(), yout.ToArray(),
                color: System.Drawing.Color.Red,
                markerSize: 0, // 无散点,仅绘制折线
                lineWidth: 2,
                label: "重采样数据");
            // 设置图表标题和轴标签
            plt.Title("电压曲线(原始 vs 重采样)");
            plt.XLabel("时间 (秒)");
            plt.YLabel("电压 (伏特)");
            // 显示图例(右上角)
            plt.Legend();
            // 自动调整轴范围,确保负值电压可见
            plt.AutoScale();

            // 添加鼠标交互:显示坐标
            formsPlot.MouseMove += (s, e) =>
            {
                // 获取鼠标在图表中的坐标
                var coords = formsPlot.Plot.GetCoordinates(e.Location);
                // 更新窗体标题,显示时间和电压
                form.Text = $"电压曲线展示 - 时间: {coords.X:F3} 秒, 电压: {coords.Y:F3} 伏特";
            };

            // 保存图像到文件
            try
            {
                plt.SavePng("voltage_curve.png", 800, 600);
                Console.WriteLine("图像已保存为 voltage_curve.png");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"保存图像失败:{ex.Message}");
            }

            // 刷新图表
            formsPlot.Refresh();

            // 运行窗体
            Application.Run(form);
        }
    }
}

代码中文解释

1. ResamplerMgr 类

  • 功能:实现电压数据的重采样,确保负值电压区域的曲线平滑连续。

  • 关键特性:

    • 使用 MathNet.Numerics 的 CubicSpline 插值,替代线性插值,优化负值区域的平滑性。

    • 验证输入数据:确保 x 严格递增,无 NaN 或无穷大。

    • 处理插值异常:当结果为 NaN 或无穷大时,使用边界值(y[0] 或 y[y.Count - 1])。

    • 支持三种重采样方式:

      • 均匀采样(Resample)。

      • 指定点采样(Resample 重载)。

      • 两次重采样(Resample2)。

  • 方法详解:

    • Resample(int sampleCount, List<double> x, List<double> y, out List<double> xout, out List<double> yout):

      • 输入:采样点数(sampleCount)、时间(x)、电压(y)。

      • 输出:重采样的时间(xout)和电压(yout)。

      • 逻辑:

        • 验证输入。

        • 创建样条插值对象。

        • 计算步长:(stop - start) / (sampleCount - 1)。

        • 逐点插值,处理负值异常。

    • Resample(int sampleCount, List<double> x, List<double> y, List<double> xx, out List<double> yy):

      • 根据指定时间点(xx)插值,生成电压(yy)。

    • Resample2:

      • 先用两倍采样点重采样,再用目标采样点重采样,增强平滑性。

    • ValidateInput:

      • 检查输入:非空、长度一致、递增、无异常值。

  • 代码片段:

    csharp

    IInterpolation ip = Interpolate.CubicSpline(x.ToArray(), y.ToArray());
    double step = (stop - start) / (sampleCount - 1);
    for (double sx = start; sx <= stop + 1e-10; sx += step)
    {
        double sy = ip.Interpolate(sx);
        if (double.IsNaN(sy) || double.IsInfinity(sy))
        {
            sy = sx <= x[0] ? y[0] : y[y.Count - 1];
        }
        xout.Add(sx);
        yout.Add(sy);
    }

2. 模拟电压数据

  • 生成逻辑:

    • 时间(x):从 -5 到 5 秒,步长 0.5,共 21 个点。

    • 电压(y):正弦波(5.0 * Math.Sin(t))加随机噪声(±0.25V),模拟真实交流信号,包含负值。

    • 使用固定种子(Random(42))确保结果可重复。

  • 代码:

    csharp

    for (double t = -5.0; t <= 5.0; t += 0.5)
    {
        x.Add(t);
        double voltage = 5.0 * Math.Sin(t) + (rand.NextDouble() - 0.5) * 0.5;
        y.Add(voltage);
    }

3. 重采样

  • 参数:sampleCount = 100,生成 100 个均匀采样点。

  • 调用:ResamplerMgr.Resample(sampleCount, x, y, out xout, out yout)。

  • 效果:

    • 原始数据:21 个点,可能在负值区域不连续。

    • 重采样数据:100 个点,平滑连续,负值区域无断点。

4. ScottPlot 5.0.55 FormsPlot 控件

  • 创建控件:

    • 使用 FormsPlot,设置 Dock = DockStyle.Fill 填充窗体。

  • 绘图配置:

    • Add.Scatter:

      • 原始数据:蓝色散点(markerSize=5)。

      • 重采样数据:红色折线(markerSize=0,lineWidth=2)。

    • 图表设置:

      • Title:电压曲线(原始 vs 重采样)。

      • XLabel:时间 (秒)。

      • YLabel:电压 (伏特)。

      • Legend:显示图例。

      • AutoScale:自动调整轴范围,确保负值可见。

  • 交互功能:

    • 鼠标移动时,通过 GetCoordinates 获取图表坐标,更新窗体标题显示时间和电压。

  • 保存图像:

    • 使用 SavePng 保存为 800x600 像素的 PNG 文件。

  • 代码:

    csharp

    var plt = formsPlot.Plot;
    plt.Add.Scatter(x.ToArray(), y.ToArray(), color: System.Drawing.Color.Blue, markerSize: 5, label: "原始数据");
    plt.Add.Scatter(xout.ToArray(), yout.ToArray(), color: System.Drawing.Color.Red, markerSize: 0, lineWidth: 2, label: "重采样数据");
    plt.Title("电压曲线(原始 vs 重采样)");
    plt.XLabel("时间 (秒)");
    plt.YLabel("电压 (伏特)");
    plt.Legend();
    plt.AutoScale();
    formsPlot.MouseMove += (s, e) =>
    {
        var coords = formsPlot.Plot.GetCoordinates(e.Location);
        form.Text = $"电压曲线展示 - 时间: {coords.X:F3} 秒, 电压: {coords.Y:F3} 伏特";
    };
    plt.SavePng("voltage_curve.png", 800, 600);
    formsPlot.Refresh();

5. 窗体显示

  • 创建 800x600 像素的窗体,标题为“电压曲线展示 - ScottPlot 5.0.55”。

  • 添加 FormsPlot 控件,运行应用程序。


运行结果

  • 输出:

    • 显示一个窗体,包含:

      • 蓝色散点:原始数据(21 点,包含噪声,可能在负值区域不连续)。

      • 红色曲线:重采样数据(100 点,平滑连续,负值区域无断点)。

    • X 轴:时间从 -5 到 5 秒。

    • Y 轴:电压从约 -5.5 到 5.5 伏特。

    • 图例:区分“原始数据”和“重采样数据”。

    • 鼠标移动:窗体标题动态显示鼠标位置的坐标(时间和电压)。

    • 保存图像:生成 voltage_curve.png 文件。

  • 负值电压处理:

    • 样条插值(CubicSpline)确保负值区域平滑。

    • 边界处理:插值异常时使用边界值。

    • 输入验证:排除 NaN、无穷大、非递增等问题。


使用说明

  1. 运行步骤:

    • 创建 Windows Forms 项目。

    • 安装 NuGet 包:

      bash

      dotnet add package ScottPlot.WinForms --version 5.0.55
      dotnet add package MathNet.Numerics --version 5.0.0
    • 复制代码到 Program.cs,运行程序。

  2. 调整参数:

    • 采样点数量:修改 sampleCount(例如 200)以提高曲线分辨率。

    • 数据模拟:更改正弦波或噪声:

      csharp

      double voltage = 10.0 * Math.Sin(t * 2) + (rand.NextDouble() - 0.5) * 1.0; // 双倍频率,更大噪声
  3. 调试负值问题:

    • 检查控制台输出,确认是否触发 NaN 警告。

    • 添加详细日志:

      csharp

      xout.Add(sx);
      yout.Add(sy);
      Console.WriteLine($"点:sx={sx:F3}, sy={sy:F3}");
  4. 扩展功能:

    • 保存其他格式:

      csharp

      plt.SaveSvg("voltage_curve.svg", 800, 600);
    • 大数据优化:若数据量大(>10,000 点),使用 Signal:

      csharp

      plt.Add.Signal(yout.ToArray(), secondsPerPoint: (xout[1] - xout[0]));
    • WPF 支持:安装 ScottPlot.WPF(5.0.55),替换 FormsPlot 为 WpfPlot:

      csharp

      using ScottPlot.WPF;
      var wpfPlot = new WpfPlot { Dock = DockStyle.Fill };

ScottPlot 5.0.55 常见问题

  1. “FormsPlot 未找到”:

    • 确保安装 ScottPlot.WinForms 而非 ScottPlot(核心库)。

    • 检查命名空间:using ScottPlot.WinForms;。

  2. 性能问题:

    • 本例数据量小(100 点),使用 Scatter 足够。若数据量大,使用 Signal 或 SignalXY:

      csharp

      plt.Add.SignalXY(xout.ToArray(), yout.ToArray());
  3. 负值区域不连续:

    • 确保 ResamplerMgr 的输入数据(x, y)有效。

    • 检查控制台日志,确认插值异常。

    • 增加 sampleCount 或使用 Resample2 增强平滑性。

  4. 跨平台需求:

    • ScottPlot 5.0.55 支持 WPF、Avalonia、Maui 等平台,需安装对应包(如 ScottPlot.WPF)。


负值问题解决总结

  • 问题:负值电压导致曲线不连续,通常由插值 NaN 或输入数据异常引起。

  • 解决方案:

    • 使用 CubicSpline 插值,提高负值区域平滑性。

    • 边界处理:插值异常时使用边界值。

    • 输入验证:确保 x 递增、无 NaN 或无穷大。

    • 增加采样点(sampleCount = 100),提高分辨率。

    • 日志记录:便于调试异常点。


扩展与参考

如果遇到特定问题(如输入数据导致不连续)或需要其他平台(如 WPF、Avalonia)的代码,请提供详情,我可以提供定制化解决方案!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐