做数据可视化的时候,折线图是用得最多的图表类型。但默认的折线图往往太"硬"——直来直去的折线放在展示页面上,总感觉少了点什么。客户看了说"能不能做得好看一点",领导看了说"这个曲线能不能平滑一些",自己看了也觉得差点意思。
更让人头疼的是,LiveCharts 2 的文档虽然有,但关于平滑曲线、阶梯线、虚线这几个进阶配置,说得相当简略。很多开发者翻了半天文档,要么找不到对应的属性,要么配置了没效果,要么效果出来了但不知道怎么组合使用。
读完本文,你将掌握三个可以直接落地的技能:
GeometryFill、LineSmoothness 的正确配置方式测试环境:.NET 6 + LiveCharts2 0.19.x + WinForms,代码经过本地运行验证。
默认折线图用的是直线段连接数据点,这在数学上没问题,但在视觉传达上会带来两个问题。
第一,数据波动被放大。直线连接会让相邻数据点之间的变化显得很"突兀",尤其是传感器采集的连续数据,本来是平滑变化的物理量,折线图画出来却像锯齿一样,反而干扰了读者对趋势的判断。
第二,无法区分数据语义。有些数据是连续变化的(比如温度、压力),有些数据是离散跳变的(比如设备状态、库存数量)。用同一种直线去表达这两类数据,在语义上是有歧义的。阶梯线正是为后者设计的——它明确地告诉读者"这个值在某个时间点发生了突变,而不是线性过渡"。
很多开发者在遇到这个问题时,会尝试手动插值——在原始数据点之间插入大量中间点来"模拟"平滑效果。这个做法有几个明显的坑:
LiveCharts 2 其实内置了完整的曲线插值支持,根本不需要手动处理。问题只是很多人不知道去哪里找、怎么配置。
在深入代码之前,先把 LineSeries<T> 里几个最重要的属性梳理清楚:
| 属性 | 类型 | 作用 |
|---|---|---|
LineSmoothness | double(0~1) | 控制曲线平滑程度,0 为直线,1 为最大平滑 |
GeometrySize | double | 数据点圆点的大小,设为 0 可隐藏 |
GeometryFill | SolidColorPaint | 数据点填充色 |
GeometryStroke | SolidColorPaint | 数据点边框色 |
Stroke | IPaint<SkiaSharpDrawingContext> | 折线描边,虚线也在这里配置 |
Fill | IPaint<SkiaSharpDrawingContext> | 折线下方区域填充 |
StepLineSeries<T> 是独立的系列类型,用于阶梯线,属性结构与 LineSeries<T> 基本一致。
适合展示连续变化的物理量,如温度曲线、心率监测、股价走势、传感器数据实时展示等。平滑曲线能有效降低视觉噪声,让趋势更直观。
csharpusing LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using SkiaSharp;
namespace AppLiveChart13
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
InitChart();
}
private void InitChart()
{
// 模拟温度传感器数据(24小时)
var temperatureData = new double[]
{
18.2, 17.8, 17.1, 16.9, 16.5, 17.0,
18.5, 20.3, 22.1, 24.6, 26.2, 27.8,
28.5, 29.1, 28.7, 27.9, 26.5, 24.8,
23.1, 21.6, 20.4, 19.7, 19.1, 18.6
};
var smoothSeries = new LineSeries<double>
{
Values = temperatureData,
Name = "室内温度(°C)",
// 核心配置:平滑度设为 0.8,接近最大平滑
// 取值范围 0~1,0 = 折线,1 = 最大曲率
LineSmoothness = 0.8,
// 折线描边:蓝色,2px 宽
Stroke = new SolidColorPaint(SKColors.DodgerBlue, 2),
// 折线下方填充:半透明蓝色
Fill = new SolidColorPaint(SKColors.DodgerBlue.WithAlpha(40)),
// 数据点配置
GeometrySize = 8,
GeometryFill = new SolidColorPaint(SKColors.White),
GeometryStroke = new SolidColorPaint(SKColors.DodgerBlue, 2),
};
// 绑定到 CartesianChart 控件
cartesianChart1.Series = new ISeries[] { smoothSeries };
// X 轴配置(显示小时标签)
cartesianChart1.XAxes = new[]
{
new Axis
{
Name = "时间(小时)",
Labels = Enumerable.Range(0, 24)
.Select(h => $"{h}:00")
.ToArray(),
LabelsRotation = 45
}
};
cartesianChart1.YAxes = new[]
{
new Axis { Name = "温度(°C)" }
};
}
}
}

