资讯动态

ECharts图表导出PDF:前端三件套实现与避坑指南

发布时间:2026/10/14 14:22:58 来源:尧图企业网站定制
简介面向需要将ECharts图表导出为PDF的前端开发者这套压缩包汇总了ECharts、html2canvas、jsPDF三个关键JavaScript库及一个可直接运行的示例页面解决在数据分析报表场景中浏览器端生成含图表PDF的常见需求。压缩包共4个文件以三个js库为主另有一个html示例整体仅345KB轻量易下载适合有基础前端知识、希望快速集成导出功能的读者参考。配套说明详细给出了从初始化ECharts、通过html2canvas将图表转为Canvas再到用jspdf输出PDF的完整思路并提示了图片质量、多页处理及跨域Base64编码等注意事项。示例页面可以帮读者免去自行组合配置的繁琐直接对照演示代码修改数据和选项即可。目前已有1393人浏览学习对于需要处理可视化报告导出的学习者是一份可直接上手的小工具包。1. ECharts 图表生成 PDF 所需文件三个 JS 就能打通导出链路做过报表导出的人都有印象页面里 ECharts 图画得再漂亮到了 PDF 里不是白屏就是模糊。最常见的解法不是后端截图而是前端拼三个文件——echarts.min.js 负责画图html2canvas 负责把图表区域拍成 canvasjspdf.min.js 负责把 canvas 排进 PDF。这个资源包就是把生成 ECharts 图表 PDF 所需的三个文件按可用版本配好拿到就能直接引入项目。适合正在给后台管理、数据大屏加导出功能的前端开发者也适合不知道依赖怎么配的学生项目。下面我把完整的调用逻辑、参数设置和踩过的坑按实现顺序拆开说。2. 三件套分工echarts.min.js、html2canvas、jspdf.min.js 各自要干什么这套方案的链路非常直白ECharts 把数据画成图表html2canvas 把图表所在区域复制成一张图片jsPDF 把这张图片粘贴进 PDF 文件。三个库各管一段边界清晰。2.1 echarts.min.js容器、实例与渲染器选择ECharts 本身是个绘图库它把配置项转换成图形绘制在 canvas 或 SVG 上。默认渲染逻辑是 canvas也就是页面里能看到的是一个canvas元素ECharts 通过canvas.getContext(2d)在上面绘制柱形、折线、饼图等。在做 PDF 导出时最终要拿到的是 canvas 上的像素数据所以建议保持默认的 canvas 渲染器。如果你在项目里特意设置了renderer: svghtml2canvas 虽然也能识别 SVG 并截图但 SVG 内部的文字、渐变、跨域图片处理会比 canvas 多一层额外转化导出的边界情况更难排查。因此我一般在初始化 ECharts 时不改 renderer保证导出链路一致。ECharts 实例必须绑定在一个有实际宽高的容器上。如果容器是display:none或者被v-if卸载后再初始化echarts.init()会拿到宽高为 0 的 canvas后续截图必然是空的。常见做法是给容器固定宽高const chartDom document.getElementById(chart); const chart echarts.init(chartDom); chart.setOption({ xAxis: { type: category, data: [Mon, Tue, Wed] }, yAxis: { type: value }, series: [{ type: bar, data: [120, 200, 150] }] });echarts.init的第二个参数可以传主题第三个参数可以传renderer。这里保持默认即可。需要注意的是setOption是异步渲染的代码执行到下一行时画布不一定已经完成绘制这个问题在导出时会放大下文会有对应的处理方式。2.2 html2canvas把 DOM 拍成 canvas 快照的原理html2canvas 并不是浏览器原生的截图接口它做的事情是递归遍历目标 DOM 节点把每个元素的位置、尺寸、颜色、边框、字体等样式计算出来再利用 canvas 的绘图 API 重新画一遍。也就是说它模拟了一次浏览器渲染。这个特性决定了两个重要结论第一它能看到 ECharts 画出的canvas元素但只会把这个 canvas 当成一张普通图片贴上去不会重新生成图表内的柱形和文字第二如果 ECharts 的 canvas 还没有绘制完成html2canvas 贴上去的就是一张空白画布。所以导出函数里经常能看到setTimeout或等待finished事件本质上是给 ECharts 留够绘制时间。html2canvas 的常用配置项里我几乎每次都带这几项html2canvas(document.getElementById(chart), { scale: 2, useCORS: true, backgroundColor: #ffffff, logging: false });scale控制输出 canvas 的分辨率倍率默认是 1。如果直接以这个分辨率放进 A4 PDF图片宽度会被拉伸到 595 磅左右等效 DPI 不够打印出来文字全是锯齿。设成 2 或 3 能明显改善清晰度。useCORS允许跨域图片参与绘制否则 canvas 会被污染导致toDataURL()直接抛错。backgroundColor强制白色背景避免透明背景在 PDF 阅读器里显示成黑色。2.3 jspdf.min.js页面坐标、单位和图片格式jsPDF 的职责是创建一个 PDF 文档然后通过addImage把图片放到页面上。实例化时可以指定单位常用pt磅或mm。A4 纸在pt单位下是 595.28 x 841.89在mm下是 210 x 297。我个人习惯用pt因为前端代码里的像素单位换算起来更直观1 像素约等于 0.75 磅导出时通常会做 2 倍缩放。addImage的签名是addImage(imageData, format, x, y, width, height)。imageData可以是图片 URL也可以是 canvas 的 DataURL。format要对应传了 PNG 就写PNG传了 JPEG 就写JPEG。如果图片本身是透明背景用 JPEG 会把透明区域变成黑色所以要么用 PNG要么在 html2canvas 阶段就垫白底。坐标原点是页面左上角y 轴向下增加。很多人第一次写addImage时把 y 坐标当成了数学里的纵轴结果图片跑到了页面右上角或直接被裁掉。下面这段代码是一个最基础的导出动作const { jsPDF } window.jspdf; const pdf new jsPDF(p, pt, a4); const imgData canvas.toDataURL(image/jpeg, 0.92); const pageWidth pdf.internal.pageSize.getWidth(); const margin 20; const imgWidth pageWidth - margin * 2; const imgHeight (canvas.height * imgWidth) / canvas.width; pdf.addImage(imgData, JPEG, margin, margin, imgWidth, imgHeight); pdf.save(chart.pdf);canvas.height / canvas.width保持图片原始宽高比避免图表被拉扁。2.4 为什么不直接用 ECharts 的 getDataURL这里有个很容易被忽视的替代方案ECharts 实例自己就有getDataURL()方法能拿到 canvas 的 base64。那为什么还要引入 html2canvas因为getDataURL()只能拿到图表自身的 canvas图表外面的标题、边框、自定义 HTML 模板、水印、统计卡片都不会包含进去。如果需求只是“把这张图导出去”可以直接用chart.getDataURL({ pixelRatio: 2 })但如果需求是“把整个报表卡片导出去”就必须靠 html2canvas 截取 DOM 区域。所以这套三件套的适用边界很清楚导出内容里除了 ECharts 图表还有周边 DOM 元素。纯图表导出用不上三个文件反过来也说明这三个文件组合是报表导出场景的标配。3. 最小可用导出链路从 ECharts 实例到 PDF 下载这一章给出一个可以完整跑通的最小实现。页面里只有一个图表和一个导出按钮点击按钮后生成 PDF。3.1 先准备一个固定宽高的图表容器HTML 部分不需要复杂结构一个带 id 的 div 就行。建议加上明确的宽高和白色背景避免后续截图时拿到 0 尺寸或透明底div idreport stylewidth: 800px; height: 450px; background: #fff; padding: 12px; div idchart stylewidth: 100%; height: 400px;/div /div注意外层容器带 padding这样导出的图片会把内边距也带进去视觉上更像一张报表卡片。#chart是 ECharts 的挂载点宽高由外层撑开。3.2 完成图表渲染在页面脚本里初始化 ECharts 并塞入一份测试数据const chart echarts.init(document.getElementById(chart)); chart.setOption({ title: { text: 一周访问量趋势 }, tooltip: { trigger: axis }, xAxis: { type: category, data: [Mon, Tue, Wed, Thu, Fri] }, yAxis: { type: value }, series: [{ name: 访问量, type: line, data: [820, 932, 901, 934, 1290] }] });初始化时如果容器刚被创建但尚未布局完成echarts.init会计算出错误的尺寸。我的习惯是在window.requestAnimationFrame或setTimeout回调里再 init确保浏览器已完成当前帧的样式计算。这个项目里如果页面是静态的一般不会碰到但静态页面转成动态框架后就很容易踩。3.3 导出按钮触发截图与 PDF 生成按钮点击事件里做的事其实就三步截图、计算尺寸、写入 PDF。完整代码如下document.getElementById(exportBtn).addEventListener(click, function () { const target document.getElementById(report); html2canvas(target, { scale: 2, useCORS: true, backgroundColor: #ffffff }).then(function (canvas) { const { jsPDF } window.jspdf; const pdf new jsPDF(p, pt, a4); const imgData canvas.toDataURL(image/jpeg, 0.92); const pageWidth pdf.internal.pageSize.getWidth(); const pageHeight pdf.internal.pageSize.getHeight(); const margin 24; const imgWidth pageWidth - margin * 2; const imgHeight (canvas.height * imgWidth) / canvas.width; let y margin; if (imgHeight pageHeight - margin * 2) { // 单张图超过一页高度时先切页下面第 4 章会展开 pdf.addPage(); y margin; } pdf.addImage(imgData, JPEG, margin, y, imgWidth, imgHeight); pdf.save(report.pdf); }); });这段代码里的scale: 2保证输出图片的物理像素足够imgHeight按宽高比计算防止图表变形。pdf.save会触发浏览器下载文件名可以按当前日期动态拼接这里省略了。3.4 导出时机ECharts 渲染完成的判断上面代码里如果点击按钮时 ECharts 动画还没播完html2canvas截到的可能是一半柱形。最稳的处理方式是监听 ECharts 的finished事件但这个事件只触发一次不适合每次导出都绑定。我一般会在导出函数开头调用一次chart.resize()把尺寸强制刷新然后用requestAnimationFrame给一个帧的等待时间chart.resize(); requestAnimationFrame(function () { html2canvas(target, { scale: 2, useCORS: true }).then(...); });chart.resize()会同步更新 canvas 尺寸requestAnimationFrame保证浏览器完成一次绘制。如果图表数据量很大可以把requestAnimationFrame换成 200~300ms 的setTimeout但没必要一上来就固定死。4. 多图表与分页输出把一整张报表排成多页 PDF真实业务里不可能只有一个图表。一个数据看板往往有 4 到 6 个图表如果全部截到一张图片里要么文字被压缩要么导出后看不清。正确的做法是每个图表单独截图在 PDF 里按顺序排列放不下就自动加页。4.1 布局与选择器先给所有图表加一个统一样式类例如每个卡片都带chart-paneldiv classchart-panel stylewidth: 100%; height: 380px; background: #fff; margin-bottom: 12px; idpanel1/div div classchart-panel stylewidth: 100%; height: 380px; background: #fff; margin-bottom: 12px; idpanel2/div每个面板都已经初始化好 ECharts 实例导出时只需要把这些面板的 DOM 元素收集起来逐一截图。4.2 用 Promise.all 处理异步截图html2canvas是异步任务如果在一个循环里逐个调用再最后生成 PDF会因为任务还没结束就执行save而出错。推荐用Promise.all把所有截图任务并行执行全部完成后统一写入 PDFconst panels Array.from(document.querySelectorAll(.chart-panel)); Promise.all( panels.map(el html2canvas(el, { scale: 2, useCORS: true, backgroundColor: #ffffff })) ).then(function (canvases) { const { jsPDF } window.jspdf; const pdf new jsPDF(p, pt, a4); const pageWidth pdf.internal.pageSize.getWidth(); const pageHeight pdf.internal.pageSize.getHeight(); const margin 24; let offsetY margin; canvases.forEach(function (canvas) { const imgWidth pageWidth - margin * 2; const imgHeight (canvas.height * imgWidth) / canvas.width; // 剩余空间不足时切换到新页面 if (offsetY imgHeight pageHeight - margin) { pdf.addPage(); offsetY margin; } pdf.addImage(canvas.toDataURL(image/jpeg, 0.92), JPEG, margin, offsetY, imgWidth, imgHeight); offsetY imgHeight 16; // 16 为图表间距 }); pdf.save(dashboard.pdf); });offsetY是当前页的纵向光标每放一张图就累加图片高度和间距。当光标接近页底时先加一页再重置为margin这样就不会出现“第二张图画在第一张图上”的错乱。16 这个间距值可以调整也可以从 CSS 样式里读取但因为 html2canvas 已经把 margin 画进了截图里这里最好用一个独立间距避免与截图内的边距混淆。4.3 单图超高时的图片拆分如果某个图表本身高度超过一页例如一张超长的河流图或时序全景图直接塞到 PDF 会超出页面边界后半截内容被裁掉。处理办法是把大 canvas 切成多段每段对应 PDF 的一页。function splitCanvasToPdf(canvas, pdf) { const pageHeight pdf.internal.pageSize.getHeight(); const margin 24; const viewableHeight pageHeight - margin * 2; const imgWidth pdf.internal.pageSize.getWidth() - margin * 2; const ratio imgWidth / canvas.width; const imgHeightPerPage viewableHeight; let renderedHeight 0; const slice document.createElement(canvas); slice.width canvas.width; slice.height Math.floor(viewableHeight / ratio); while (renderedHeight canvas.height) { const curHeight Math.min(slice.height, canvas.height - renderedHeight); slice.getContext(2d).drawImage( canvas, 0, renderedHeight, canvas.width, curHeight, 0, 0, canvas.width, curHeight ); if (renderedHeight 0) { pdf.addPage(); } pdf.addImage(slice.toDataURL(image/jpeg, 0.92), JPEG, margin, margin, imgWidth, curHeight * ratio); renderedHeight curHeight; } }这里用drawImage做按行切分renderedHeight记录已经裁出的部分。要注意pdf.addImage传入的 height 必须与切分区域的curHeight按比例换算否则页面上下会留出错误空白。这个方法也可以和前面的多图表逻辑合并在遍历图表时先判断高度是否超出一页。4.4 横向或自定义页面尺寸如果图表整体很长使用纵向 A4 可能需要 3 到 4 页阅读起来不连贯。常见做法是把 PDF 改成横向 A4new jsPDF(l, pt, a4)第一参数传l表示横向。页面宽高会交换pageWidth仍然是internal.pageSize.getWidth()代码逻辑不用变但截图里内容的呈现方向需要前端先确认。5. 避坑与常见问题导出白屏、模糊与多页错位的排查记录这一章记录我在实际项目中反复遇到过的 5 个问题每条都按“现象 → 原因 → 解决”的顺序给出排查时可以照顺序对一遍。5.1 导出时图表区域一片空白现象html2canvas 返回的 canvas 尺寸正常但图表区域是纯白或透明PDF 里看不到柱形和文字。原因绝大多数情况是导出时机太早ECharts 的 canvas 还没完成绘制另一种常见原因是容器被弹窗或隐藏 tab 包裹初始化时宽度为 0canvas 虽然存在但没有任何像素。解决导出前先调用chart.resize()如果图表所在的 Tab 刚切换到激活状态resize会重新计算容器尺寸。接着再等一帧chart.resize(); requestAnimationFrame(() { html2canvas(target, { scale: 2, useCORS: true }).then(...); });如果容器本身是display:none需要先把它设为display:block等浏览器完成布局后再执行截图。隐藏容器是 Canvas 白屏的头号原因建议所有图表初始化前都确认offsetWidth大于 0。5.2 生成的 PDF 图片模糊文字发虚现象PDF 打开后图表内的中文文字边缘有严重锯齿放大 200% 后模糊不清。原因html2canvas 的scale默认是浏览器设备像素比一般就是 1。图片在 PDF 里被拉伸到页宽相当于把一张 800px 宽的图画在 600pt 宽的页面上物理像素不够。解决把scale提到 2 或 3。如果图表本身的 DOM 宽度是 800pxscale: 2会得到 1600px 宽的 canvas放进 PDF 后每个 pt 对应约 2.7 像素清晰度足够。注意scale不是越高越好超过 3 后生成时间和 PDF 体积会明显上升收益却很小。5.3 多图表导出时内容叠加或第二张图被截断现象多个图表依次导出后第二张图覆盖在第一张图上面或者第二张图只有上半部分下半部分消失。原因offsetY没有正确累加。如果导出逻辑里用的是循环内html2canvas的then每张图完成后都去addImage由于截图任务并发完成写 PDF 的顺序会乱掉后完成的图可能覆盖先完成的图。解决统一用Promise.all收集所有 canvas再按数组顺序写入 PDF。offsetY在forEach里累加并加上图表间距。判断语句必须是“当前偏移 图片高度 页面高度 - 边距”否则正好卡在页面边界时也会被截断。5.4 图表使用了远程图片时报跨域错误现象ECharts 的series或背景图引用了 CDN 图片浏览器控制台报canvas has been tainted by cross-origin data导出按钮点击后没有反应。原因html2canvas 把远程图片画进 canvas 时会污染 canvas。污染后的 canvas 调用toDataURL()会抛出安全错误。解决给 html2canvas 设置useCORS: true同时确认远程图片的响应头里带有Access-Control-Allow-Origin或*。如果图片服务器不受控制一种变通方案是在渲染 ECharts 前先用fetch把图片转成 base64 再传给图表这样 html2canvas 拿到的就是同源数据。图标、标记类图片建议直接内嵌 base64能少一件事。5.5 导出后的 PDF 页面背景是黑色或透明现象PDF 里图表周围的背景是黑色或者打开后页面底色是透明的打印出来变成深色。原因html2canvas 默认backgroundColor是透明。如果页面或容器的底色是深色生成的 canvas 背景会保留深色如果代码里没给容器背景canvas 就是透明而多数 PDF 阅读器把透明区域渲染成黑色。解决在 html2canvas 配置里显式写backgroundColor: #ffffff或者给目标 DOM 加一个background: #fff的样式。两种方式任选其一我倾向在配置里写因为这样不污染页面样式。排查顺序建议先打开浏览器控制台看有没有跨域报错再看导出的 canvas 宽度和高度是否为 0最后看 PDF 生成的坐标参数。80% 的问题都集中在两个点截图时机太早和 scale 设置过低。6. 进阶封装一个可复用的导出函数处理多图表、水印和分页如果项目里到处都要用导出功能把逻辑封装成一个函数会省掉很多重复代码。下面是我最近在某个跨平台系统里用的一个版本入口参数只有四个选择器、文件名、清晰度、水印文字。async function exportDashboardToPdf({ selector .chart-panel, fileName dashboard.pdf, scale 2, watermark } {}) { const containers Array.from(document.querySelectorAll(selector)); if (containers.length 0) { throw new Error(没有匹配到任何图表容器); } const canvases await Promise.all( containers.map(el html2canvas(el, { scale, useCORS: true, backgroundColor: #ffffff })) ); const { jsPDF } window.jspdf; const pdf new jsPDF(p, pt, a4); const pageWidth pdf.internal.pageSize.getWidth(); const pageHeight pdf.internal.pageSize.getHeight(); const margin 24; let offsetY margin; canvases.forEach(canvas { const imgWidth pageWidth - margin * 2; const imgHeight (canvas.height * imgWidth) / canvas.width; if (offsetY imgHeight pageHeight - margin) { pdf.addPage(); offsetY margin; } pdf.addImage(canvas.toDataURL(image/jpeg, 0.92), JPEG, margin, offsetY, imgWidth, imgHeight); offsetY imgHeight 16; }); if (watermark) { pdf.setGState(pdf.GState({ opacity: 0.12 })); pdf.setFontSize(72); pdf.setTextColor(120, 120, 120); pdf.text(watermark, pageWidth / 2, pageHeight / 2, { align: center, angle: 30 }); } pdf.save(fileName); }使用时的调用方式很简单document.getElementById(exportBtn).addEventListener(click, () { exportDashboardToPdf({ selector: .chart-panel, fileName: 运营报表.pdf, scale: 3, watermark: 内部预览 }); });这个函数把前面所有关键点收了进来Promise.all保证多图顺序offsetY控制分页scale暴露成参数后不用再改源码调清晰度水印用GState设置透明度避免遮挡数据。最后说一个我自己的教训。第一次做这类导出时我把所有图表都放在一个弹窗里用户点击导出时弹窗刚打开ECharts 还在初始化结果是第一张图永远白屏。后来我在html2canvas前强制判断图表容器offsetHeight 0不满足就拦截导出并提示重试。从那以后我每次导出前都会走一遍这个检查再顺手调一下chart.resize()再也没有交付过白底 PDF。这个习惯看起来多写了三行代码但在用户那少挨不少骂。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