深入了解 ScottPlot 的兴趣!ScottPlot 是一个功能强大、轻量级的 .NET 开源绘图库,专为快速、高效的 2D 数据可视化设计,广泛应用于科学、工程和数据分析领域。以下是对 ScottPlot 的全面介绍,涵盖其核心特性、架构、API 使用方法、性能分析、跨平台支持、定制化能力,以及一个基于 ScottPlot 5.0.55 的完整示例代码,展示如何绘制电压曲线(结合 ResamplerMgr 类处理负值电压,确保平滑连续)


1. ScottPlot 概述

  • 简介:

    • ScottPlot 是一个 .NET 开源绘图库,专注于简单、高性能的 2D 图表绘制,支持折线图、散点图、柱状图、热图等。

    • 开源(MIT 许可证),始于 2018 年,社区活跃,更新频繁。

    • GitHub 仓库:https://github.com/ScottPlot/ScottPlot

    • 最新版本(截至 2025 年 6 月 22 日):5.0.55(2025 年 5 月发布)。

    • 官网:ScottPlot - Interactive Plotting Library for .NET

    • 特点:API 简洁,性能优异,适合大数据和快速开发。

  • 核心特性:

    • 丰富的图表类型:支持折线图(Scatter)、信号图(Signal)、柱状图(Bar)、饼图(Pie)、热图(Heatmap)等。

    • 高性能:Signal 模式优化大数据(支持 >10 亿点),渲染时间 <1ms。

    • 跨平台:支持 Windows Forms、WPF、MAUI、Avalonia、Blazor、Console。

    • 交互功能:支持缩放、平移、鼠标事件,内置右键菜单(放大、自动缩放、保存图像)。

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

    • 简单 API:单行代码即可绘制图表,例如 plt.Add.Scatter(x, y)。

    • 轻量级:依赖少,适合嵌入式和快速原型开发。

  • 适用场景:

    • 科学数据可视化(如电压曲线、传感器数据)。

    • 大数据绘制(如时间序列、频谱分析)。

    • 简单交互和快速开发的桌面或控制台应用。

    • 跨平台数据可视化(MAUI、Avalonia)。


2. ScottPlot 架构

ScottPlot 采用简洁的分层设计:

  • Plot:

    • 核心对象,表示整个图表,包含所有绘图元素(系列、轴、标题、图例)。

    • 示例:var plt = new ScottPlot.Plot();

  • Plottables:

    • 数据系列,表示图表中的可视化元素。

    • 常见类型:

      • Scatter:散点图/折线图,适合中小数据集。

      • Signal:高效信号图,适合大数据(>百万点)。

      • Bar:柱状图。

      • Heatmap:热图。

    • 示例:plt.Add.Scatter(x, y);

  • Axes:

    • 坐标轴,控制 X/Y 轴的范围、标签、网格线。

    • 支持多轴(Axes.AddAxis)和自定义样式。

    • 示例:plt.Axes.Left.Label.Text = "电压";

  • Renderers:

    • 负责将 Plot 渲染为图像。

    • 基于 SkiaSharp(跨平台 2D 图形库),支持硬件加速。

    • 示例:plt.Save("chart.png", 800, 600);

  • Controls:

    • 平台特定的控件,负责 UI 显示和交互。

    • 示例:

      • WinForms:ScottPlot.WinForms.FormsPlot。

      • WPF:ScottPlot.Wpf.WpfPlot。

      • MAUI:ScottPlot.Maui.MauiPlot。

  • Interaction:

    • 内置鼠标交互(缩放、平移、右键菜单)。

    • 支持自定义事件(如 MouseMove 获取坐标)。


3. ScottPlot API 使用方法

