几种常见的 .NET 绘图库(包括 ScottPlot 5.0.55、OxyPlot、LiveCharts 2 和 Plotly.NET)在绘制电压曲线(如本例中的正负值电压数据)场景下的详细比较,结合 ResamplerMgr 类重采样需求,分析其功能、性能、易用性、跨平台支持和适用性。示例基于 Windows Forms 环境,但也会提及其他平台支持(如 WPF、Avalonia)。最后提供一个使用 OxyPlot 的完整示例代码,与 ScottPlot 示例对比,包含清晰的中文注释。


绘图库比较

以下比较基于以下维度:

  • 功能:支持的图表类型、交互功能、自定义能力。

  • 性能:处理大数据集(如数千点以上)的效率。

  • 易用性:API 设计、学习曲线、文档质量。

  • 跨平台支持:支持 Windows Forms、WPF、Avalonia、Maui 等。

  • 社区与生态:社区活跃度、更新频率、开源许可。

  • 适用性:适合电压曲线(正负值、平滑重采样)的特点。

1. ScottPlot 5.0.55

  • 功能:

    • 支持散点图、折线图、信号图(Signal)、热图等,适合科学数据可视化。

    • 交互功能:鼠标缩放、拖动、右键菜单、坐标显示(通过 GetCoordinates)。

    • 自定义能力:支持颜色、线宽、标记形状、图例、轴标签等。

    • 图像导出:支持 PNG、SVG、JPEG、BMP。

  • 性能:

    • 优异,尤其在大数据集(>10,000 点)时,使用 Signal 或 SignalXY 可高效渲染。

    • 本例(100 点)使用 Scatter 足够,性能无瓶颈。

  • 易用性:

  • 跨平台支持:

    • 支持 Windows Forms(FormsPlot)、WPF(WpfPlot)、Avalonia、Maui、Blazor。

    • 需安装对应包(如 ScottPlot.WinForms、ScottPlot.WPF)。

  • 社区与生态:

    • 开源(MIT 许可),社区活跃,GitHub 仓库更新频繁。

    • 版本 5.0.55(2023 年发布)优化了性能和 API。

  • 适用性:

    • 非常适合电压曲线场景,负值区域通过 AutoScale 自动适配。

    • 结合 ResamplerMgr,平滑曲线渲染良好,支持交互(鼠标坐标显示)。

    • 适合快速原型开发和科学数据可视化。

  • 不足:

    • 复杂图表(如 3D 或动态仪表盘)支持有限。

    • 交互功能较基础,高级交互需自定义。

