我在第一次上手 ECharts 的时候,最大的感受是:这个库把“写图表”这件事变得太像“拼配置”了。你不用手动去画 SVG、算坐标、处理刻度,只要按照它定义的规则把数据和样式丢进一个对象里,图表就能自己跑起来,还自带动画和交互。今天这篇就来用最直白的方式把 ECharts 的基础逻辑和折线图讲透,从零开始讲,手把手带你写一个能直接用于项目里的折线图,再看清楚背后的设计思路。这样才能保证自己写的配置不是网上抄的,而是真能跟着需求灵活改。
如果你已经看了一些官方文档但觉得碎片化,或是在公司项目里需要用折线图展示趋势但不知道怎么结构化管理配置,这篇应该正合适。我会把“ECharts 到底是什么、核心概念有哪些、折线图从创建到定制的完整路径、以及我在实际项目里踩过的坑”一次讲完,让你看完后不仅会写一个折线图,还能看懂官方示例里那些复杂配置是怎么组合出来的。
1. 先把 ECharts 的设计思路摸清楚
ECharts 是一个基于 JavaScript 的可视化图表库,底层默认用 Canvas 渲染。很多人一开始会把关注点放在“画图”本身,但其实 ECharts 的核心设计理念是:你用数据驱动图形,而不是用绘图 API 手动操作图形。这种模式和直接用 Canvas、SVG 写图表最本质的区别在于——你不再关心“线怎么画”“刻度怎么摆”,而是告诉它“我的数据是什么、我想展示成什么形式”,剩下交给渲染引擎。
从架构角度来看,ECharts 有三个关键词必须理解:
- option(配置项):一切图表的源头。你传入一个普通的 JavaScript 对象,里面描述标题、图例、坐标轴、数据系列等所有信息。
- series(系列):每个图表里一个系列代表一类数据。折线图的一个系列就是一条线,柱状图一个系列就是一组柱子,饼图一个系列就是一张饼。
- 渲染器与实例:通过
echarts.init()创建一个实例绑定在某个 DOM 容器上,再调用setOption()把配置灌进去,图表就出现在页面上。
理解 option 是理解 ECharts 的关键钥匙。官方文档里的大量 API 说明,本质上都是在解释 option 的各个字段。而 option 又是一个嵌套很深的树形结构,你在一开始的时候不需要背下来,但你要建立一个“查字典”的意识:遇到想实现的功能,第一个反应应该是去官网搜对应的配置字段。
在项目里我通常会把 option 拆成几层来维护,比如基础的容器配置一层、数据处理的结果一层、业务样式的覆盖层。这么做的好处是,当 UI 设计师调整了视觉细节,你只需要改动样式层,不需要动数据组装逻辑。等你看完基础概念后,我会在实操部分给出一个清晰的分层写法,现在先不跳那么远。
另外一个容易忽略的点是:ECharts 的渲染底层在 5.0 之后对 SVG 渲染也提供了良好支持。Canvas 适合数据量大、交互频繁的图表,SVG 适合小规模、需要更好清晰度和可访问性的场景。开发前先预估一下数据量级,再决定用哪种渲染器,这个决策虽然简单,但对后续性能有很大影响。
2. 环境准备与第一次跑通折线图
2.1 引入 ECharts 的两种常见方式
在实际开发中,引入 ECharts 一般按项目形态分两条路:
方式一:直接用 HTML 标签引入。简单项目或只想快速试验时,用script标签从 CDN 引入,比如本地开发时引入一个版本的echarts.min.js文件。你现在看到的官方示例大多也是这种方式,好处是所见即所得,文件一刷新就能跑。
方式二:模块化引入。在 Webpack/Vite 这类工程化项目里,通过 npm 安装依赖,然后在代码里import * as echarts from 'echarts'。这种方式更适合大型项目,可以配合按需引入做打包体积优化。
如果你是在用 Vue 或者 React,社区里也有对应的封装组件,但我个人建议先别急着上封装,把原生 ECharts 的语法搞明白了,后面看任何封装都觉得通透。
用 npm 安装的命令很简单:
npm install echarts --save然后在一个模块文件中引入核心模块并使用:
import * as echarts from 'echarts'; const chartDom = document.getElementById('main'); const myChart = echarts.init(chartDom);2.2 最小可运行的完整代码
我们先写一个最小化的折线图。这一段代码是后面所有折腾的基础,建议你手敲一遍,别复制。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>ECharts 折线图基础示例</title> <style> #chart { width: 800px; height: 400px; } </style> </head> <body> <div id="chart"></div> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> <script> // 1. 获取 DOM 容器 const chartDom = document.getElementById('chart'); // 2. 初始化 ECharts 实例 const myChart = echarts.init(chartDom); // 3. 定义配置项 const option = { xAxis: { type: 'category', data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] }, yAxis: { type: 'value' }, series: [ { name: '访问量', type: 'line', data: [820, 932, 901, 934, 1290, 1330, 1320] } ] }; // 4. 渲染图表 myChart.setOption(option); </script> </body> </html>这段代码里最核心的只有三件事:
第一,echarts.init接收一个 DOM 节点,这个节点必须有宽高。如果你忘记给容器设置宽度,那你大概率看到的是一个 0 像素高的空白区域,这是新手最容易踩的第一个坑。
第二,xAxis 的type设置成category代表 x 轴是类别型,下面用data枚举出 7 个类目。yAxis 设置成value代表 y 轴是数值型,由 ECharts 自动计算区间。
第三,series 数组里定义了一个对象,type为line表示折线图,name是这条线对应的系列名,data则是与 xAxis 数据一一对应的数据数组。
当你把这串代码在浏览器里跑起来,页面应该出现一条从左到右升高的折线,默认是蓝色,带灰色边框,底部有 7 个星期类目,鼠标悬停会显示出数据提示框。第一张图跑通了,接下来就可以往上叠加细节。
2.3 基于常见实践的模块化封装思路
在项目里我通常不会直接裸setOption一个上千行的配置,而是拆成几个文件。这个做法在训给新人时我会特别推荐,因为它并不复杂,但能把“数据组装”和“样式配置”彻底分开,后续维护舒服很多。
简单说来就是三层:
baseOption:图表的通用结构,比如坐标轴类型、图例位置、tooltip 触发器,属于项目 UI 基线。formatData:把后端接口返回的数据转换成 ECharts 可消费的数据格式。mergeOption:用setOption合并配置,实现局部更新和动态交互。
举个例子:接口返回的是数组对象,其中字段名是date和value,但图表需要的是两个独立的数组xData和yData,格式转换这一层就应该单独写一个函数去处理。这样当后端改了字段名,你只需要动一个函数,而不是去图表配置里找。
function formatLineChartData(rawData) { return { xData: rawData.map(item => item.date), yData: rawData.map(item => item.value) }; }这个封装思路不是 ECharts 官方强制要求的,也不是某种框架的最佳实践,而是我实际项目里验证过的、适合中小型前端团队的方式。你的项目复杂度上去了自然需要分层;如果只是单个小页面,那就把配置写得直白一点,别过度设计。
3. 折线图的核心细节与定制玩法
3.1 xAxis 与 yAxis 的参数选择逻辑
写折线图时,坐标轴配置是最绕不开的。xAxis 和 yAxis 都有type字段,常见值为category、value和time。很多人一开始搞不清这三者的区别,导致数据画出来是乱的。
category:适合离散类目,比如星期、月份、地区名称。每个类目之间没有严格的数值间距,纯粹是等距排列。value:适合连续数值,它需要 ECharts 根据数据的最大最小值自动计算刻度范围,比如销售额从 0 到 10000 的连续变化。time:适合时间序列。它的特殊之处在于 ECharts 会智能识别时间粒度,你给它一组时间字符串,它会自动调节轴标签的显示密度,不用手动做格式化。
在折线图里最常见的是“时间序列折线图”和“类目比较折线图”。如果你的数据是一个月内每天的变化,那 x 轴用time是最好用的,因为 ECharts 能自动处理日期的间距:一个月的 31 天让它自动决定横轴显示多少刻度,不会挤成一团。
xAxis: { type: 'time', axisLabel: { formatter: '{MM}-{dd}' } }这段配置的意思是把 x 轴的标签格式化成月-日的显示形式。底层 ECharts 会根据数据的时间跨度智能增加或减少 tick,不需要你手动控制数量。
对于 yAxis,大部分时候用value就够。但注意scale字段,默认值是 false,意思是 y 轴从 0 开始;如果你设置成true,那么 y 轴会从数据的最小值附近开始,更适合展示波动幅度大的数据。比如股票价格在 100 到 102 之间波动,默认从 0 开始就会让折线看起来像一条直线,毫无信息量。这时候把scale设为true就会好很多。
3.2 series 的核心配置项逐项拆解
series 是折线图的核心,我挑几个高频配置详细说说。
type指定为'line',这个不用多解释。data是数据数组,既可以是一维数组,比如[820, 932, 901],也可以是二维数组,比如[[ '周一', 820], ['周二', 932]],二维数组模式下每一子项会同时包含类目轴和数值轴的信息。
smooth控制折线是否平滑。设为true时会用贝塞尔曲线把数据点连接起来,视觉效果更圆润。很多人以为平滑曲线和真实数据之间有偏差,所以对展示数据精确性有要求的场景建议保持默认值的折线,甚至用smooth: false来保证数据表达的严谨性。
symbol控制数据点的标记形状。默认是空心圆圈,你可以改成circle、rect、triangle等。如果数据点比较多,想显得干净一点,可以设置成'none',只显示一条线不显示点。
lineStyle控制线条样式,比如颜色、宽度、虚线样式:
lineStyle: { color: '#409EFF', width: 2, type: 'dashed' }areaStyle给折线下方区域填充渐变颜色,这在看趋势对比的图中很常用。
areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: 'rgba(64, 158, 255, 0.4)' }, { offset: 1, color: 'rgba(64, 158, 255, 0)' } ] } }这个配置生成一个从顶部向下渐变的半透明区域,视觉上给折线加了一层“尾巴”,非常适合展示占比或流量的趋势变化。
stack是折线图的另一个重要概念,比如要展示三个渠道各自的访问量以及总量时,如果在 series 里把三个系列的stack设为同一个值,它们就会叠加显示在一起,表现为“堆叠面积图”,每条线表示在其前一条基础上累积的结果。
3.3 tooltip、legend、dataZoom 的实际作用
光有一条折线,信息表达是不够的,你还需要三大辅助组件:tooltip(提示框)、legend(图例)、dataZoom(数据缩放)。
tooltip是鼠标悬浮时显示的浮层。默认情况下它会显示当前数据点和值,但你可以配置触发方式和自定义格式:
tooltip: { trigger: 'axis', formatter: function (params) { let html = `${params[0].name}<br/>`; params.forEach(item => { html += `${item.seriesName}: ${item.value}<br/>`; }); return html; } }trigger有两个选项:'item'表示只在悬停到数据点时才触发提示框;'axis'表示鼠标在坐标轴区域内移动时以坐标为基准触发所有系列在该点的数据。多系列折线图通常用'axis',体验更像“阅读一整条趋势线”。
legend是顶部的系列图例组件。当图例被取消选中时,对应系列会隐藏,这能让用户在图表上动态切换要看哪条线。你只需要在 option 里声明一层legend: { data: ['访问量', '下载量'] },ECharts 会根据每个 series 的name自动生成可点击交互的图例。如果不声明 legend.data,ECharts 也会自动取所有 series 的 name 来生成。
dataZoom是我在真实项目里最常加的一个组件,尤其是数据跨度大或数据点数量多的时候。它提供了滑块缩放区域,用户拖动范围就能让局部细节放满整个视图。
dataZoom: [ { type: 'inside', start: 0, end: 50 }, { type: 'slider', start: 0, end: 50 } ]当同时配置inside和slider两种时,inside允许鼠标滚轮直接在图表上缩放,slider在底部提供一个可拖动的滑块条。两者都设了的话,用户体验接近主流带缩放功能的图表产品,如股票行情软件。
4. 完整实操:做一个带交互的多系列折线图
光讲配置项太散,我现在从零带你把一个稍微完整点的折线图做出来,包含多系列、渐变面积、图例切换、缩放和自定义提示框。这套图我在实际项目中给运营做过“七日渠道访问趋势看板”,整体结构可以直接平移到你的业务场景里。
先准备好数据。假设我有三个渠道,分别是:自然搜索、直接访问、外部引流,每天各有访问量。数据结构如下:
const rawData = [ { date: '2025-02-24', organic: 820, direct: 320, referral: 150 }, { date: '2025-02-25', organic: 932, direct: 365, referral: 198 }, { date: '2025-02-26', organic: 901, direct: 380, referral: 187 }, { date: '2025-02-27', organic: 934, direct: 421, referral: 210 }, { date: '2025-02-28', organic: 1290, direct: 468, referral: 240 }, { date: '2025-03-01', organic: 1330, direct: 443, referral: 223 }, { date: '2025-03-02', organic: 1320, direct: 492, referral: 258 } ];接着我们把数据格式化成图表需要的 xData 和 yData:
const xData = rawData.map(item => item.date); const yDataMap = { organic: rawData.map(item => item.organic), direct: rawData.map(item => item.direct), referral: rawData.map(item => item.referral) };然后组装完整的 option。这个 option 的写法没有使用任何高级 API,但组合效果已经很接近企业级看板了:
const option = { color: ['#409EFF', '#36CFC9', '#F56C6C'], tooltip: { trigger: 'axis' }, legend: { data: ['自然搜索', '直接访问', '外部引流'], top: 10, left: 'center' }, grid: { left: '3%', right: '4%', bottom: '15%', containLabel: true }, xAxis: { type: 'category', boundaryGap: false, data: xData }, yAxis: { type: 'value', scale: true }, dataZoom: [ { type: 'inside', start: 0, end: 100 }, { type: 'slider', start: 0, end: 100 } ], series: [ { name: '自然搜索', type: 'line', smooth: true, symbol: 'circle', symbolSize: 6, data: yDataMap.organic, areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: 'rgba(64, 158, 255, 0.3)' }, { offset: 1, color: 'rgba(64, 158, 255, 0)' } ] } } }, { name: '直接访问', type: 'line', smooth: true, symbol: 'circle', symbolSize: 6, data: yDataMap.direct, areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: 'rgba(54, 207, 201, 0.3)' }, { offset: 1, color: 'rgba(54, 207, 201, 0)' } ] } } }, { name: '外部引流', type: 'line', smooth: true, symbol: 'circle', symbolSize: 6, data: yDataMap.referral, areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: 'rgba(245, 108, 108, 0.3)' }, { offset: 1, color: 'rgba(245, 108, 108, 0)' } ] } } } ] };在这个实例里我做了一个很关键的小决策:xAxis 的boundaryGap设置成false。因为这是时间连续数据,让它从坐标轴最左侧开始描点,更符合“趋势从开始延续到最后”的阅读直觉。而柱状图通常会把boundaryGap留为true,让柱子两端的间隙成立,点到为止不展开。
另外关于grid配置,你可能注意到我没用传统的left: 64这种像素值,而是用了百分比。这样做的好处是图表可以跟随容器宽度自适应,不至于在窄屏下把坐标轴标签挤没。
渲染完成后,还可以动态更新数据。比如某个运营同学在页面上切换了时间范围,后端返回了新的数组,你就直接:
myChart.setOption({ xAxis: { data: newXData }, series: [ { name: '自然搜索', data: newOrganicData }, { name: '直接访问', data: newDirectData }, { name: '外部引流', data: newReferralData } ] });这里的setOption是部分合并更新,不需要把整个 option 重写一遍,也不会重绘整个图表,性能开销很小。图表会基于新数据自动重新渲染并保留设备的交互状态。
这里我再补充一个细节:当你用setOption更新时,如果 series 的 name 不变,ECharts 是按 name 匹配来更新对应系列数据的。如果你不写 name 而只靠顺序,是有可能因为系列数量变化而错乱的。所以,在动态更新场景下,用 name 来标记 series 是值得坚持的习惯。
5. 实际项目里的常见问题与排查心得
5.1 为什么图表不显示或者只显示一半?
最典型的原因一定是容器没有宽高。ECharts 初始化后是直接以 px 为单位的,父容器如果没有高度,init()内部获取的 clientHeight 就是 0,图表自然就没有空间绘制。解决办法是把容器宽高写死,或者通过window.resize重新计算。
另外在有些后台管理系统里,如果你在display:none的标签页中初始化图表,也容易出现空白。因为在隐藏状态下,init()无法正确获取容器尺寸。推荐的做法是等容器可见后再调用init(),或者初始化后调用chart.resize()。
5.2 数据更新但图表不刷新?
这个碰到过的概率也非常高。数据更新后你调用了setOption,但界面纹丝不动。大概率是你在网上搜索的时候看到了“合并策略”之类的说法:如果setOption的 series 没有提供name,且原图表已有 n 个系列、现在提供了 m 个系列,新旧数量不一致时,ECharts 的合并行为可能不符合预期。最简单的排查方式就是在更新时不仅传入 data,也传入对应的 name,或者干脆用myChart.clear()后重新setOption。
对于复杂图中的动态数据,还有一个进阶做法是用replaceMerge。它可以自动以 name 为依据增删系列,这在图表系列有时多有时少、身份还不确定的场景下很有用。这个 API 我建议你在了解了 setOption 的基本行为后再使用,否则很容易踩坑。
5.3 图表容器尺寸变化后图形变形?
在浏览器窗口缩放时,图表不会自动去适应新尺寸,你需要手动监听 resize 事件并调用myChart.resize():
window.addEventListener('resize', () => { myChart.resize(); });如果你的图表藏在多标签页组件里,还需要在标签页被激活时再调一次resize(),否则切换回来会看到一团模糊或截断的图形。
5.4 数据差距太大,折线图看起来像一条直线?
当多条线的数据范围差异很大时,会出现小的那条线一直贴地。最粗暴的办法是把小数据的组单独拆图,但更好的做法是:控制 yAxis 刻度,或者采用双 y 轴。双 y 轴在 ECharts 里实现很简单,给 series 加一个yAxisIndex: 1,然后在 yAxis 数组中定义两个坐标轴对象:
yAxis: [ { type: 'value', name: '访问量' }, { type: 'value', name: '转化率', axisLabel: { formatter: '{value}%' } } ], series: [ { name: '访问量', type: 'line', data: organicData }, { name: '转化率', type: 'line', yAxisIndex: 1, data: rateData } ]这样虽然数据绝对值相差几十倍,但各自有各自的坐标系,就能看出各自的变化规律。
5.5 tooltip 数据格式不对,显示 NAN,或者时间格式化不好看
这个我建议直接用一个通用函数去格式化 tooltip 内容,而不是把所有逻辑都塞进 formatter 里。比如对时间类数据进行new Date(value).toLocaleString()转换,对超大数据做“万”单位的缩写。把工具函数抽出来,不但可以在多个图表间复用,也能让 option 结构保持清爽,结尾再用一段经验收尾。
5.6 构建打包报错,提示 echarts 体积太大
在工程化项目里,ECharts 全量 import 会占很大体积。官方支持按需引入,比如只用折线图,你可以只引入折线图和基础渲染模块。核心思路是使用 ECharts 提供的 tree-shaking 友好 API:
import * as echarts from 'echarts/core'; import { LineChart } from 'echarts/charts'; import { GridComponent, TooltipComponent, LegendComponent, DataZoomComponent } from 'echarts/components'; import { CanvasRenderer } from 'echarts/renderers'; echarts.use([ LineChart, GridComponent, TooltipComponent, LegendComponent, DataZoomComponent, CanvasRenderer ]);这样打包体积能从全量的 1MB 左右降到一个更可接受的范围。不过我要提醒一句:按需引入时,凡是配置里涉及的组件都得在echarts.use()里注册齐全,漏一个图表就不渲染,而且不会在控制台报错很明显,排查起来还挺费时间。新手的话可以考虑先全量引入把功能跑通,最后再做优化。
6. 折线图的进阶方向与方法论沉淀
把上面这一套跑通,你已经能在项目里写折线图了。但我还建议你再往前走一小步:折线图的“表达能力”是可以通过组合配置大幅提升的。
比如你可以在系列数据上增加一个标记点markPoint,用{ type: 'max' }和{ type: 'min' }标注峰值和谷值,这样运营同学一眼就能看到高点出现在哪一天。也可以增加markLine,标注平均值或目标值,用来做 KPI 考核线:
series: [ { name: '目标完成率', type: 'line', data: rateData, markLine: { data: [ { type: 'average', name: '平均值' }, { yAxis: 80, name: '目标线' } ] } } ]再比如你需要在数据点上方直接展示数值时,用label配置:
label: { show: true, position: 'top', formatter: '{c}' }这里的{c}是 ECharts 内置的模板变量,自动替换成当前点的数据值。
做完这些,你会积累下一个属于你自己的“折线图标准模板”,往后遇到任何新需求,第一反应不再是搜文档死磕,而是打开自己的模板库增删字段。
这里我再分享最后一个心得:在学习 ECharts 的过程中,不要把官方示例直接复制了就跑,而是试着去“改变其中一个属性,观察图形变化”。比如把smooth从 true 改成 false,把stack加上再删掉,把boundaryGap从 false 改成 true,每改一次,你心里对这个属性的行为就多一分确认。这个笨办法比读十遍文档都管用,它让你建立了自己的感知地图,而不是只拥有一个收藏夹。
我自己第一次深入折线图时,就是从观察 symbol 变到 label 再到 markLine 一个属性一个属性调过来的,最后把这些属性焊成了一套自己的固定套路。你现在照着这篇文章把示例跑一遍,再按我建议的“改动思维”去瞎折腾几下,差不多也就到手了。这幅图的代码你之后做任何趋势展示类页面,都大概率能直接复用。