简介:面向Web前端与.NET开发者的Ajax+ECharts动态数据可视化示例包,解决异步获取JSON数据并实时渲染ECharts图表的常见需求,适合正在学习ASP.NET数据展示或需要快速搭建实时监控图表的开发者。压缩包共29个文件,核心为aspx页面、cs后台代码及js脚本,另有config配置文件与dll程序集,属于可直接运行的ASP.NET项目示例,配套Web.config等环境配置,便于本地调试。已有1193人学习。资源内含“InitialChartByAjax”和“ClickBtnLoadChartByAjax”两种触发方式,演示了初始化图表、按钮点击后通过Ajax请求服务器、解析JSON并调用setOption更新图表,同时涉及定时刷新、错误处理、数据格式化等实用思路。对于想掌握Ajax异步交互与ECharts整合的开发者,是份简洁实用的参考代码。 搞前端这些年,ECharts 图表的需求我接过不下二十次。最早都是写死静态数据,后来真正上线要对接接口,才发现从 Ajax 动态获取数据到图表渲染,中间每一步都有讲究。最近一个“销售数据看板”项目又让我踩了几个坑,顺手把整条链路梳理一遍:Ajax 怎么传参、编码格式怎么处理、后端 JSON 怎么转成 ECharts 能用的数据结构,再到饼图、柱状图、雷达图、多 Y 轴、3D 效果、legend 全选全不选这些配置怎么落地,最后是几个特别容易让人懵的问题,比如[echarts] can't get dom width or height、移动端图表点不动这类,希望给正在做类似需求的朋友省点时间。
1. 项目整体思路:从写死的数据到动态数据流
1.1 为什么必须用 Ajax 动态获取数据
写 demo 的时候,大家习惯在页面里直接放一个数组,然后扔给 ECharts 的setOption。真到上线就会发现完全不行:数据会变、不同角色看到的范围不同、用户要按时间筛选、点击某个图表要联动刷新另一个图表,这些全都依赖后端动态数据。
Ajax 的核心价值恰恰在这里:不刷新整个页面,也能向服务器请求数据并局部更新视图。放在图表场景里,这个特性几乎就是量身定做的。用户切换日期、切换产品线时,图表重新请求数据的过程不会打断操作体验,哪怕数据量再大,也只是局部 loading,而不是整页白屏。
我手头这个销售看板项目就是典型:页面顶部有日期范围选择器、区域下拉框,中间放了 4 个图表,分别是销售趋势柱状图、品类占比饼图、区域销售雷达图和一个带数字占比的科技感环形图。用户一改筛选条件,4 个图表要同时刷新。这种需求如果用整页刷新,体验会非常差,所以必须走 Ajax 动态获取数据这条路。
1.2 整体数据流设计与选型思路
一套完整的图表数据流大概是这个顺序:
页面模板渲染完成 → 初始化图表(可以先给个空配置或 loading 状态) → 发送 Ajax 请求 → 后端接口查询数据库 → 返回 JSON → 前端解析并转换成 ECharts 配置项 → 调用setOption渲染 → 用户交互触发重新请求。
设计这个链路时有三个点必须提前想清楚,不然后面返工很痛苦。
第一,接口返回的数据结构和 ECharts 配置项结构尽量对齐。这不是说后端必须懂 ECharts,而是在接口设计阶段就约定好字段名。比如后端返回categories表示 X 轴类目,而不是返回data这种模糊名字,前端就能少写很多转换逻辑。要是后端返回的数据没办法直接用,前端一定要做一层适配,不要在图表配置里强行处理复杂的嵌套数据。
第二,加载状态和错误状态不能省。有请求就要有 loading,请求失败要有明确提示。很多新手只处理 success 分支,一旦接口超时或者后端报错,图表就永远停在空白状态,用户根本不知道发生了什么。我会在成功回调里先判断业务状态码,再决定是否渲染图表,请求失败时用 layer 或前端 UI 组件直接弹提示。
第三,想清楚请求时机。是页面加载时自动请求,还是用户点击查询按钮才请求,还是两者都要?这个看板项目的做法是:页面加载时先请求一次默认时间范围的数据,用户点击“查询”时再重新请求。另外,如果用户连续快速点击按钮,前一个请求还没回来,后一个又发出去了,这会产生数据错乱。我的做法是给请求加一个时序标记,或者干脆用abort()把上一次请求取消掉。
2. Ajax 请求:参数、编码与数据解析
2.1 给 ajax 请求参数动态赋值
这个项目的筛选条件有三个:开始日期、结束日期、区域。用户选择后点击查询,就需要把这几个值动态塞进 Ajax 请求里。
最常见的写法是从页面控件读取值,然后放到data字段里:
function loadChartData() { var startDate = $('#startDate').val(); var endDate = $('#endDate').val(); var region = $('#regionSelect').val(); $.ajax({ url: '/api/sales/report', method: 'GET', data: { startDate: startDate, endDate: endDate, region: region }, dataType: 'json', success: function(res) { if (res.code === 0) { renderAllCharts(res.data); } else { showError(res.msg); } }, error: function() { showError('请求失败,请稍后重试'); } }); }这里有个细节容易被忽略:data里的值最好做一层判空。比如用户没选区域,region是空字符串,如果后端比较严格,空字符串会导致 SQL 查询条件异常。前端可以统一处理成region || '',或者干脆在后端接口里做默认值兜底。
如果项目里用的是 layui,那么写法会稍微不同。layui 内置了 jQuery,所以可以通过layui.jquery拿到的$来发请求:
layui.use(['jquery', 'layer'], function() { var $ = layui.jquery; $.get('/api/sales/report', { startDate: startDate, endDate: endDate, region: region }, function(res) { if (res.code === 0) { renderAllCharts(res.data); } else { layui.layer.msg(res.msg); } }, 'json'); });$.get的特点是把参数放在 URL 上,适合 GET 请求、参数量小、不涉及敏感信息的场景。参数多或者需要 POST 提交时,我还是建议用$.ajax配合method: 'POST',因为 POST 的参数放在请求体里,不会因为 URL 过长被浏览器截断。
2.2 请求编码格式与乱码问题
Ajax 的编码问题主要出在中文参数和中文返回内容上。常见现象是:接口请求成功了,但后端拿到的中文是乱码,或者前端拿到的响应里中文变成了问号。
先说请求侧。如果使用$.ajax的默认设置,contentType是application/x-www-form-urlencoded; charset=UTF-8,这时参数会以表单格式提交,中文一般没问题。但如果你改成提交 JSON 数据,就要明确指定编码:
$.ajax({ url: '/api/sales/save', method: 'POST', contentType: 'application/json;charset=UTF-8', data: JSON.stringify({ regionName: '华东区', remark: '季度汇总' }), dataType: 'json' });很多乱码的根源是前端用JSON.stringify把对象转成字符串,但contentType还保持默认的application/x-www-form-urlencoded,服务端拿到的 body 就不是标准表单格式,再用表单解析器去解析,自然就乱码或者解析失败。记住一点:提交什么格式的数据,contentType就要匹配什么格式。
响应侧的编码问题通常要靠服务端解决。比如 Java 接口要设置response.setCharacterEncoding("UTF-8"),Node.js 里要确保Content-Type带charset=utf-8。前端能做的,是在dataType: 'json'的同时,尽量保证页面本身也是 UTF-8 编码,不然浏览器解析响应时可能沿用页面编码,导致中文乱码。
2.3 回调数据处理:把后端 JSON 转成图表数据
后端返回的数据几乎不可能正好是 ECharts 需要的结构。比如这个看板项目,后端接口返回的销售趋势数据长这样:
{ "code": 0, "data": { "months": ["1月", "2月", "3月"], "series": [ { "name": "销售额", "type": "bar", "values": [120, 150, 90] }, { "name": "订单量", "type": "line", "values": [320, 480, 260] } ] } }ECharts 需要的 series 结构里,每个系列要有name、type、data,而这里后端返回的是values字段。我习惯在渲染前做一个专门的数据适配函数,把后端的values重命名成data,同时补上默认的barWidth、itemStyle这些样式字段:
function buildSeries(rawSeries) { return rawSeries.map(function(item) { return { name: item.name, type: item.type || 'bar', data: item.values || [], barMaxWidth: 30, itemStyle: { borderRadius: [4, 4, 0, 0] } }; }); }为什么不直接改后端?因为后端接口往往是复用的,可能同一个接口同时服务移动端列表和 PC 端图表,字段改动会影响其他端。前端做适配层,既隔离风险,又方便针对图表做样式增强,这是更稳妥的做法。
另一个常见情况是后端返回的数据需要汇总。比如饼图需要把 A 部门和 B 部门的销售额汇总成占比,后端很可能返回明细数据,前端就自己做个reduce累加,再拼装成{ value: 总销售额, name: 部门名 }的格式。这类逻辑放在前端,比要求后端为每个图表单独写一个接口要高效得多。
3. ECharts 图表配置:从基础图表到炫酷效果
3.1 基础图表配置:饼图、柱状图、雷达图
这个项目的 4 个图表里,饼图、柱状图、雷达图是最基础的,但配置细节直接影响最终效果。
饼图我用的是环形样式,通过radius控制内外半径,内圈留白可以放中心文字。核心配置如下:
option = { tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' }, legend: { orient: 'vertical', right: 10, top: 'center' }, series: [{ name: '品类占比', type: 'pie', radius: ['45%', '70%'], avoidLabelOverlap: true, itemStyle: { borderRadius: 6, borderColor: '#fff', borderWidth: 2 }, label: { show: false, position: 'center' }, emphasis: { label: { show: true, fontSize: 18, fontWeight: 'bold' } }, data: pieData }] };这里我关了默认 label,只在鼠标悬停时显示中心文字,视觉上更干净。avoidLabelOverlap: true是为了防止扇区标签重叠,数据多的时候特别有用。
柱状图相对简单,但要注意 X 轴类目过多时的显示问题,数据量大时要开启axisLabel的interval设置,或者配合dataZoom组件实现缩放。销售趋势柱状图我做成了圆角柱体,视觉上不会太生硬。
雷达图展示单点信息是这个项目的另一个需求:给某个员工的能力评估打分。雷达图的核心配置是指标和最大值:
option = { radar: { indicator: [ { name: '销售能力', max: 100 }, { name: '技术能力', max: 100 }, { name: '服务意识', max: 100 }, { name: '协作能力', max: 100 } ], radius: '65%', splitArea: { areaStyle: { color: ['rgba(34, 34, 34, 0.3)', 'rgba(34, 34, 34, 0.1)'] } } }, series: [{ type: 'radar', data: [{ name: '当前员工', value: [82, 93, 74, 88], areaStyle: { opacity: 0.35 } }] }] };单点展示时,没有对比对象,所以data数组里只有一项。重点是把雷达图的网格底色和分割线调淡,让数据区域更突出。
3.2 visualmap pieces 与多 Y 轴图表
这个看板项目里有一个“区域销售热度图”,我用了visualMap的pieces模式,把销售数值按区间分成低中高三个档位,分别映射不同颜色。这样做的好处是用户不需要理解连续色阶,直接看颜色就能判断档位。
visualMap: { type: 'piecewise', pieces: [ { min: 0, max: 100, label: '低', color: '#5470c6' }, { min: 100, max: 500, label: '中', color: '#fac858' }, { min: 500, label: '高', color: '#ee6666' } ], left: 20, bottom: 20 }多 Y 轴图表是我觉得 ECharts 里最能体现配置功力的需求之一。比如同一张图里要显示销售额(单位:万元)和增长率(单位:百分比),两者量纲完全不同,必须用双 Y 轴。配置要点是定义两个yAxis,再让不同系列通过yAxisIndex指定使用哪个轴:
option = { xAxis: { type: 'category', data: months }, yAxis: [ { type: 'value', name: '销售额(万元)', position: 'left' }, { type: 'value', name: '增长率(%)', position: 'right', splitLine: { show: false } } ], series: [ { name: '销售额', type: 'bar', yAxisIndex: 0, data: salesData }, { name: '增长率', type: 'line', yAxisIndex: 1, data: growthData } ] };右边的 Y 轴之所以要隐藏splitLine,是因为横向网格线只保留一组就够了,两组会显得特别乱。
3.3 3D 效果与科技感图表实现
热词里好几条都在问 3D 饼图、柱状图 3D 效果、地图立体效果,我挨个说下我的方案。
第一,3D 饼图。ECharts 本身没有原生的 3D 饼图,但有两个方向的方案:一是引入echarts-gl扩展,用pie3D系列实现真正的 3D 效果;二是在不引入额外依赖的情况下,用普通饼图配合roseType、渐变、阴影和透明度的视觉伪装,做出“看着像 3D“的效果。后者更适合对体积敏感的管理系统项目,我实测下来观感差距不大:
series: [{ type: 'pie', roseType: 'radius', radius: ['20%', '70%'], itemStyle: { shadowBlur: 20, shadowColor: 'rgba(0, 0, 0, 0.3)', color: function(params) { // 这里可以用线性渐变形成立体层次 return new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: params.color }, { offset: 1, color: '#111' } ]); } } }]第二,柱状图仿 3D。不需要任何扩展,靠itemStyle的渐变和阴影就能做出立体柱子的视觉效果:
itemStyle: { borderRadius: [6, 6, 0, 0], color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: '#58a1ff' }, { offset: 1, color: '#145fa8' } ]), shadowBlur: 8, shadowColor: 'rgba(20, 95, 168, 0.5)' }如果是真正的地图立体效果,D 系列里设置regionHeight和viewControl可以做地图板块的立体拉伸,适合大屏展示,但配置复杂度会高不少,建议先评估是否需要。
第三,科技感图表。热词里“中心是数字占比,周围散发长短不一的动态线条”这个需求,在大屏项目里特别常见。我的实现思路是组合多个 series:中心放一个环形饼图或graphic文字组件显示占比数字,外层用lines类型的 series 生成多条从中心向外发散的动态线条,配合effect的trailLength和period做出流光效果。
大致配置思路如下:
series: [ { type: 'pie', radius: ['55%', '75%'], label: { show: false }, data: [ { value: percent, name: '占比' }, { value: 100 - percent, name: '剩余', itemStyle: { color: 'rgba(255,255,255,0.1)' } } ] }, { type: 'lines', coordinateSystem: 'polar', data: linesData, effect: { show: true, trailLength: 0.2, period: 4, symbol: 'circle', symbolSize: 3 }, lineStyle: { width: 2, color: '#58a1ff', opacity: 0.6 } } ]实际做的时候,linesData需要用循环生成多个角度不同、长度不同的线条端点,加上点随机抖动,就能模拟出“周围散发长短不一动态线条”的效果。
3.4 交互功能:legend 全选/全不选、hover、框选
看板页面上方我放了“全选”“全不选”两个按钮,用来快速控制图例的显示状态。ECharts 提供了现成的dispatchAction方法:
// 全选所有图例 chart.dispatchAction({ type: 'legendAllSelect' }); // 图例全不选 chart.dispatchAction({ type: 'legendInverseSelect' });这里有个小坑:legendInverseSelect的实际效果是“反选”,如果当前只有部分被选中,执行后选中的会变成未选中,未选中的变成选中,并不是严格意义的“全不选”。要实现真正全不选,可以手动遍历 legend 数据,逐个legendToggleSelect:
function deselectAllLegend(chart, legendData) { legendData.forEach(function(name) { chart.dispatchAction({ type: 'legendToggleSelect', name: name }); }); }调用前要先判断当前图例是不是已经全选了,否则会执行两次反选,又变回原样。
柱状图的 hover 事件是常见的联动场景。比如柱状图 hover 时,旁边的指标卡片同步显示对应数据。监听方式很简单:
chart.on('mouseover', function(params) { if (params.componentType === 'series' && params.seriesType === 'bar') { updateIndicatorCard(params.name, params.value); } });注意params.componentType判断是必写的,否则点击坐标轴、图例等非系列元素时也会触发,导致数据混乱。
框选事件我用的是brush组件。用户可以用鼠标在图表上拖拽框选区域,ECharts 会触发brushSelected事件,我在事件回调里拿到选中的 dataIndex,再联动刷新页面下部的明细表格:
brush: { toolbox: ['rect', 'clear'], xAxisIndex: 0 }, chart.on('brushSelected', function(params) { var selectedIndices = params.batch[0].selected[0].dataIndex; // 用 selectedIndices 去查询明细数据并刷新表格 });brush组件需要引入,如果是按需加载方式,记得把BrushComponent加进去,不然 toolbox 里根本显示不出刷子图标。
4. 踩坑实录:常见问题与排查技巧
4.1 DOM 宽度高度为 0 导致图表不渲染
热词里那条log.js:72 [echarts] can't get dom width or height. please check dom.clientWidth是 ECharts 最经典的报错之一。我第一次遇到是在一个 tab 切换页面里,图表容器放在隐藏的 tab 面板中,页面加载时就初始化图表,此时容器display: none,clientWidth为 0,ECharts 就抛这个错,而且后续切换到该 tab 时图表往往还是空白。
解决办法有几种:
- 最推荐的做法:在容器可见后再初始化图表。可以用监听 tab 切换的回调,切换完成后再调用
init。 - 如果必须在隐藏状态下初始化,可以在
setTimeout里延迟执行,或者等requestAnimationFrame后再初始化。 - 已经出现这个问题后,在容器显示时调用
chart.resize()可以补救,因为resize()会重新读取容器的宽高。
另外,init时容器如果设置了width: 100%但父容器没有明确宽度,也会取不到宽度。我的习惯是给每个图表容器直接设置固定的高度,宽度交给样式表解决,确实需要响应式时再配合resize监听。
4.2 移动端无法点击图表的排查思路
ECharts 在移动端“点不动”,通常不是 ECharts 本身的问题,而是页面环境问题。最常见的三种原因:
第一,有透明遮罩层盖在 canvas 上方。比如某些 UI 组件的弹层、下拉框选项,或者自己写的悬浮层,因为z-index更高,把 canvas 挡住了。点击事件被遮罩层吃掉了。排查时用浏览器开发者工具检查点击位置到底是什么元素。
第二,图表所在区域外层滚动容器的touchmove被拦截。移动端为了实现“滚动穿透”或者自定义滚动逻辑,经常会给父容器加touchmove的preventDefault(),这会让 canvas 上的手势完全失效。我踩过这个坑,最后是在监听函数里判断目标元素是否是 canvas 或 echarts 实例容器,再决定要不要拦截。
第三,canvas 的默认触摸行为干扰。可以给 canvas 加样式:
canvas { touch-action: manipulation; }touch-action: manipulation可以保留缩放和滚动手势,同时禁用双击缩放,减少误触。如果是小程序里的echarts wx-canvas,要检查小程序 canvas 的type配置,老版本项目用 2d canvas 可能会遇到触摸事件不上报的问题,需要做适配。
4.3 echarts require 与按需引入的坑
开发模式里用 CDN 引入完整版 echarts 很方便,所有组件和图表类型都内置了。但到了打包上线阶段,完整版体积太大,通常要做按需引入。
按需引入的写法比较固定:
// echarts/core 提供核心初始化方法 import * as echarts from 'echarts/core'; // 按需引入图表类型 import { BarChart, PieChart, RadarChart } from 'echarts/charts'; // 按需引入组件 import { TooltipComponent, GridComponent, LegendComponent, DatasetComponent, BrushComponent } from 'echarts/components'; // 引入 canvas 渲染器 import { CanvasRenderer } from 'echarts/renderers'; echarts.use([ BarChart, PieChart, RadarChart, TooltipComponent, GridComponent, LegendComponent, DatasetComponent, BrushComponent, CanvasRenderer ]);忘加组件的现象很隐蔽:比如用了Tooltip但没引入TooltipComponent,图表不会报错,只是 tooltip 不显示。用Brush但没引入BrushComponent,brush配置会被忽略。我的排查习惯是:当某个配置项不起作用时,先检查是否引入了对应组件。另外,如果想在按需引入时继续使用echarts.graphic.LinearGradient,需要把GraphicComponent也引进来。
4.4 大屏与多图表场景的性能优化
这种图表看板项目,页面一多、图表一多就容易卡。我的几个优化经验:
- 关闭不必要的动画。初始化加载时有个动画效果确实好看,但用户在图之间切换、筛选时反复请求数据,过度动画会让交互显得迟钝。我一般在重新
setOption时设animation: false,或者用notMerge: true直接替换配置,避免新旧数据结构不一致导致的拼接问题。 - 大数据量的柱状图开启
sampling。对于几千上万条数据的折线图、柱状图,可以设置sampling: 'lttb',有效降低渲染点数,图还是那张图,但性能提升明显。 - 图表被销毁时移除 resize 监听。我在
window.addEventListener('resize', handler)对应的地方,一定会在页面卸载或容器销毁时removeEventListener,不然多页应用里会积累一堆没用的监听,页面越来越卡。 - 大屏分辨率适配用缩放方案。我试过 rem、vw 方案,最后还是回到最直接的方式:根据设计稿比例对图表容器做 scale 缩放,配合
chart.resize()在窗口尺寸变化时重新计算。这样能少处理很多响应式细节。
最后分享一点经验
这个销售看板项目从接口对接到最后样式打磨,前前后后花了两天。最大的体会是:Ajax 动态获取数据 + ECharts 这套组合,真正难的不是某个图表配置,而是把整个数据链路理顺。你先别急着研究 3D、科技感这些炫酷效果,先把一个简单柱状图的“请求 - 转换 - 渲染 - 交互 - 重新请求”闭环跑通,再逐步往里面加视觉和交互能力。踩坑的时候也别慌,像can't get dom width or height这种报错,绝大多数是容器尺寸和初始化时机问题,按上面说的方法排查基本都能解决。如果后面有时间,我会再写一篇关于大屏可视化技术选型的文章,把 echarts-gl、地图立体效果和性能优化单独展开聊聊。
本文还有配套的精品资源,点击获取