2. OxyPlot

  • 功能:

    • 支持广泛的图表类型:折线图、散点图、柱状图、饼图、热图、3D 图等。

    • 交互功能:缩放、平移、悬停显示数据点、选择区域。

    • 自定义能力:高度可配置,支持自定义轴、样式、注解。

    • 图像导出:支持 PNG、SVG、PDF。

  • 性能:

    • 中等,适合中小数据集(<10,000 点)。大数据集可能需要优化。

    • 本例(100 点)性能良好,无明显延迟。

  • 易用性:

    • API 较为复杂,需创建 PlotModel 和系列(如 LineSeries)。

    • 学习曲线稍陡,但文档全面(https://oxyplot.github.io/)。

    • 提供设计器支持,适合拖放开发。

  • 跨平台支持:

    • 支持 Windows Forms、WPF、Avalonia、Xamarin、Maui。

    • 需安装对应包(如 OxyPlot.WindowsForms、OxyPlot.Wpf)。

  • 社区与生态:

    • 开源(MIT 许可),社区活跃但更新频率低于 ScottPlot。

    • 长期稳定,适合传统 .NET 应用。

  • 适用性:

    • 适合电压曲线,支持负值和重采样数据。

    • 提供更丰富的交互(如数据点跟踪器)和自定义选项。

    • 适合需要复杂图表或高可配置性的场景。

  • 不足:

    • API 复杂,代码量较多。

    • 大数据集性能不如 ScottPlot 的 Signal。

3. LiveCharts 2

  • 功能:

    • 专注于动态和交互式图表,支持折线图、柱状图、饼图、仪表盘等。

    • 交互功能:动画、工具提示、缩放、平移,适合现代 UI。

    • 自定义能力:支持主题、动画、数据绑定。

    • 图像导出:支持 PNG、SVG。

  • 性能:

    • 优化动态数据,适合实时更新场景。

    • 本例(100 点)性能优秀,但大数据集需测试。

  • 易用性:

    • API 现代化,基于 MVVM 模式,适合 WPF 和 Maui。

    • 学习曲线中等,文档完善(https://livecharts.dev/)。

    • Windows Forms 支持较弱,需额外配置。

  • 跨平台支持:

    • 主要支持 WPF、Maui、Avalonia,Windows Forms 支持有限。

    • 需安装 LiveChartsCore.SkiaSharpView.WinForms。

  • 社区与生态:

    • 开源(MIT 许可),社区较新但快速增长。

    • 版本 2.x 重构了 API,现代化但生态较年轻。

  • 适用性:

    • 适合动态电压曲线(如实时监控),负值区域支持良好。

    • 动画效果适合现代 UI,但本例静态数据无需动画。

    • Windows Forms 集成复杂,推荐 WPF。

  • 不足:

    • Windows Forms 支持不完善,配置复杂。

    • 文档示例偏向 WPF 和 Maui。

4. Plotly.NET

  • 功能:

    • 基于 Plotly.js,支持丰富的图表类型:折线图、散点图、3D 图、地图、统计图。

    • 交互功能:缩放、平移、悬停提示、导出交互式 HTML。

    • 自定义能力:高度灵活,支持 JSON 配置和动态样式。

    • 图像导出:支持 PNG、JPEG、SVG、HTML。

  • 性能:

    • 适合中小数据集(<10,000 点),大数据集需优化。

    • 本例(100 点)性能无问题。

  • 易用性:

  • 跨平台支持:

    • 主要通过 HTML 输出,支持所有平台(需浏览器或 WebView)。

    • Windows Forms/WPF 需嵌入 WebView 或导出静态图像。

  • 社区与生态:

    • 开源(MIT 许可),依托 Plotly.js 生态,社区活跃。

    • C# 绑定较新,生态不如 JavaScript 原生。

  • 适用性:

    • 适合交互式电压曲线,负值区域支持良好。

    • HTML 输出适合 Web 应用,但 Windows Forms 集成复杂。

    • 结合 ResamplerMgr,可生成平滑曲线。

  • 不足:

    • C# 集成复杂,需额外处理 HTML 或 WebView。

    • 性能不如 ScottPlot 和 OxyPlot。


比较总结

维度

ScottPlot 5.0.55

OxyPlot

LiveCharts 2

Plotly.NET

功能

散点图、折线图、信号图,交互基础

丰富图表类型,交互强大

动态图表,动画支持

丰富图表,交互式 HTML 输出

性能

优异(

Signal

适合大数据)

中等(中小数据集)

适合动态数据,静态稍逊

中等(依赖 Web 渲染)

易用性

简洁,学习曲线低,文档完善

稍复杂,文档全面

MVVM 友好,WinForms 复杂

独特 API,学习曲线陡

跨平台支持

WinForms、WPF、Avalonia、Maui

WinForms、WPF、Avalonia、Maui

WPF、Maui 为主,WinForms 有限

HTML 输出,需 WebView

社区与生态

开源,活跃,更新频繁

开源,稳定,更新较慢

开源,较新,快速增长

开源,依赖 Plotly.js 生态

适用性

电压曲线理想,负值平滑,快速开发

适合复杂图表,负值支持良好

动态场景优,WinForms 不佳

交互式 Web 图表,集成复杂

不足

复杂图表支持有限

API 复杂,性能中等

WinForms 支持弱,配置复杂

C# 集成复杂,性能一般

推荐选择:

  • ScottPlot 5.0.55:适合本例(电压曲线、负值数据、Windows Forms),API 简洁,性能优异,负值区域平滑,易于集成。

  • OxyPlot:适合需要复杂图表或高级交互的场景,Windows Forms 支持良好。

  • LiveCharts 2:推荐用于 WPF 或动态数据场景,Windows Forms 不理想。

  • Plotly.NET:适合 Web 应用或交互式 HTML 输出,桌面应用集成复杂。


OxyPlot 示例代码

以下是使用 OxyPlot(最新版本,例如 2.1.0)的完整示例代码,与 ScottPlot 示例功能一致,展示电压曲线(原始 vs 重采样),支持负值电压,确保平滑连续。代码基于 Windows Forms,使用 OxyPlot.WindowsForms,包含鼠标交互和图像保存。

安装 OxyPlot

bash

dotnet add package OxyPlot.WindowsForms --version 2.1.0
dotnet add package MathNet.Numerics --version 5.0.0

代码

csharp

using MathNet.Numerics;
using MathNet.Numerics.Interpolation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using OxyPlot;
using OxyPlot.Series;
using OxyPlot.WindowsForms; // OxyPlot 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];

                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);
                    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;
            }

            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;
            }

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

            return true;
        }
    }

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

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

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

            // 模拟电压数据:正弦波 + 随机噪声,包含负值
            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);

            // 配置 OxyPlot 图表
            var plotModel = new PlotModel
            {
                Title = "电压曲线(原始 vs 重采样)",
                DefaultFont = "Arial",
                TitleFontSize = 14
            };

            // 添加 X 轴(时间)
            plotModel.Axes.Add(new OxyPlot.Axes.LinearAxis
            {
                Position = OxyPlot.Axes.AxisPosition.Bottom,
                Title = "时间 (秒)",
                MajorGridlineStyle = LineStyle.Solid,
                MinorGridlineStyle = LineStyle.Dot
            });

            // 添加 Y 轴(电压)
            plotModel.Axes.Add(new OxyPlot.Axes.LinearAxis
            {
                Position = OxyPlot.Axes.AxisPosition.Left,
                Title = "电压 (伏特)",
                MajorGridlineStyle = LineStyle.Solid,
                MinorGridlineStyle = LineStyle.Dot
            });

            // 添加原始数据(蓝色散点)
            var scatterSeries = new ScatterSeries
            {
                Title = "原始数据",
                MarkerType = MarkerType.Circle,
                MarkerSize = 5,
                MarkerFill = OxyColors.Blue
            };
            for (int i = 0; i < x.Count; i++)
            {
                scatterSeries.Points.Add(new ScatterPoint(x[i], y[i]));
            }
            plotModel.Series.Add(scatterSeries);

            // 添加重采样数据(红色折线)
            var lineSeries = new LineSeries
            {
                Title = "重采样数据",
                Color = OxyColors.Red,
                LineStyle = LineStyle.Solid,
                StrokeThickness = 2
            };
            for (int i = 0; i < xout.Count; i++)
            {
                lineSeries.Points.Add(new DataPoint(xout[i], yout[i]));
            }
            plotModel.Series.Add(lineSeries);

            // 设置图例
            plotModel.LegendPosition = LegendPosition.RightTop;
            plotModel.LegendBackground = OxyColors.White;
            plotModel.LegendBorder = OxyColors.Black;

            // 绑定 PlotModel 到 PlotView
            plotView.Model = plotModel;

            // 添加鼠标交互:显示坐标
            plotView.MouseMove += (s, e) =>
            {
                var mousePos = plotView.InverseTransform(e.Location);
                form.Text = $"电压曲线展示 - 时间: {mousePos.X:F3} 秒, 电压: {mousePos.Y:F3} 伏特";
            };

            // 保存图像到文件
            try
            {
                using (var stream = System.IO.File.Create("voltage_curve_oxyplot.png"))
                {
                    var exporter = new PngExporter { Width = 800, Height = 600 };
                    exporter.Export(plotModel, stream);
                    Console.WriteLine("图像已保存为 voltage_curve_oxyplot.png");
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"保存图像失败:{ex.Message}");
            }

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

