
过去做销售看板的时候最怕领导指着折线图问一句为什么这一周数据突然掉这么多 我总不能每次都口头解释因为春节放假了如果图表本身能把这个信息标出来那才是真的省事。后来我就是用 ECharts 的 markLine 给图表加上了节假日标注一条竖线加一个节日名数据波动的为什么直接写在图上连注释都不用额外写。这篇文章就把我实际项目里的完整做法、配置参数、还有踩过的坑一次讲清楚。1. 为什么这个不起眼的 markLine 是节假日标注的最优解1.1 先说说我原来的土办法最早我处理节假日标注用的是最笨的招在图表容器外面叠一层绝对定位的 div算好百分比根据日期在时间轴上的位置放一个小标签。图表一 resize这个 div 的位置就得跟着重新算还得监听窗口变化。更麻烦的是如果图表有 dataZoom、滚动条或者切换了时间范围div 的位置全错位这简直就是给自己挖坑。后来试过用 ECharts 的 graphic 组件直接往画布上画文字位置计算逻辑倒是统一了但 graphic 不跟随坐标系缩放也不跟随 dataZoom 平移数据一滚动节日标签就漂移到完全不对的地方。而且 graphic 的坐标是像素值图表容器一变宽像素坐标就得全重算。折腾一圈下来我判断这个方向也不靠谱。1.2 markLine 正好解决了这些痛点markLine 是 ECharts 里专门做标注线的组件它挂在 series 配置项下面本质上是坐标系里的一个辅助标记。它的位置由轴上的值决定——你告诉它x 轴在 2024-02-10 这一天画一条线它就会严格贴在那个数据点上。坐标系怎么缩放、dataZoom 怎么拖动它都会跟着数据走完全不用手动算像素。相比我前面踩过的 div 叠加、graphic 坐标markLine 有几个很实在的优势不占用新的 series你不需要为了画一条线单独造一个辅助数据系列图表的 legend、tooltip 都不会被干扰位置精准绑定坐标系对时轴、类目轴都支持省去所有手动换算和监听同步自带样式配置颜色、虚线、箭头、标签文字都能调还能加富文本做出来的标注能融入图表整体风格渲染成本低它就是画布上几条线几个字比再叠加一个 series 轻量得多。我用一张表把事情说清楚几种方案在一起对比一下方案是否跟随 dataZoom是否跟随 resize是否需要额外 series开发成本绝对定位 div不跟随需手动同步不跟随需监听重算不需要高极易错位graphic 组件不跟随平移缩放需手动换算不需要中写入像素坐标额外 series 画线跟随跟随需要影响图例中数据造假味重markLine跟随跟随不需要低配置即所得所以我后来的项目里凡是涉及在特定日期处做说明、做分割、做提示的需求第一反应就是 markLine。节假日标注只是它最典型的一个场景类似涨跌停线、活动开始日、版本发布节点、异常时间点处理方式都同源。2. markLine 的关键配置从一条线到一个标注体系2.1 定位一条线的核心写法markLine 的 data 数组是核心。往最简单了说你想在 y 轴数值为 100 的地方画一条横向参考线配置长这样option { series: [{ type: line, data: [/* 折线数据 */], markLine: { silent: true, data: [ { yAxis: 100 } ] } }] };yAxis: 100的意思是这条线在 y 轴值为 100 的位置。同理想在 x 轴的某个类目位置画一条竖线就用xAxis或者xAxis: value来指定。这是 markLine 定位的基础逻辑——你把线上某个轴的值告诉它它就画一条垂直于该轴的线。2.2 标注一条竖线的两种姿势针对节假日标注这个场景我们主要关心竖线怎么画。ECharts 里指定 x 轴位置有两类方式用哪个取决于你的 x 轴类型这个细节特别容易踩坑。第一种类目轴xAxis.type: category。它的轴数据是一个字符串数组比如[2024-01-01, 2024-01-02, ...]。这时候 markLine 的 data 里写精确匹配的字符串markLine: { data: [ { xAxis: 2024-02-10 } ] }前提是2024-02-10这个字符串必须存在于类目轴的数据里否则这条线不会画出来。或者你直接用类目的索引值第 10 个类目就写{ xAxis: 9 }索引从 0 开始。用索引的好处是即使类目字符串格式变化了线也不会丢缺点是你得自己算日期对应的下标。第二种时间轴xAxis.type: time。它的数据是时间戳或者 Date 对象markLine 里就必须给数值型时间戳毫秒markLine: { data: [ { xAxis: new Date(2024-02-10).getTime() } ] }时间轴的好处很明显不用保证日期恰好是轴数据里的某个点线会精确落在时间轴上对应时刻即使该时刻没有数据点线也照样显示。对于节假日标注这种日期值经常不落在数据点上的场景时间轴明显更省心。2.3 一条线段、多条线、还有参考区域markLine 不仅能画单点线还能画两点之间的线段节假日场景里常用它框出一个区间markLine: { data: [ [ { xAxis: 2024-02-10 }, // 春节假期开始 { xAxis: 2024-02-17 } // 春节假期结束 ] ] }这就画出了一条从 2 月 10 日到 2 月 17 日的线段。多见于股价图的涨跌停区间、活动开始结束区间。搭配markArea还能把两块线之间的区域填充阴影色一眼扫过去就知道这段是假期比单条线更直观markArea: { data: [ [ { xAxis: 2024-02-10 }, { xAxis: 2024-02-17 } ] ] }2.4 标注线上最常用的样式配置项markLine 之所以能做出好看的节假日标注在于样式配置很完整。我挑几个最常用的列成表格配置项作用节假日标注的建议值symbol线的端点样式circle或[none, none]不要箭头时用lineStyle.color线的颜色红色#d93026和普通数据线区分lineStyle.type线型dashed虚线表示时间分割点lineStyle.width线宽1或2太粗会喧宾夺主label.formatter线上标签内容显示节日名称如春节label.position标签位置insideEndTop等避免遮挡曲线label.color标签文字颜色跟随线颜色emphasis鼠标悬浮高亮{ disabled: true }可关闭避免干扰注意label.formatter它是一个字符串或回调函数支持模板变量和多段样式。我一般写成春节{c}如果有富文本需求还能写更复杂的结构后面第 5 节细讲。3. 日期转坐标的三种姿势category 轴与 time 轴的差异3.1 为什么日期转坐标会难住不少人说句实话markLine 本身的 API 一点都不复杂复杂的是你的数据在哪种轴模型下。节假日标注的本质是你把一个公历日期比如2024年2月10日翻译成图表坐标系能理解的位置。而位置在这个坐标系里有两种解释——类目序号或者时间戳数值。你选哪一种轴直接决定翻译方式。我在帮朋友改报表的时候见过一类典型报错x 轴是 category 轴轴数据是后端吐的2024/02/10这种字符串他在 markLine 里写了{ xAxis: 2024-02-10 }中间是短横线。格式不一致ECharts 找不到对应类目线就凭空消失了。这类问题在社区里搜一下一大把基本都是这个原因。3.2 姿势一类目轴精确字符串匹配这是最直觉的写法。类目轴数据先是一个数组const dateList [2024-01-01, 2024-01-02, 2024-01-03]; // 以此类推然后直接指定markLine: { data: [{ xAxis: 2024-02-10 }] }注意前后格式必须逐字符一致2024-02-10和2024/02/10不是同一个值。建议在生成类目轴数据时统一固定一种格式并且写一个工具函数把节假日日期批量格式化成同样的格式避免手打出错。这个姿势适合轴数据量小、且每个日期都有数据点的场景。3.3 姿势二类目轴索引定位如果轴数据是等间距的比如每天都有一条那么用索引定位会非常稳// 先找到目标日期在 dateList 中的下标 const idx dateList.indexOf(2024-02-10); markLine: { data: [{ xAxis: idx }] }这里有个实用细节indexOf找出来的是第一个匹配项的下标如果日期列表有重复比如按周统计、按月统计的边界你需要用lastIndexOf或者自己遍历拿最后一个下标否则线可能画在月初而不是月末。这种姿势还适合那种我不想关心轴标签格式的情况下标错不了。不过要注意类目轴的 markLine 如果指定了一个不存在的下标负数或超过数组长度线同样画不出来这个边界问题要在函数里兜住。3.4 姿势三时间轴时间戳定位这是我最推荐在做节假日标注时用的方式特别是当图表做了 dataZoom、数据点不是每天连续的情况。x 轴设成type: time数据用[时间戳, 值]的元组形式xAxis: { type: time }, series: [{ type: line, data: [ [new Date(2024-01-01).getTime(), 320], [new Date(2024-01-02).getTime(), 450] ], markLine: { data: [{ xAxis: new Date(2024-02-10).getTime() }] } }]时间轴的一个坑在时区。new Date(2024-02-10)默认按浏览器本地时区解析如果服务器返回的是带Z结尾的 UTC 字符串ISO 8601解析结果会和本地时间差 8 小时东八区导致线画在前一天晚上也就是坐标上偏移了 8 个小时。对日粒度数据来说8 小时后可能会落到前一天表现出来就像线画偏了一天。稳妥做法是统一用本地时间字符串不带 Z或者用日期库解析指定时区。3.5 三种姿势怎么选我做了个对照表场景推荐姿势理由每天都有数据、类目轴固定精确字符串匹配语义清晰配置直观类目轴有重复项或格式不稳定索引定位不依赖字符串格式下标算准就稳数据不连续、需要 dataZoom、跨时区时间轴时间戳线能落在任意时刻不受数据点限制涉及农历节假日、动态日期时间轴时间戳运行时计算后转时间戳不用改数据结构4. 完整实战给销售折线图加上节假日标注4.1 场景设定与数据准备假设这样一个看板场景公司要做一季度销售复盘折线图展示每一天的销售额横轴是日期纵轴是金额。因为 2 月中旬有春节假期销售数据会出现一个明显的凹坑需要在图上标注春节假期让看的人第一时间理解凹坑的原因。类似的需求还有国庆节、端午节、双十一大促等。先说数据准备。日期用类目轴为了演示方便我只列 2 月初到 2 月底的例子完整一季度同理关键是生成连续的日期和对应的销售额const dateList []; const valueList []; // 生成 2024-02-01 到 2024-03-01 的日期 for (let d new Date(2024-02-01); d new Date(2024-03-01); d.setDate(d.getDate() 1)) { const year d.getFullYear(); const month String(d.getMonth() 1).padStart(2, 0); const day String(d.getDate()).padStart(2, 0); dateList.push(${year}-${month}-${day}); // 模拟销售额周末高、工作日低假期暴跌 const dayOfWeek d.getDay(); let value Math.round(8000 Math.random() * 4000); if (dayOfWeek 0 || dayOfWeek 6) value 1500; if (d new Date(2024-02-10) d new Date(2024-02-17)) value Math.round(600 Math.random() * 300); valueList.push(value); }上面的逻辑很简单周末销售额高一点2 月 10 日到 17 日是春节假期销售额压到几百块。这样折线图上必然会出现一个深坑标记出假期坑的原因就不言自明了。4.2 完整 option 配置与逐段解释const holidays [ { date: 2024-02-10, name: 春节 } ]; const holidayMarkData holidays.map(h ({ xAxis: h.date, label: { formatter: h.name, position: insideEndTop, color: #d93026 }, lineStyle: { color: #d93026, type: dashed, width: 1 } })); option { tooltip: { trigger: axis }, xAxis: { type: category, data: dateList, axisLabel: { formatter: function(value) { // 每 5 天显示一个标签避免文字重叠 return dateList.indexOf(value) % 5 0 ? value : ; } } }, yAxis: { type: value, name: 销售额元 }, series: [{ name: 销售额, type: line, data: valueList, smooth: true, lineStyle: { width: 2, color: #409eff }, itemStyle: { color: #409eff }, markLine: { symbol: [none, none], data: holidayMarkData } }] };这里有三个细节我特意处理过。第一itemStyle.color和lineStyle.color都可以控制线的颜色但 markLine 的lineStyle.color是独立的必须写进每个 data 项里否则默认是随机颜色。我不喜欢默认的灰色统一改成红色虚线跟蓝色销售线形成对比异常/特殊时段的信号感就出来了。第二symbol: [none, none]是去掉标记线两端的箭头和圆点。如果我没记错markLine 默认会在 line 的起始/结束位置画circle端点对于日期分割线来说端点没有意义反而显得累赘所以要去掉。第三axisLabel.formatter里我做了每 5 天显示一个标签的逻辑。类目轴如果 29 个日期全部显示标签必然挤成一团这个格式化函数会判断 index 是否为 5 的倍数是才返回日期字符串否则返回空串保证横轴清爽。4.3 效果与验证跑出来之后图上 2 月 10 日位置会有一根垂直的红色虚线线上方或下方标着春节两个字销售曲线在两天内断崖下跌然后又快速回升。看图的人不需要任何人解释一眼就知道这段波动的原因。这种标注方式最赞的一点是你后续如果换成 time 轴、加上 dataZoom、让它支持拖动这根标记线始终会跟着日期走不用自己动一根像素。以前用 div 方案的时候每次拖动完图表我都得重新算一遍标签位置还容易出 bug现在这个方案一劳永逸。5. 进阶美化与实用组合让标注更专业5.1 给节日线加箭头端点有些场景会希望节假日线在坐标轴顶部带个小箭头起到更强的提示作用。markLine 的symbol可以单独配置起点和终点的样式比如markLine: { symbol: [none, arrow], symbolSize: [8, 10], data: [{ xAxis: 2024-02-10, lineStyle: { color: #d93026 } }] }symbol数组第一个是起点样式第二个是终点样式。这里终点arrow会在线的上端画一个箭头视觉上朝上指着数据点特别适合请看这里的提示语义。配合symbolSize控制箭头大小别太大否则显得很粗糙。5.2 用 label 富文本让节日名排版更漂亮有时候一个节日需要展示两行信息比如春节假期下面加一行2月10日-17日。markLine 的 label 支持富文本 rich可以做出多段样式的标签markLine: { data: [{ xAxis: 2024-02-10, label: { formatter: {name|春节}\n{range|2024.2.10-2.17}, rich: { name: { color: #d93026, fontSize: 14, fontWeight: bold, lineHeight: 20 }, range: { color: #999, fontSize: 11, lineHeight: 16 } } }, lineStyle: { color: #d93026, type: dashed } }] }富文本模板里{name|...}这种写法冒号前是 rich 里定义的样式名。我想强调的是\n换行在 formatter 模板里是有效的配合lineHeight控制行距这个标签虽然只是辅助信息但做出来会显得很精致。实际项目里我还给大促节点配过满 300 减 50这种二级说明效果很好。5.3 动态计算农历节日不写死日期春节、中秋这种农历节日每年公历日期都不一样。把日期写死在配置里明年图表就错了。我在项目里是封装了一个节气/节日换算函数往里传年份返回当年的公历日期范围function lunarHoliday(year) { // 这里内部调用农历换算逻辑返回 { name: 春节, start: 2025-01-29, end: 2025-02-04 } 之类 // 实际可直接引入现成的农历库如 solarlunar 之类的 npm 包 return { name: 春节, start: lunarToSolar(year, 1, 1), end: lunarToSolar(year, 1, 7) }; }函数返回后再把 start/end 拼进 markLine 的 data。注意农历换算逻辑自己写容易出错建议直接引成熟库处理。我当时为了一个闰月差点把日期调错后来干脆交给库省心太多。5.4 多条节日线 markArea 阴影综合实战真实看板不会只标一个节日。把春节线 国庆阴影 端午线放在一起才像一个完整体。常假国庆、春节用 markArea 填充淡色阴影表示时间跨度单日假日端午、中秋用 markLine 点出日期。组合代码示意series: [{ type: line, data: valueList, markLine: { data: [ { xAxis: 2024-06-10, label: { formatter: 端午 } } ] }, markArea: { itemStyle: { color: rgba(217, 48, 38, 0.08) }, data: [ [ { xAxis: 2024-10-01 }, { xAxis: 2024-10-07 } ] ] } }]这里 markArea 是一块「低透明度底色」OK 的是它不遮挡数据曲线因为透明度很低。实际做过一次之后你会发现有阴影 有线条 有文字读者在几秒内就能把图表读透信息传达效率显著提升。6. 实测中踩过的坑完整排查链路与解决思路6.1 坑一类目轴写了字符串线却消失现象markLine 配置了xAxis: 2024-02-10但图表上完全看不到线。控制台也不报错。排查链路我先确认类目轴的数据来源。发现后端接口返回的日期格式是2024/02/10带斜杠的而我在 markLine 里写的是短横线2024-02-10。ECharts 做类目匹配时做的是严格相等比较两个字符串不完全一致它就会认为这个类目不存在于是静默丢掉了这条线。解决把两边格式统一。我的处理是写了一个normalizeDate函数无论是轴数据还是节假日配置都先过一遍这个函数统一输出YYYY-MM-DD。从那以后我再也没有手写过日期字符串进配置一律由函数生成。6.2 坑二time 轴时区偏移线标到了前一天现象x 轴是 time 轴数据用Date.parse(2024-06-10T00:00:00Z)生成的markLine 也用同样方式生成时间戳。结果标记线总是落在 6 月 9 日的位置。排查链路打印时间戳换算成本地时间发现2024-06-10T00:00:00Z在 UTC 时间是 6 月 10 日凌晨但东八区已经 6 月 10 日早上 8 点确切说这是同一时刻的不同表达。问题出在字符串尾部的Z表示 UTC 时区解析出的时间戳没问题但之后我把时间戳再转回本地时间显示时会发现它显示为 6 月 10 日 08:00。严格来说时间戳并没有错错的是一些日期库按本地时区取了日期部分。如果数据点本身存的是北京时间零点那么new Date(2024, 5, 10)这种本地构造方式才是正确的带 Z 的 UTC 字串反而偏了 8 小时。解决全部用本地时间字符串构造或者干脆用new Date(2024, 5, 10)手动指定年月日时分秒让本地时区直接命中零点。我给所有日期处理环节都统一了北京时间零点原则再没出过偏移。6.3 坑三多点标注的文字挤成一团现象春节线、端午线、国庆阴影全部打开后线上方的节日名标签互相重叠特别是国庆阴影区域两端如果都放了 label会遮挡数据线。排查链路我看到 label 默认 position 是end也就是在线的端点处显示。当两条线距离较近时端点标签就会打架。通过调整label.position为insideEndTop、insideEndBottom等可以分散标签的位置。此外ECharts 有label.hideOverlap属性设置为 true 可以自动隐藏重叠标签。解决我的统一策略是——单日节日标签放在线的上端insideEndTop用distance拉开与数据线的距离跨日期的 markArea则在 data 的起始项上把label.show设为 false避免阴影区和线上文字重复。实践经验是宁可少标一点也别让标签互相覆盖标签一糊整张图就废了。6.4 坑四加了 dataZoom 后标注线位置对不上现象图表加了 dataZoom 之后拖动缩放节日标注线可以跟随数据滚动但线的位置偶尔会偏。排查链路我是在类目轴 索引定位情况下遇到的。dataZoom作用于 x 轴时如果类目轴数据是动态生成的indexOf拿到的下标在数据更新后可能变化更隐蔽的是当数据还没完全加载完成时就设置了 markLine下标对应的日期和数据加载后的日期不一致。解决不要用写死的索引。改为在setOption之前先根据当前渲染的 dateList 动态计算 index并且每次数据更新后都重新调用 setOption。类目轴索引定位虽然灵活但必须保证算 index 的数组和渲染轴数据的数组是同一个引用。现在我的做法是干脆用 time 轴 真实时间戳从根上绕开这个问题。6.5 排查路线总结现象重点排查方向解决思路线完全不显示类目字符串格式、坐标值是否在轴范围内统一日期格式/用 index 或时间戳线位置偏一天时区解析、UTC 字串、本地时间构造统一北京时间零点原则标签重叠遮挡label.position、hideOverlap、distance调整位置、关闭重叠标签、阴影去字数据更新后错位索引与数据源不同步、setOption 时机动态算 index渲染前做配置这些坑单个拎出来都不算难但组合在一起就非常磨人。尤其时区那个我一度以为自己写的日期函数出了毛病打印到控制台看了好几遍才意识到是 UTC 字符串在捣鬼。把这些记录下来就是为了让后来的人不用再走我这段弯路。我个人现在只要是做和日期相关的标注需求默认就是 time 轴 毫秒级时间戳 农历库动态转换。类目轴那条路除非图表本身数据量小且格式稳定否则我不会再碰。这组方案配合 markLine我在好几个数据看板项目里都用得很稳。如果你正在为图表上怎么标节假日发愁希望这篇实战记录能帮你省下大半天的排查时间。