以下是使用 ScottPlot 5.0.55 绘制图表的基本步骤:

  1. 安装 NuGet 包:

    • Windows Forms:

      bash

      dotnet add package ScottPlot.WinForms --version 5.0.55
      dotnet add package MathNet.Numerics --version 5.0.0
      dotnet add package System.Drawing.Common --version 8.0.0
    • WPF(替代示例):

      bash

      dotnet add package ScottPlot.Wpf --version 5.0.55
  2. 创建 Plot:

    • 初始化 Plot 对象:

      csharp

      var plt = new ScottPlot.Plot();
  3. 添加数据系列:

    • 添加散点图或折线图:

      csharp

      var scatter = plt.Add.Scatter(x, y);
      scatter.MarkerStyle.Size = 5;
      scatter.LineStyle.Width = 2;
  4. 配置轴和样式:

    • 设置标题和轴标签:

      csharp

      plt.Title("电压曲线");
      plt.Axes.Left.Label.Text = "电压 (伏特)";
      plt.Axes.Bottom.Label.Text = "时间 (秒)";
  5. 绑定到控件:

    • 在 WinForms 中使用 FormsPlot:

      csharp

      var formsPlot = new FormsPlot { Dock = DockStyle.Fill };
      formsPlot.Plot.Add.Scatter(x, y);
  6. 交互与导出:

    • 添加鼠标交互:

      csharp

      formsPlot.MouseMove += (s, e) =>
      {
          var coords = formsPlot.Plot.GetCoordinates(new Pixel(e.Location.X, e.Location.Y));
          form.Text = $"时间: {coords.X:F3} 秒, 电压: {coords.Y:F3} 伏特";
      };
    • 导出图像:

      csharp

      plt.Save("chart.png", 800, 600, ImageFormat.Png);

4. ScottPlot 性能分析

  • 中小数据集(<100,000 点):

    • 性能极佳,渲染和交互无延迟,适合本例(100 点)。

  • 大数据集(>100,000 点):

    • Signal 模式使用高效算法,渲染 >10 亿点只需数毫秒。

    • 优于 OxyPlot,接近 LiveCharts2 的 Backers 包。

  • 动态更新:

    • 支持实时更新,但需手动调用 formsPlot.Refresh()。

    • 性能依赖于数据量和刷新频率。

  • 局限:

    • 复杂交互(如内置 Tooltip)需手动实现。

    • 复杂图表(如 3D、热图)支持有限。


5. 跨平台支持

ScottPlot 5.0.55 支持多种平台:

  • Windows Forms:

    • 使用 ScottPlot.WinForms.FormsPlot。

    • 适合传统桌面应用,交互简单。

  • WPF:

    • 使用 ScottPlot.Wpf.WpfPlot。

    • 支持 MVVM,但交互仍需手动实现。

  • MAUI:

    • 使用 ScottPlot.Maui.MauiPlot。

    • 支持 iOS、Android、Windows、macOS。

  • Avalonia:

    • 使用 ScottPlot.Avalonia.AvaloniaPlot。

    • 跨平台桌面应用。

  • Blazor:

    • 支持 Web 应用,生成静态图像。

  • Console:

    • 支持生成图像到文件,适合脚本和服务器端。

  • 局限:

    • WinForms 和 WPF 交互不如 LiveCharts2 丰富。

    • MAUI 和 Avalonia 支持较新,文档较少。


6. 定制化能力

ScottPlot 提供灵活的定制化,但以简单性为优先:

  • 样式:

    • 自定义颜色、线型、标记:

      csharp

      scatter.Color = Colors.Blue;
      scatter.MarkerStyle.Size = 5;
      scatter.LineStyle.Width = 2;
  • 多轴:

    • 支持添加额外轴:

      csharp

      var secondaryAxis = plt.Axes.AddLeftAxis();
      secondaryAxis.Label.Text = "功率";
  • 交互:

    • 自定义鼠标事件:

      csharp

      formsPlot.MouseMove += (s, e) => { /* 自定义逻辑 */ };
    • 内置右键菜单(放大、自动缩放、保存)。

  • 主题:

    • 支持自定义背景、网格、字体:

      csharp

      plt.FigureBackground.Color = Colors.White;
      plt.Grid.MajorLineColor = Colors.Gray;
  • 局限:

    • 复杂交互(如 Tooltip)需手动实现。

    • 不支持动态主题切换。


7. ScottPlot 示例代码

以下是基于 ScottPlot 5.0.55 的完整示例代码,与 OxyPlot 和 LiveCharts2 示例功能一致,绘制电压曲线(原始 vs 重采样),支持负值电压,确保平滑连续。代码基于 Windows Forms,包含鼠标交互(窗体标题显示坐标)、图像导出,并结合 ResamplerMgr 类。

环境要求

  • NuGet 包:

    bash

    dotnet add package ScottPlot.WinForms --version 5.0.55
    dotnet add package MathNet.Numerics --version 5.0.0
    dotnet add package System.Drawing.Common --version 8.0.0
  • 项目:Windows Forms,.NET 8.0 或 .NET Framework 4.8。