LineSmoothness 的取值不是越大越好。在我的项目实践中,0.5~0.7 是大多数场景下视觉效果最自然的区间。设置为 1 的时候,曲线在极值点附近可能会出现轻微的"过冲"——也就是曲线会短暂超出实际数据点的值域范围,在某些业务场景下(比如金融数据)这是不可接受的,需要特别注意。
阶梯线专门用于表达离散跳变的状态数据。比如设备运行状态(开/关)、库存数量变化、价格调整记录、系统告警级别等。这类数据在两个时间点之间并不存在"过渡",用阶梯线可以准确传达"值在某时刻发生了突变"这个语义。
csharpusing LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace AppLiveChart13
{
public partial class Form2 : Form
{
public Form2()
{
InitializeComponent();
InitChart();
}
private void InitChart()
{
// 模拟仓库库存变化数据
var stockData = new double[]
{
500, 500, 480, 480, 480,
320, 320, 320, 210, 210,
350, 350, 350, 280, 280,
150, 150, 420, 420, 420
};
// 使用 StepLineSeries 而非 LineSeries
var stepSeries = new StepLineSeries<double>
{
Values = stockData,
Name = "库存数量",
// 阶梯线描边:橙色,2.5px
Stroke = new SolidColorPaint(SKColors.OrangeRed, 2.5f),
// 阶梯线下方填充:半透明橙色
Fill = new SolidColorPaint(SKColors.OrangeRed.WithAlpha(30)),
// 数据点配置:稍大一些,方便识别跳变位置
GeometrySize = 10,
GeometryFill = new SolidColorPaint(SKColors.White),
GeometryStroke = new SolidColorPaint(SKColors.OrangeRed, 2),
};
cartesianChart1.Series = new ISeries[] { stepSeries };
cartesianChart1.XAxes = new[]
{
new Axis { Name = "时间节点" }
};
cartesianChart1.YAxes = new[]
{
new Axis { Name = "库存数量(件)", MinLimit = 0 }
};
}
}
}

StepLineSeries 的 LineSmoothness 属性不生效——这个类型的设计初衷就是硬折角,如果你在代码里设置了这个属性,编译不会报错,但运行时会被忽略。另外,StepLineSeries 默认的阶梯方向是"先水平后垂直"(即先延伸到下一个 X 位置,再跳变 Y 值),这符合大多数时序数据的语义,一般不需要修改。
虚线在图表中通常用于表达预测数据、参考基准线、目标值等"非实测"的信息。比如把实线用于历史数据,虚线用于未来预测,视觉上就能清晰区分两类数据的性质。
csharpusing LiveChartsCore;
using LiveChartsCore.SkiaSharpView;
using LiveChartsCore.SkiaSharpView.Painting;
using LiveChartsCore.SkiaSharpView.Painting.Effects;
using LiveChartsCore.SkiaSharpView.WinForms;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
namespace AppLiveChart13
{
public partial class Form3 : Form
{
public Form3()
{
InitializeComponent();
InitChart();
}
private void InitChart()
{
// 历史销售数据(实线)
var historicalData = new double[]
{ 120, 135, 128, 145, 162, 158, 171 };
// 预测销售数据(虚线)
// 注意:与历史数据共享最后一个点,保证曲线连续
var forecastData = new double[]
{ double.NaN, double.NaN, double.NaN, double.NaN,
double.NaN, double.NaN, 171, 185, 192, 210, 228 };
// 实线系列:历史数据
var historicalSeries = new LineSeries<double>
{
Values = historicalData,
Name = "历史销量",
LineSmoothness = 0.5,
Stroke = new SolidColorPaint(SKColors.SteelBlue, 2.5f),
Fill = null, // 不填充
GeometrySize = 8,
GeometryFill = new SolidColorPaint(SKColors.White),
GeometryStroke = new SolidColorPaint(SKColors.SteelBlue, 2),
};
// 虚线系列:预测数据
// 关键:通过 PathEffect 设置虚线样式
var dashedStroke = new SolidColorPaint(SKColors.OrangeRed, 2)
{
// DashEffect 参数:float[] 为虚线间隔模式
// { 实线长度, 间隔长度 },可以设置更复杂的模式
PathEffect = new DashEffect(new float[] { 6, 4 })
};
var forecastSeries = new LineSeries<double>
{
Values = forecastData,
Name = "预测销量",
LineSmoothness = 0.5,
Stroke = dashedStroke,
Fill = null,
GeometrySize = 6,
GeometryFill = new SolidColorPaint(SKColors.OrangeRed),
GeometryStroke = new SolidColorPaint(SKColors.OrangeRed, 1.5f),
};
cartesianChart1.Series = new ISeries[]
{
historicalSeries,
forecastSeries
};
// X 轴月份标签
cartesianChart1.XAxes = new[]
{
new Axis
{
Name = "月份",
Labels = new[]
{
"1月","2月","3月","4月","5月","6月",
"7月","8月","9月","10月","11月"
}
}
};
cartesianChart1.YAxes = new[]
{
new Axis { Name = "销售数量(件)", MinLimit = 0 }
};
}
}
}