OxyPlot 示例中文解释

1. ResamplerMgr 类

  • 功能:与 ScottPlot 示例一致,使用 CubicSpline 插值进行重采样,确保负值电压区域平滑。

  • 方法:Resample、Resample2、ValidateInput,逻辑相同,处理负值异常。

2. 模拟电压数据

  • 生成逻辑:正弦波(±5V)加噪声(±0.25V),时间范围 -5 到 5 秒,步长 0.5。

  • 代码:与 ScottPlot 示例一致,生成 21 个点,包含负值。

3. OxyPlot PlotView 控件

  • 创建控件:

    • 使用 OxyPlot.WindowsForms.PlotView,设置 Dock = DockStyle.Fill。

  • 绘图配置:

    • 创建 PlotModel,设置标题(“电压曲线(原始 vs 重采样)”)。

    • 添加轴:

      • X 轴(LinearAxis):时间,带网格线。

      • Y 轴(LinearAxis):电压,带网格线。

    • 添加系列:

      • ScatterSeries:原始数据,蓝色圆点(MarkerType=Circle)。

      • LineSeries:重采样数据,红色折线(StrokeThickness=2)。

    • 设置图例:右上角,带白色背景和黑色边框。

  • 交互功能:

    • 使用 MouseMove 和 InverseTransform 获取鼠标坐标,更新窗体标题。

  • 保存图像:

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

  • 代码:

    csharp

    var plotModel = new PlotModel { Title = "电压曲线(原始 vs 重采样)" };
    var scatterSeries = new ScatterSeries { Title = "原始数据", MarkerType = MarkerType.Circle, MarkerSize = 5, MarkerFill = OxyColors.Blue };
    var lineSeries = new LineSeries { Title = "重采样数据", Color = OxyColors.Red, LineStyle = LineStyle.Solid, StrokeThickness = 2 };