代码

csharp

using MathNet.Numerics;
using MathNet.Numerics.Interpolation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
using System.Drawing; // 添加 System.Drawing 引用
using ScottPlot;
using ScottPlot.WinForms;

namespace ResamplerLib
{
    /// <summary>
    /// 重采样管理类,用于对电压数据进行插值重采样,确保负值区域平滑连续。
    /// 使用 MathNet.Numerics 的样条插值,支持均匀采样和指定点采样。
    /// </summary>
    public static class ResamplerMgr
    {
        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);
            }
        }

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

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

        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>
    /// 主程序:模拟电压数据,执行重采样,使用 ScottPlot 5.0.55 绘制曲线。
    /// 支持鼠标交互(显示坐标),保存图像为 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)
            {
                x.Add(t);
                double voltage = 5.0 * Math.Sin(t) + (rand.NextDouble() - 0.5) * 0.5;
                y.Add(voltage);
            }

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

            // 配置 ScottPlot 图表
            var plt = formsPlot.Plot;

            // 添加原始数据(蓝色散点)
            var scatterOriginal = plt.Add.Scatter(x.ToArray(), y.ToArray(), color: Colors.Blue);
            scatterOriginal.MarkerStyle.Size = 5;
            scatterOriginal.Label = "原始数据";

            // 添加重采样数据(红色折线)
            var scatterResampled = plt.Add.Scatter(xout.ToArray(), yout.ToArray(), color: Colors.Red);
            scatterResampled.MarkerStyle.Size = 0; // 无散点
            scatterResampled.LineStyle.Width = 2;
            scatterResampled.Label = "重采样数据";

            // 设置图表标题和轴标签
            plt.Title("电压曲线(原始 vs 重采样)", size: 14);
            plt.Axes.Bottom.Label.Text = "时间 (秒)";
            plt.Axes.Left.Label.Text = "电压 (伏特)";

            // 显示图例
            plt.Legend.IsVisible = true;
            plt.Legend.Alignment = Alignment.UpperRight;

            // 自动调整轴范围
            plt.Axes.AutoScale();

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

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

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

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

示例代码中文解释

1. ResamplerMgr 类

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

  • 关键特性:

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

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

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

    • 支持均匀采样(Resample)、指定点采样(Resample 重载)和两次重采样(Resample2)。

  • 方法详解:

    • Resample:生成均匀采样点。

    • Resample(重载):根据指定 xx 插值。

    • Resample2:两次重采样,增强平滑性。

    • ValidateInput:检查输入有效性。

2. 模拟电压数据

  • 生成逻辑:

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

    • 电压(y):正弦波(5.0 * Math.Sin(t))加噪声(±0.25V),包含负值。

    • 固定种子(Random(42))确保可重复。

3. ScottPlot FormsPlot 控件

  • 创建控件:

    • 使用 ScottPlot.WinForms.FormsPlot,设置 Dock = DockStyle.Fill。

  • 绘图配置:

    • Scatter:

      • 原始数据:蓝色散点,MarkerStyle.Size = 5。

      • 重采样数据:红色折线,MarkerStyle.Size = 0,LineStyle.Width = 2。

    • Axes:

      • X 轴:时间,Axes.Bottom.Label.Text。

      • Y 轴:电压,Axes.Left.Label.Text。

    • Title:设置标题,Title 方法。

    • Legend:右上角,Legend.IsVisible = true。

    • AutoScale:自动调整轴范围。

  • 交互:

    • MouseMove:将 System.Drawing.Point 转换为 Pixel,显示坐标。

  • 保存:

    • 使用 Save 方法保存为 800x600 PNG。

  • 代码:

    csharp

    var scatterOriginal = plt.Add.Scatter(x.ToArray(), y.ToArray(), color: Colors.Blue);
    scatterOriginal.MarkerStyle.Size = 5;
    scatterOriginal.Label = "原始数据";
    var scatterResampled = plt.Add.Scatter(xout.ToArray(), yout.ToArray(), color: Colors.Red);
    scatterResampled.MarkerStyle.Size = 0;
    scatterResampled.LineStyle.Width = 2;
    scatterResampled.Label = "重采样数据";

