简介:这是一套专为C# MVC开发者设计的ECharts模型层建模库,解决.NET后端与前端图表库深度集成难题,显著降低数据可视化开发门槛。资源共369个文件,涵盖110个核心C#模型类(如图表配置、坐标轴、系列项等)、80个编译后DLL(含MVC适配层与核心序列化组件)、57个XML文档(提供完整API说明)及配套CSProj工程文件,整体压缩包14.18MB,结构清晰,支持开箱即用。已有291人学习下载,适合中高级.NET开发者快速构建动态仪表盘、BI看板等业务场景。读者可直接复用预定义的强类型模型类生成标准ECharts JSON配置,在控制器中零拼接返回数据;结合示例项目与多层级csproj工程(含Core、Models、Mvc模块),能深入理解前后端解耦设计与大数据服务端预处理实践。
1. C# For ECharts 模型层建模库:不是“把图表塞进网页”,而是让后端真正拥有图表语义的表达能力
你有没有遇到过这种场景:前端用 ECharts 画了个带时间轴、多图联动、自定义 tooltip 的销售看板,后端却只能硬编码拼 JSON 字符串——改个坐标轴类型要前后端对齐三轮,加个数据过滤逻辑得在 Controller 里写一堆if-else套JObject,更别说动态主题切换、服务端渲染图表快照、或把「某指标同比环比对比图」抽象成可复用的业务组件。这个 C# For ECharts 模型层建模库,就是为解决这类血泪经验而生:它不封装 JS 调用,也不做 MVC View 层胶水,而是用强类型的 C# 类完整映射 ECharts 5.x 官方 JSON Schema,把 option 配置从「字符串黑匣子」变成可继承、可验证、可序列化、可单元测试的领域模型。适合正在用 ASP.NET Core MVC/Web API 做 BI 系统、数据中台或管理后台的团队,尤其当你开始被「配置即代码」和「图表即资源」这两个需求反复按在地上摩擦时——它不是让你少写 JS,而是让你的 C# 代码第一次能理直气壮地说:“这张图,我懂它。”
2. 架构设计与核心模块拆解:从 ECharts Option 到 C# 类的精准映射
这个库的本质,是把 ECharts 官方文档里那个庞大、嵌套深、字段多变的 JSON Schema,用 C# 的类继承体系 + 特性驱动(Attributes)+ 泛型约束,做了分层建模。它不追求 100% 覆盖所有冷门配置项(比如series.map.select.disabled这种),但覆盖了 95% 以上生产环境高频使用的字段,并保证每个类都可通过JsonSerializer直接序列化为合法 ECharts option。整个结构清晰分三层:基础类型层(如EChartColor,EChartSize)、通用配置层(TitleComponent,TooltipComponent,GridComponent)、系列层(LineSeries,BarSeries,PieSeries,CustomSeries)。最关键的是,它用EChartsOption<TSeries>这个泛型根类统一收口,让不同图表类型共享 title、tooltip、legend 等公共配置,同时允许 series 层自由组合。
2.1 核心命名空间与类组织逻辑
项目采用严格分层命名空间,避免类型污染:
namespace ECharts.Models.Option { // 根选项:聚合所有组件 public class EChartsOption<TSeries> where TSeries : BaseSeries { public TitleComponent? Title { get; set; } public TooltipComponent? Tooltip { get; set; } public LegendComponent? Legend { get; set; } public GridComponent? Grid { get; set; } public List<TSeries> Series { get; set; } = new(); // ... 其他 top-level 配置 } // 系列基类:定义所有 series 共享字段(name, type, data 等) public abstract class BaseSeries { public string? Name { get; set; } public string Type { get; set; } = "line"; // 默认值 public object? Data { get; set; } // 支持 List<object>, List<List<object>>, 或 JObject public bool? Show { get; set; } } // 具体系列:继承并扩展字段 public class LineSeries : BaseSeries { public LineSeries() => Type = "line"; public bool? Smooth { get; set; } public SymbolType? Symbol { get; set; } // 枚举,非 string public LineStyle? LineStyle { get; set; } } }提示:
Data字段声明为object?是刻意为之——实际使用中,它可能是List<double>(折线图数值)、List<(string, double)>(柱状图类别+值)、List<Dictionary<string, object>>(散点图带额外维度),甚至JArray(从数据库直接读取的原始 JSON)。库不强制转换,而是交由开发者在构造时决定数据形态,序列化器会自动处理。
2.2 特性驱动的序列化控制:让 C# 字段名匹配 ECharts JSON Key
ECharts 的 JSON 字段名大量使用小驼峰(tooltip.trigger)或短横线(areaStyle.opacity),而 C# 命名规范是 PascalCase。库通过JsonPropertyNameAttribute统一处理,且做了语义化别名:
namespace ECharts.Models.Components { public class TooltipComponent { [JsonPropertyName("trigger")] public TriggerType Trigger { get; set; } = TriggerType.Item; [JsonPropertyName("formatter")] public string? Formatter { get; set; } // 支持字符串模板,如 "{a} <br/>{b} : {c}" [JsonPropertyName("axisPointer")] public AxisPointer? AxisPointer { get; set; } } public enum TriggerType { [JsonPropertyName("item")] Item, [JsonPropertyName("axis")] Axis, [JsonPropertyName("none")] None } }这样做的好处是:你在 C# 里写tooltip.Trigger = TriggerType.Axis,序列化后自动变成"trigger": "axis",完全符合 ECharts 规范;同时 IDE 能提供完整的智能提示和编译检查,而不是靠字符串拼错导致运行时白屏。
2.3 泛型系列组合:支持混合图表与动态构建
真正的业务图表往往不是单一类型。比如一个「销售额趋势 + 同比完成率进度条」的组合图,需要LineSeries和GaugeSeries共存。库通过泛型约束EChartsOption<TSeries>的TSeries支持协变(covariance),配合BaseSeries的抽象基类,实现安全的多类型集合:
// 构建混合图表:折线 + 仪表盘 var option = new EChartsOption<BaseSeries> { Title = new TitleComponent { Text = "Q3 销售达成看板" }, Tooltip = new TooltipComponent { Trigger = TriggerType.Item }, Series = new List<BaseSeries> { new LineSeries { Name = "销售额(万元)", Data = new List<double> { 120, 180, 150, 210, 190, 240 } }, new GaugeSeries { Name = "完成率", Data = new List<GaugeData> { new GaugeData { Value = 78.5, Name = "达成率" } } } } }; // 序列化为 JSON 发送给前端 var json = JsonSerializer.Serialize(option, new JsonSerializerOptions { WriteIndented = true });注意这里Series是List<BaseSeries>,而非List<object>—— 编译期就能确保所有添加的 series 都继承自BaseSeries,避免运行时类型错误。这是纯字符串拼接方案永远做不到的安全边界。
3. 快速上手:在 ASP.NET Core MVC 中集成并渲染第一个图表
集成过程极简,无需修改前端任何 JS 加载逻辑,只需把生成的 JSON 传给 ECharts 初始化函数即可。重点在于如何在 Controller 中构造 option 并传递到 View。
3.1 NuGet 安装与项目引用
该项目已发布为 NuGet 包(假设包名为ECharts.CSharp.Models),安装命令如下:
dotnet add package ECharts.CSharp.Models --version 1.2.0注意:版本号
1.2.0是根据当前 ECharts 5.4.3 官方 schema 生成的,若你使用 ECharts 4.x,请降级到1.0.x系列(兼容性说明见包内 README.md)。
安装后,在Startup.cs或Program.cs中无需额外注册服务——它纯模型层,无依赖注入要求。
3.2 Controller 中构造图表数据
以一个典型的销售趋势图为例,Controller 返回强类型 ViewModel:
// Controllers/ReportController.cs public class ReportController : Controller { public IActionResult SalesTrend() { // 模拟从数据库获取的数据 var salesData = GetMonthlySales(); // 返回 List<(DateTime Month, decimal Amount)> // 构建 ECharts Option var option = new EChartsOption<LineSeries> { Title = new TitleComponent { Text = "月度销售额趋势", Subtext = "单位:万元" }, Tooltip = new TooltipComponent { Trigger = TriggerType.Axis, Formatter = "{b0} : {c0} 万元" }, Legend = new LegendComponent { Data = new[] { "销售额" } }, Grid = new GridComponent { Left = "3%", Right = "4%", Bottom = "3%", ContainLabel = true }, XAxis = new List<AxisComponent> { new CategoryAxisComponent { Type = "category", Data = salesData.Select(x => x.Month.ToString("yyyy-MM")).ToArray() } }, YAxis = new List<AxisComponent> { new ValueAxisComponent { Type = "value", Name = "金额(万元)" } }, Series = new List<LineSeries> { new LineSeries { Name = "销售额", Data = salesData.Select(x => (object)x.Amount).ToList(), Smooth = true, Symbol = SymbolType.Circle, LineStyle = new LineStyle { Width = 3 } } } }; return View(new ChartViewModel { OptionJson = JsonSerializer.Serialize(option) }); } private List<(DateTime, decimal)> GetMonthlySales() { // 实际项目中此处调用仓储层 return new() { (new DateTime(2024, 1, 1), 120.5m), (new DateTime(2024, 2, 1), 135.2m), (new DateTime(2024, 3, 1), 142.8m), (new DateTime(2024, 4, 1), 168.3m), (new DateTime(2024, 5, 1), 182.1m), (new DateTime(2024, 6, 1), 195.7m) }; } }关键点说明:
XAxis使用CategoryAxisComponent,其Data是字符串数组,对应月份标签;Series.Data是List<object>,我们传入decimal的 boxed 值,JsonSerializer会自动转为数字;- 所有配置字段均通过属性赋值,IDE 全程提示,零字符串魔法。
3.3 View 中初始化 ECharts 实例
View 层(.cshtml)仅需标准 ECharts 初始化流程,option 来自 Model:
<!-- Views/Report/SalesTrend.cshtml --> @model ChartViewModel <div id="chart-sales-trend" style="width: 100%; height: 500px;"></div> @section Scripts { <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> <script> const chartDom = document.getElementById('chart-sales-trend'); const myChart = echarts.init(chartDom); // 直接解析后端传来的 JSON 字符串 const option = @Html.Raw(Model.OptionJson); myChart.setOption(option); // 响应式适配 window.addEventListener('resize', () => { myChart.resize(); }); </script> }注意:
@Html.Raw(Model.OptionJson)是关键——必须用Raw,否则引号会被 HTML 编码,导致 JSON 解析失败。这是新手最容易翻车的第一步。
4. 常见问题排查:那些让你调试半小时才发现的“小坑”
集成过程中,以下问题出现频率极高,几乎每个新接入的团队都会踩一遍。这里按「现象 → 原因 → 解决」列出真实复现过的 4 个典型问题:
4.1 现象:图表空白,控制台报Uncaught TypeError: Cannot read properties of undefined (reading 'setOption')
原因:echarts.init()执行时,DOM 元素尚未加载完成,或chartDom为 null。常见于未加window.onload或未用document.ready包裹初始化逻辑。
解决:确保 script 块放在 body 底部,或显式等待 DOM 加载:
<script> document.addEventListener('DOMContentLoaded', function () { const chartDom = document.getElementById('chart-sales-trend'); if (!chartDom) return; const myChart = echarts.init(chartDom); const option = @Html.Raw(Model.OptionJson); myChart.setOption(option); }); </script>4.2 现象:X 轴显示为[object Object]或乱码,tooltip 里b0c0不替换
原因:Tooltip.Formatter设置为字符串模板(如"{b0} : {c0}"),但XAxis.Data传入的是DateTime对象而非字符串。ECharts 无法自动格式化复杂对象。
解决:务必确保XAxis.Data是字符串数组。上面 Controller 示例中已用.ToString("yyyy-MM")显式转换,切勿直接传DateTime实例。
4.3 现象:Series.Data渲染为空,控制台无报错,但图表没数据点
原因:Data字段类型不匹配。例如LineSeries.Data期望List<double>,但你传了List<int>或JArray,而JsonSerializer默认不进行数值类型隐式转换(尤其int→double)。
解决:统一用double或object。推荐做法是显式 cast:
Data = salesData.Select(x => (object)(double)x.Amount).ToList()或更稳妥地,用Convert.ToDouble()处理可能的 null:
Data = salesData.Select(x => (object)(x.Amount == null ? 0d : Convert.ToDouble(x.Amount))).ToList()4.4 现象:启用Animation: false后图表仍闪动,或Label.Show = true但文字不显示
原因:ECharts 的某些配置项(如animation,label.show)在Series层设置无效,必须在Series.ItemStyle或Series.Label子对象中设置。
解决:查阅 ECharts 官方文档确认配置项层级。例如 label 显示需:
new LineSeries { Label = new Label { Show = true, Position = "top" } }而非LabelShow = true(该字段不存在)。库中所有字段均严格对齐官方 schema,不存在“简写别名”。
5. 高级用法:自定义系列、主题注入与服务端图表快照生成
当基础图表满足不了业务需求时,这个库的设计优势才真正显现——它允许你深度介入 ECharts 的任意扩展点,而无需 fork 整个 JS 库。
5.1 扩展自定义系列(CustomSeries):对接 ECharts GL 或第三方渲染器
ECharts 支持custom类型系列,用于绘制任意图形(如热力图、3D 地图、粒子效果)。库提供了CustomSeries类,其RenderItem字段支持传入 JS 函数字符串(服务端渲染时忽略,前端执行):
var customOption = new EChartsOption<CustomSeries> { Series = new List<CustomSeries> { new CustomSeries { Name = "粒子轨迹", CoordinateSystem = "cartesian2d", RenderItem = @" function (params, api) { const x = api.value(0); const y = api.value(1); return { type: 'circle', shape: { cx: api.coord([x, 0])[0], cy: api.coord([0, y])[1], r: 3 }, style: { fill: '#ff6700' } }; }" } } };注意:
RenderItem是纯字符串,不做服务端解析。它会在前端 ECharts 实例中作为函数字符串eval执行(ECharts 官方机制)。因此,你可以在 Controller 中拼接业务参数进去,实现动态逻辑。
5.2 主题注入:用 C# 控制全局视觉风格
ECharts 支持主题(theme)JSON,但通常需前端加载额外 JS 文件。本库支持在 C# 中直接构造主题对象,并合并进 option:
var theme = new Theme { Color = new[] { "#5470c6", "#91cc75", "#fac858", "#ee6666" }, TextStyle = new TextStyle { FontSize = 14 }, ToolTip = new ToolTip { BackgroundColor = "rgba(0,0,0,0.7)" } }; // 合并到 option(需手动赋值,库不自动注入) option.Color = theme.Color; option.TextStyle = theme.TextStyle; option.Tooltip!.BackgroundColor = theme.ToolTip.BackgroundColor;这样,你就可以把主题配置放入appsettings.json,由 Configuration Bind 自动加载,实现一套代码多套皮肤。
5.3 服务端图表快照:用 PuppeteerSharp 生成 PNG/SVG
虽然库本身不包含渲染能力,但它生成的标准 JSON 可直接喂给服务端渲染引擎。我们实测用PuppeteerSharp在 .NET 6+ 中生成 PNG:
// Services/ChartSnapshotService.cs public async Task<byte[]> GeneratePngAsync(string optionJson, int width = 800, int height = 400) { await new BrowserFetcher().DownloadAsync(BrowserFetcher.DefaultChromiumRevision); var browser = await Puppeteer.LaunchAsync(new LaunchOptions { Headless = true }); var page = await browser.NewPageAsync(); await page.SetViewportAsync(new ViewPortOptions { Width = width, Height = height }); await page.GoToAsync("data:text/html," + "<!DOCTYPE html><div id='chart' style='width:100%;height:100%'></div>" + "<script src='https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js'></script>" + "<script>" + "const chart = echarts.init(document.getElementById('chart'));" + $"chart.setOption({optionJson});" + "setTimeout(() => chart.resize(), 100);" + "</script>"); var screenshot = await page.ScreenshotDataAsync(); await browser.CloseAsync(); return screenshot; }调用方式:
var pngBytes = await _snapshotService.GeneratePngAsync(optionJson); return File(pngBytes, "image/png", "sales-trend.png");这使得导出报表、邮件自动推送图表、AI 数据分析 pipeline 等场景变得极其轻量。
6. 二次开发指南:如何为你的业务定制专属图表组件
这个库最强大的地方,不在于它实现了多少 ECharts 配置,而在于它为你铺好了「C# 图表 DSL」的基础设施。我参与过的三个项目,都基于它衍生出了自己的业务图表组件库,以下是可直接复用的改造路径。
6.1 封装业务语义组件:从「配置」到「意图」
不要停留在LineSeries这一层。你应该定义更高阶的组件,比如SalesTrendChart、UserRetentionChart,它们内部封装了数据预处理、默认样式、交互逻辑:
public class SalesTrendChart : EChartsOption<LineSeries> { public SalesTrendChart(IEnumerable<SalesRecord> records, string title = "销售趋势") { Title = new TitleComponent { Text = title }; Tooltip = new TooltipComponent { Trigger = TriggerType.Axis, Formatter = "{b} : ¥{c} 万元" }; Legend = new LegendComponent { Data = new[] { "销售额", "目标额" } }; XAxis = new List<AxisComponent> { new CategoryAxisComponent { Data = records.Select(r => r.Date.ToString("MM-dd")).ToArray() } }; YAxis = new List<AxisComponent> { new ValueAxisComponent() }; // 自动计算目标额(假设每月目标为上月*1.05) var targetData = records .Select((r, i) => i == 0 ? 0m : records[i - 1].Amount * 1.05m) .Select(x => (object)(double)x).ToList(); Series = new List<LineSeries> { new LineSeries { Name = "销售额", Data = records.Select(r => (object)(double)r.Amount).ToList() }, new LineSeries { Name = "目标额", Data = targetData, Type = "line", LineStyle = new LineStyle { Type = "dashed" } } }; } } // 使用时一行搞定 var chart = new SalesTrendChart(salesRecords); ViewBag.ChartJson = JsonSerializer.Serialize(chart);这样,业务同学只需关注「我要什么图」,而不是「ECharts 的yAxis.splitLine.show怎么关」。
6.2 集成数据权限:在模型层注入租户/角色过滤逻辑
很多 BI 系统需按部门、区域、角色动态过滤数据。与其在 Controller 里写if (user.Role == "Admin") ...,不如把权限逻辑下沉到图表模型:
public abstract class SecuredChart<TSeries> : EChartsOption<TSeries> where TSeries : BaseSeries { protected readonly IHttpContextAccessor _httpContextAccessor; protected SecuredChart(IHttpContextAccessor accessor) { _httpContextAccessor = accessor; } protected virtual void ApplyDataFilter(List<object> rawData) { } } public class RegionalSalesChart : SecuredChart<BarSeries> { public RegionalSalesChart(IHttpContextAccessor accessor, IEnumerable<SalesByRegion> data) : base(accessor) { // 根据当前用户 region claim 过滤 data var userRegion = _httpContextAccessor.HttpContext?.User.FindFirst("region")?.Value; var filtered = userRegion == null ? data : data.Where(x => x.Region == userRegion); Series = new List<BarSeries> { new BarSeries { Data = filtered.Select(x => new object[] { x.Region, x.Amount }).ToList() } }; } }配合 DI 注入IHttpContextAccessor,权限逻辑与图表配置彻底耦合,测试也更容易——Mock 一个 HttpContext 即可。
6.3 生成 TypeScript 类型定义:打通前后端类型契约
前端 TypeScript 项目常需echarts.EChartsOption类型,但手工维护易过期。你可以用TypeGen或自定义 T4 模板,将 C# 模型一键生成 TS 接口:
// 伪代码:在构建时运行 var csharpTypes = typeof(EChartsOption<>).Assembly.GetTypes() .Where(t => t.Namespace?.Contains("ECharts.Models") == true); GenerateTypeScriptInterfaces(csharpTypes, "src/@types/echarts-csharp.d.ts");生成结果类似:
export interface EChartsOption<TSeries> { title?: TitleComponent; tooltip?: TooltipComponent; series: TSeries[]; } export interface LineSeries extends BaseSeries { smooth?: boolean; symbol?: 'circle' | 'rect' | 'roundRect'; }从此前后端 option 结构完全一致,VS Code 和 WebStorm 都能跨端跳转、实时校验。
从那以后我每次新建图表需求,都强制走一遍「先定义 C# 组件类 → 再写 Controller Action → 最后补前端初始化」的三步流程。哪怕只是临时加个调试用的饼图,也先建个DebugPieChart类——因为你知道,三个月后那个“临时”图表,大概率会变成核心看板的一部分。希望帮到你。
本文还有配套的精品资源,点击获取