DashEffect 接收一个 float[] 数组,定义虚线的绘制模式。数组元素按顺序表示"画线长度、跳过长度、画线长度、跳过长度……"的循环模式:
{ 6, 4 } — 画 6px,跳 4px,经典虚线{ 2, 3 } — 画 2px,跳 3px,密集点线{ 10, 4, 2, 4 } — 画 10px,跳 4px,画 2px,跳 4px,点划线在实际项目里,{ 8, 4 } 这个比例在大多数 DPI 下视觉效果最清晰,是我比较推荐的默认选择。
预测数据中用 double.NaN 填充历史段是一个关键技巧。LiveCharts 2 遇到 NaN 值时会自动断开连线,这样就能实现"实线和虚线在同一个 X 轴上各自覆盖不同区间"的效果,而不需要手动拆分 X 轴或做复杂的坐标偏移。
真实项目里,这三种线型往往需要组合使用。下面是一个把平滑曲线、阶梯线、虚线放在同一个图表里的完整示例,适合设备监控仪表盘这类场景:
csharpprivate void InitCombinedChart()
{
// 实时温度(平滑曲线)
var tempSeries = new LineSeries<double>
{
Values = new double[] { 22, 23, 24, 25, 24, 23, 22, 21, 22, 23 },
Name = "实时温度",
LineSmoothness = 0.7,
Stroke = new SolidColorPaint(SKColors.DodgerBlue, 2),
Fill = new SolidColorPaint(SKColors.DodgerBlue.WithAlpha(25)),
GeometrySize = 6,
GeometryFill = new SolidColorPaint(SKColors.White),
GeometryStroke = new SolidColorPaint(SKColors.DodgerBlue, 1.5f),
};
// 设备运行状态(阶梯线,映射到副 Y 轴)
var statusSeries = new StepLineSeries<double>
{
Values = new double[] { 1, 1, 0, 0, 1, 1, 1, 0, 1, 1 },
Name = "设备状态(1=运行)",
Stroke = new SolidColorPaint(SKColors.MediumSeaGreen, 2),
Fill = null,
GeometrySize = 0, // 隐藏数据点,状态线不需要圆点
ScalesYAt = 1, // 绑定到第二条 Y 轴
};
// 温度预警阈值(虚线基准线)
var thresholdData = Enumerable.Repeat(26.0, 10).ToArray();
var thresholdSeries = new LineSeries<double>
{
Values = thresholdData,
Name = "预警阈值(26°C)",
LineSmoothness = 0,
Stroke = new SolidColorPaint(SKColors.Tomato, 1.5f)
{
PathEffect = new DashEffect(new float[] { 8, 5 })
},
Fill = null,
GeometrySize = 0, // 阈值线不需要数据点圆点
};
cartesianChart1.Series = new ISeries[]
{
tempSeries,
statusSeries,
thresholdSeries
};
// 双 Y 轴配置
cartesianChart1.YAxes = new[]
{
new Axis { Name = "温度(°C)", Position = LiveChartsCore.Measure.AxisPosition.Start },
new Axis
{
Name = "设备状态",
Position = LiveChartsCore.Measure.AxisPosition.End,
MinLimit = -0.2,
MaxLimit = 1.5,
Labels = new[] { "停止", "", "运行" },
}
};
}

这个组合方案在工控监控界面里非常实用——一个图表同时展示连续的物理量、离散的设备状态和固定的告警阈值,信息密度高但层次清晰,不会让用户感到混乱。
以下数据在 i5-12400 / 16GB RAM / .NET 6 Release 模式下测试,供参考:
| 数据点数量 | LineSeries(平滑) | StepLineSeries | 虚线 LineSeries |
|---|---|---|---|
| 500 点 | ~8ms | ~6ms | ~9ms |
| 2000 点 | ~28ms | ~22ms | ~31ms |
| 5000 点 | ~71ms | ~58ms | ~78ms |
超过 5000 个数据点时建议开启 数据降采样(LiveCharts 2 提供了 DownSamplingMethod 属性),或在数据层做预处理,避免 UI 线程渲染压力过大导致界面卡顿。
平滑曲线用 LineSmoothness,值域 0~1,0.5~0.7 是最自然的区间。 阶梯线用独立的 StepLineSeries,别试图用平滑曲线去"模拟"跳变效果,语义上是错的。虚线通过 SolidColorPaint 的 PathEffect = new DashEffect(...) 配置,NaN 占位符是实现分段线型的关键技巧。
C# WinForms LiveCharts2 数据可视化 图表开发 SkiaSharp .NET
你在项目里用 LiveCharts 2 遇到过哪些"文档没说清楚"的坑?欢迎在评论区聊聊,大家一起把经验沉淀下来。另外,如果你的项目里有实时数据流(比如每秒更新 10 次以上)的场景,可以聊聊你是怎么处理渲染性能问题的——这个话题后续也可以单独展开写一篇。
相关信息
我用夸克网盘给你分享了「AppLiveChart13.zip」,点击链接或复制整段内容,打开「夸克APP」即可获取。
/22583YSmRo:/
链接:https://pan.quark.cn/s/3eb27461b1b8
提取码:XSQu
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!