简介:这份C#数据可视化项目源码面向具备一定C#基础、希望切入气象与环境数据分析领域的开发者,围绕74项环流指数(如北极涛动、南方涛动等)的变化趋势图绘制展开,解决从数据读取、统计计算到图表呈现的完整链路问题。压缩包共33个文件,约185KB,以cs源码文件为核心,辅以sln解决方案、csproj工程文件、resx资源与exe可执行程序,另含pdb调试符号、dll依赖库及少量缓存与配置项,构成可直接编译运行的Windows Forms工程。项目覆盖CSV/Excel数据读取、均值与标准差等统计处理、Charting图表组件绘制折线趋势、界面事件响应与图形样式定制等关键环节,并涉及大数据量下的分批加载思路。已有244人学习,适合作为数据可视化练手案例,帮助读者理解C#在气象数据分析中的落地方式与工程组织。
1. 用 C# 把 74 项环流指数画成趋势图:一份能直接跑的资源包
做气候诊断或大气环流分析的同行,大概率都遇到过这种局面:手里攥着 74 项环流指数(比如副高面积指数、极涡强度指数、南方涛动指数这类逐月序列),想快速看某项指数近几十年的演变趋势,结果要么在 Excel 里手动拉图拉到崩溃,要么用 Python 写完脚本发现团队里没人会维护。这份资源就是冲着这个痛点来的——它是一套用 C# 写的桌面端绘图工具,把 74 项指数的读取、解析、趋势拟合和可视化打包成可复用的工程,双击就能出图,也能嵌进你自己的 .NET 项目里二次开发。
它适合三类人:一是做气候统计、需要批量出图的业务人员;二是用 C# 做气象软件、想找个现成绘图模块的开发者;三是教学场景里想让学生直观看到环流指数年际变化的老师。核心价值不在于算法多高深,而在于把「数据格式不统一、绘图库选型纠结、趋势线拟合参数不会调」这些琐碎环节一次性趟平了。下面我按「资源里有什么 → 怎么跑起来 → 参数怎么改 → 坑在哪」的顺序拆一遍。
2. 资源拆包:74 项指数怎么组织,绘图链路怎么走
2.1 数据层:指数文件长什么样,解析器怎么认
74 项环流指数通常来自公开的气候指数数据集,常见格式是纯文本的逐月序列,每行一个年份,后面跟 12 个月的数值,缺测用 -999 或 999 占位。资源包里一般会带一个Data目录,里面按指数名称分文件,比如SubtropicalHighArea.txt、PolarVortexIntensity.txt这种。解析器要做的事很明确:跳过文件头注释行、按空白或制表符切分、把缺测值转成double.NaN,最后塞进一个List<IndexSeries>结构里。
我一般会先写一个轻量的解析类,不依赖第三方 CSV 库,因为气象指数文件的头几行经常带说明文字,通用 CSV 解析器反而容易翻车。下面这段是资源里解析逻辑的简化版,你可以直接抄:
public class IndexParser { // 读取单个指数文件,返回按年份排序的月度序列 public static IndexSeries Parse(string filePath, string indexName) { var series = new IndexSeries { Name = indexName }; foreach (var line in File.ReadLines(filePath)) { // 跳过以 # 或 // 开头的注释行,以及空行 var trimmed = line.Trim(); if (trimmed.StartsWith("#") || trimmed.StartsWith("//") || trimmed.Length == 0) continue; var parts = trimmed.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length < 13) continue; // 年份 + 12 个月,不够就丢 if (!int.TryParse(parts[0], out int year)) continue; var monthly = new double[12]; for (int m = 0; m < 12; m++) { if (double.TryParse(parts[m + 1], out double val)) { // 缺测值统一转 NaN,后续绘图时断线处理 monthly[m] = (val == -999 || val == 999) ? double.NaN : val; } else { monthly[m] = double.NaN; } } series.AddYear(year, monthly); } return series; } }逻辑说明:File.ReadLines是流式读取,74 个文件加起来内存占用也可控;parts.Length < 13这个判断能挡掉大部分格式异常行;缺测转NaN是关键一步,后面绘图库遇到NaN会自动断开线段,不会画出误导性的直线。参数方面,如果你的指数文件缺测标记不是 -999,改val == -999 || val == 999这一行即可;如果文件是逗号分隔,把Split的字符数组换成new[] { ',' }。
2.2 绘图层:为什么选 ScottPlot 而不是自己画 GDI+
C# 里画趋势图,常见方案有三类:一是原生 GDI+ 手绘,二是 MSChart(老牌但样式陈旧),三是 ScottPlot 这类现代轻量库。资源里选的是 ScottPlot,理由很实际:它一行代码就能把double[]渲染成折线图,自带坐标轴缩放、鼠标滚轮交互和导出 PNG,省掉了自己处理坐标变换和重绘的麻烦。74 项指数如果每项都手绘,光坐标轴刻度计算就能写几百行,没必要。
核心绘图代码大概长这样:
// 假设已拿到某指数的年均值序列 years[] 和 values[] var plt = new ScottPlot.Plot(800, 450); // 添加原始年均值折线 var scatter = plt.AddScatter(years, values); scatter.Label = "年均值"; scatter.MarkerSize = 0; // 不要点,只要线 // 添加线性趋势线,用最小二乘拟合 var (slope, intercept) = LinearRegression.Fit(years, values); double[] trendY = years.Select(x => slope * x + intercept).ToArray(); var trendLine = plt.AddScatter(years, trendY); trendLine.Label = $"趋势 (斜率={slope:F4})"; trendLine.LineStyle = ScottPlot.LineStyle.Dash; plt.XLabel = "年份"; plt.YLabel = "指数值"; plt.Title($"环流指数趋势图 - {series.Name}"); plt.Legend(); plt.SaveFig($"Output/{series.Name}_trend.png");逻辑说明:AddScatter返回的对象可以控制线型、标记和标签;趋势线用最小二乘拟合,斜率正负直接反映上升或下降趋势。参数上,MarkerSize = 0是为了让图面干净,如果你要标出具体年份的点,改成 3 到 5 即可;LineStyle.Dash让趋势线和原始线区分开。注意 ScottPlot 的版本差异较大,4.x 和 5.x 的 API 有变动,资源包里如果锁定了某个版本,别随意升级,否则AddScatter的返回值类型可能对不上。
2.3 批量出图:74 项怎么循环,输出目录怎么管
单张图跑通后,批量就是套一层循环。资源里一般会有一个BatchRunner类,遍历Data目录下所有.txt文件,逐个解析、绘图、保存。这里有个细节:74 项指数里有些序列很短,或者缺测太多,拟合趋势线没有统计意义。我的做法是加一个最小有效年数阈值,比如少于 20 年就跳过趋势线,只画原始折线,并在图标题里标注「样本不足」。
foreach (var file in Directory.GetFiles("Data", "*.txt")) { var name = Path.GetFileNameWithoutExtension(file); var series = IndexParser.Parse(file, name); // 统计有效年份,决定是否拟合趋势 int validYears = series.Years.Count(y => series.GetAnnualMean(y).HasValue); bool doTrend = validYears >= 20; DrawAndSave(series, doTrend); Console.WriteLine($"{name} 完成,有效年份 {validYears}"); }参数说明:validYears >= 20这个阈值可以按业务调整,气候指数一般建议至少 30 年才谈趋势,但教学演示 20 年也够看。输出目录建议按指数名建子文件夹,避免 74 张图堆在一起找不着。控制台输出进度是给自己看的,跑批时心里有数。
3. 环境搭起来:从零跑通第一张趋势图的完整步骤
3.1 工程创建与依赖安装
资源包如果是完整解决方案,直接双击.sln用 Visual Studio 打开即可。如果只有源码文件,需要自己建工程。步骤不复杂,但有几个版本坑要提前说。
第一步,装 .NET SDK。资源里如果用的是 .NET Framework 4.7.2,那就用 Visual Studio 2019 或 2022 建「Windows 窗体应用 (.NET Framework)」;如果是 .NET 6/8,建「Windows 窗体应用」或控制台应用都行。我一般推荐 .NET 6 以上,跨平台且 NuGet 包管理更顺。
第二步,装 ScottPlot。在解决方案资源管理器里右键项目 →「管理 NuGet 程序包」→ 搜索ScottPlot.WinForms(窗体项目)或ScottPlot(控制台项目)→ 安装。注意版本:如果资源代码里写的是plt.AddScatter,那大概率是 ScottPlot 4.x,装 4.1.x 系列;如果写的是plt.Add.Scatter,那是 5.x。装错版本会报「不包含 AddScatter 的定义」,这是血泪经验。
第三步,把Data目录和源码文件拷进项目,确保.cs文件的命名空间和你的项目一致,或者直接改using。
3.2 数据准备:74 项指数文件的放置与格式核对
数据文件放哪很关键。资源里通常约定Data目录在可执行文件同级,但调试时工作目录是bin/Debug/net6.0,直接放那里容易乱。我的习惯是在项目根目录建Data,然后在代码里用相对路径../../Data或者干脆在.csproj里加一条复制规则:
<ItemGroup> <None Update="Data\**\*.txt"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup>这样编译时数据文件会自动复制到输出目录,代码里写Data/xxx.txt就能找到。格式核对方面,打开任意一个指数文件,确认三件事:第一行是不是注释、列数是不是 13(年份+12 月)、缺测标记是什么。这三项对不上,解析器就要改。我见过有的数据集月份是倒序的(从 12 月到 1 月),这种要在解析后反转数组,否则图画出来季节相位是反的,趋势看着就怪。
3.3 跑通第一张图:单指数调试与输出验证
别一上来就跑 74 项,先挑一个数据最全的指数,比如副高面积指数,单独调。在Main里写死文件路径,跑一遍,看输出 PNG 是否正常。验证要点:横轴年份范围对不对、纵轴数值量级对不对、缺测年份有没有断线、趋势线斜率符号是否符合常识(比如副高面积指数在变暖背景下大概率是上升趋势)。
如果图出来了但趋势线是平的,检查LinearRegression.Fit里年份有没有转成double,整数除法会直接把斜率算成 0,这个坑我踩过不止一次。如果图是空白的,先看values数组是不是全NaN,多半是缺测标记没匹配上。单张图确认无误后,再把循环打开跑批量,74 张图大概几十秒到两分钟,取决于分辨率和是否开抗锯齿。
4. 参数调优:趋势拟合、坐标轴和导出质量的实操设置
4.1 趋势拟合:最小二乘之外,滑动平均怎么叠
线性趋势是最基础的,但环流指数往往有年际振荡,直接看原始折线很乱。资源里一般会提供滑动平均的选项,常见的是 5 年或 11 年滑动。实现上就是在绘图前对values做一次卷积:
// 计算 5 年滑动平均,窗口不足的年份返回 NaN public static double[] MovingAverage(double[] input, int window) { var result = new double[input.Length]; for (int i = 0; i < input.Length; i++) { if (i < window - 1) { result[i] = double.NaN; continue; } double sum = 0; int count = 0; for (int j = 0; j < window; j++) { if (!double.IsNaN(input[i - j])) { sum += input[i - j]; count++; } } result[i] = count > 0 ? sum / count : double.NaN; } return result; }参数说明:window设 5 能平滑掉大部分年际噪声,设 11 接近太阳活动周期,看你要突出什么信号。注意滑动平均后的序列两端会有NaN,绘图时自动断线,这是正常的。趋势线建议拟合在滑动平均序列上,而不是原始序列,这样斜率更稳健。但要在图例里注明「基于 5 年滑动平均」,不然读者会误以为你用了全部信息。
4.2 坐标轴与图面:让 74 张图风格统一
批量出图最怕风格不一致,有的图纵轴范围 0 到 100,有的 -50 到 50,放一起没法比。资源里一般会固定纵轴范围,或者按指数类别分组设定。我的做法是给每类指数配一个配置表:
| 指数类别 | 纵轴建议范围 | 是否显示零线 | 趋势线颜色 |
|---|---|---|---|
| 副高类 | 0 ~ 80 | 否 | 红色虚线 |
| 极涡类 | -60 ~ 20 | 是 | 蓝色虚线 |
| 涛动类 | -3 ~ 3 | 是 | 绿色虚线 |
| 其他 | 自动 | 是 | 灰色虚线 |
在 ScottPlot 里用plt.SetAxisLimits(yMin, yMax)固定范围,用plt.AddHorizontalLine(0)加零线。趋势线颜色通过trendLine.Color = Color.Red设置。图面统一后,74 张图拼在一起做对比分析才站得住脚。另外字号也要统一,plt.Style()可以一次性设字体,别每张图单独调。
4.3 导出与分辨率:PNG 还是 SVG,DPI 怎么定
ScottPlot 默认导出 PNG,plt.SaveFig(path, 800, 450)可以指定像素尺寸。如果图要进论文或报告,建议导出 SVG 或 PDF,矢量格式放大不糊。ScottPlot 4.x 里用plt.SaveFig(path, 800, 450)出 PNG,要 SVG 得换plt.SaveFig(path + ".svg")或者用Plot.SaveFig的重载。分辨率方面,屏幕看 96 DPI 够,印刷建议 300 DPI,对应像素宽度大概 2400。但 74 张图都导 300 DPI,文件体积会上去,按需选。
提示:导出前先调
plt.TightenLayout()或类似方法,把图例和坐标轴标签的边距收紧,否则保存的图四周留白很大,插进文档里显得图很小。
5. 避坑与排查:74 项批量绘图里最容易翻车的五件事
5.1 现象:图能出来但趋势线斜率明显不对
原因:年份数组是int,拟合时直接参与除法运算,整数除法把小数部分截断了。或者缺测值没转NaN,-999 被当成真实值参与拟合,直接把斜率拉偏。
解决:拟合前把年份转成double,years.Select(y => (double)y).ToArray();解析阶段严格把 -999 和 999 转NaN,并在拟合函数里过滤掉NaN对应的点。我一般会在拟合前打印一下有效点数,少于 20 个就警告。
5.2 现象:批量跑到一半程序卡死或报内存不足
原因:每张图都新建ScottPlot.Plot对象,但没有释放,74 个 Plot 实例加上位图缓存,内存涨到几百兆。或者File.ReadLines被换成了File.ReadAllLines,74 个文件全读进内存。
解决:用using包住 Plot 对象,或者每画完一张手动plt.Dispose();读取坚持用File.ReadLines流式。另外 ScottPlot 的SaveFig之后可以调plt.Clear()复用实例,但更稳妥的还是每张新建、用完释放。
5.3 现象:中文标题显示成方块
原因:ScottPlot 默认字体不含中文字形,plt.Title("副高面积指数")渲染出来是方框。
解决:设置支持中文的字体,比如plt.Style(fontName: "Microsoft YaHei")或plt.Title("...", fontName: "SimHei")。注意服务器或没装中文字体的环境要换字体文件路径,别硬编码「微软雅黑」,换台机器就翻车。
5.4 现象:缺测年份被画成了直线连接
原因:缺测值转成了 0 而不是NaN,绘图库把 0 当成有效值,前后年份直接连起来,趋势看着完全失真。
解决:解析时严格判断缺测标记,转double.NaN。ScottPlot 遇到NaN默认断线,如果没断,检查是不是用了AddScatter的OnNaN相关设置,或者数据里混了double.NegativeInfinity之类的异常值。
5.5 现象:74 张图输出后文件名乱码或覆盖
原因:指数文件名含中文或特殊字符,Path.GetFileNameWithoutExtension拿到的名字直接当输出名,在某些系统上编码不对。或者多个文件解析后series.Name重复,后画的覆盖前面的。
解决:输出文件名做一次清洗,只保留字母数字和下划线,中文名可以映射成拼音或编号。批量前先检查Data目录下文件名是否唯一,重复的加序号。我一般会在控制台打印「输入文件 → 输出文件」的映射表,跑完扫一眼就知道有没有覆盖。
6. 进阶技巧:把 74 张图拼成一张总览面板
单张趋势图看细节,但 74 项放一起对比,得做总览面板。我的做法是用 ScottPlot 的多子图功能,或者干脆用 GDI+ 把 74 张 PNG 拼成一张大图。ScottPlot 4.x 支持plt.AddSubplot或者用Multiplot类,但 74 个子图太多,屏幕放不下。更实际的是按类别分四张面板,每张面板 18 到 20 个子图,用Multiplot排成 5 行 4 列。
var multi = new ScottPlot.Multiplot(1600, 1200); foreach (var series in panelSeries) { var sub = multi.AddPlot(); sub.AddScatter(series.Years, series.Values); sub.Title(series.Name, fontName: "Microsoft YaHei", fontSize: 10); sub.TightenLayout(); } multi.SaveFig("Output/panel_1.png");参数说明:Multiplot的构造参数是总像素宽高,1600×1200 下每个子图大概 300×200,够看趋势。fontSize调小到 10 避免标题溢出。如果 ScottPlot 版本不支持Multiplot,退而求其次用System.Drawing把已保存的 PNG 按网格画到一张大 Bitmap 上,代码多二十行但兼容性好。
验证方法:拼完后打开大图,随机挑三个子图,和单独输出的对应图对比,确认坐标轴范围、趋势线斜率一致。我习惯在拼图前给每张子图加一个编号,和原始文件名对应,方便回溯。
从那以后我每次做批量绘图,都强制先跑一个「单指数 + 打印有效点数 + 检查输出文件列表」的预检流程,确认无误再开全量。这个习惯帮我省掉了无数次返工。希望帮到你。
本文还有配套的精品资源,点击获取