4. 运行结果

  • 输出:

    • 显示 800x600 窗体,包含:

      • 蓝色散点:原始数据(21 点,含噪声)。

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

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

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

    • 交互:窗体标题显示鼠标位置坐标,右键菜单支持缩放/保存。

    • 保存:生成 voltage_curve_scottplot.png。

  • 负值处理:

    • CubicSpline 插值确保负值区域平滑。

    • Axes.AutoScale() 自动包含负值。


8. ScottPlot vs OxyPlot vs LiveCharts2 对比

维度

ScottPlot 5.0.55

OxyPlot 2.1.0

LiveCharts2 2.0.0-rc2

功能

专注 2D 图表(散点、线、信号),功能较少

丰富(折线、散点、热图、3D 等高线)

丰富(折线、柱状、饼图、热图、仪表盘)

性能

优异,

Signal

适合 >10亿点

中等,适合 <10,000 点

优异,Backers 包支持 >50M 点,<1ms 响应

易用性

API 简洁,学习曲线低,文档示例丰富

API 复杂,学习曲线高

中等,MVVM 友好,学习曲线适中

交互性

基础(缩放、平移、右键菜单),需手动实现复杂交互

强大(内置 Tracker、缩放、选择)

强大(Tooltip、缩放、平移),WinForms 稍弱

跨平台

WinForms、WPF、MAUI、Avalonia、Blazor

WinForms、WPF、Avalonia、Maui

WPF、WinForms、MAUI、Uno、Avalonia、Blazor

定制化

灵活,但复杂图表支持有限

高度灵活(多轴、注解、主题)

高度灵活(主题、动画、多轴)

社区与生态

活跃,更新频繁

稳定,更新较慢

活跃,快速更新,Beta 阶段

适用性

适合简单曲线、大数据、快速开发

适合复杂交互、工程应用

适合实时、跨平台、动画丰富的场景

代码复杂度

低,单行 API 即可绘图

高,需配置

PlotModel

中等,MVVM 需额外配置

  • ScottPlot 优势:

    • API 极简,适合快速开发。

    • 大数据性能优(Signal 模式)。

    • 轻量级,依赖少。

  • OxyPlot 优势:

    • 内置 Tracker,交互开箱即用。

    • 支持复杂图表(多轴、热图)。

  • LiveCharts2 优势:

    • 跨平台支持强大,动画流畅。

    • MVVM 友好,适合动态数据。

  • 推荐:

    • ScottPlot:适合本例(电压曲线)中简单、高效的绘制需求,特别适合大数据和快速原型。

    • OxyPlot:适合需要高级交互和复杂图表的工程应用。

    • LiveCharts2:适合跨平台、实时更新、动画丰富的场景。


9. 使用说明与优化

  1. 运行示例:

    • 创建 Windows Forms 项目。

    • 安装 NuGet 包:

      bash

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

    • 检查 voltage_curve_scottplot.png 输出。

  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

      var secondaryAxis = plt.Axes.AddLeftAxis();
      secondaryAxis.Label.Text = "功率";
      var powerSeries = plt.Add.Scatter(x, powerData);
      powerSeries.Axes.YAxis = secondaryAxis;
    • 动态更新:

      csharp

      scatterOriginal.Data.X = newX;
      scatterOriginal.Data.Y = newY;
      formsPlot.Refresh();
    • WPF:

      • 安装 ScottPlot.Wpf:

        bash

        dotnet add package ScottPlot.Wpf --version 5.0.55
      • 使用 WpfPlot:

        csharp

        using ScottPlot.Wpf;
        var wpfPlot = new WpfPlot { Width = 800, Height = 600 };
  5. 性能优化:

    • 大数据:使用 Signal:

      csharp

      plt.Add.SignalXY(xout.ToArray(), yout.ToArray());
    • 禁用交互:

      csharp

      formsPlot.Configuration.Zoom = false;
      formsPlot.Configuration.Pan = false;

10. 负值问题解决总结

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

  • 解决:

    • 使用 CubicSpline 插值,增强负值区域平滑性。

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

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

    • 增加采样点(sampleCount = 100)。

    • 日志记录:便于调试。


11. 资源与参考

如果需要 WPF/MAUI 版本、动态数据示例、或特定场景优化(如多轴、大数据),请提供详情,我将提供定制代码!

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