4. 窗体显示

  • 创建 800x600 像素窗体,添加 PlotView 控件,运行应用程序。


ScottPlot vs OxyPlot 示例对比

特性

ScottPlot 5.0.55

OxyPlot 2.1.0

控件名称

FormsPlot

PlotView

代码量

较少,API 简洁

较多,需配置

PlotModel

和系列

绘图代码

plt.Add.Scatter

直接添加数据

需创建

ScatterSeries

LineSeries

交互实现

GetCoordinates

简单获取鼠标坐标

InverseTransform

获取坐标,需手动处理

图像导出

SavePng

简单调用

使用

PngExporter

,需创建流

负值支持

AutoScale

自动适配

需手动配置轴范围,负值支持良好

性能

优异,适合大数据(

Signal

中等,中小数据集表现良好

易用性

更简单,适合快速开发

较复杂,适合高级自定义

选择建议:

  • ScottPlot:推荐用于本例(电压曲线、Windows Forms),API 简洁,性能优,负值区域平滑。

  • OxyPlot:适合需要复杂图表(如多轴、3D)或高级交互的场景,代码稍复杂。


使用说明

  1. 运行 ScottPlot 示例:

    • 安装 ScottPlot.WinForms (5.0.55) 和 MathNet.Numerics。

    • 复制 ScottPlot 代码到 Program.cs,运行 Windows Forms 项目。

    • 检查 voltage_curve.png 输出。

  2. 运行 OxyPlot 示例:

    • 安装 OxyPlot.WindowsForms (2.1.0) 和 MathNet.Numerics。

    • 复制 OxyPlot 代码,运行项目。

    • 检查 voltage_curve_oxyplot.png 输出。

  3. 调试负值问题:

    • 若负值区域不连续,检查控制台日志(NaN 警告)。

    • 增加 sampleCount(例如 200)或使用 Resample2。

  4. 扩展:

    • 动态数据:LiveCharts 2 更适合实时更新。

    • Web 输出:Plotly.NET 适合 HTML 交互图表。

    • WPF:可提供 ScottPlot 或 OxyPlot 的 WPF 版本代码。


总结

  • ScottPlot 5.0.55:简洁高效,适合电压曲线快速可视化,负值区域平滑,推荐本例使用。

  • OxyPlot:功能丰富,交互强大,适合复杂场景,代码稍复杂。

  • LiveCharts 2:动态图表优,Windows Forms 支持弱。

  • Plotly.NET:交互式 Web 图表,桌面集成复杂。

  • 负值处理:所有库支持负值,结合 ResamplerMgr 的样条插值和边界处理,确保曲线连续。

如果需要 LiveCharts 2 或 Plotly.NET 示例代码,或特定数据测试,请提供详情,我可提供定制化实现!

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