ECharts SVG导出完全指南
文档类型: 实战指南
难度等级: ⭐⭐⭐
源码版本: ECharts 5.x
本文行数: 约380行
📋 目录
🎯 SVG vs Canvas
对比分析
| 特性 | SVG | Canvas |
|---|---|---|
| 格式 | 矢量图 | 位图 |
| 缩放 | 无损 | 有损 |
| 文件大小 | 小(简单图形) | 固定 |
| 性能 | 简单图形快 | 复杂图形快 |
| 编辑性 | 可编辑 | 不可编辑 |
| 适用场景 | Logo/图标 | 大数据量图表 |
🔧 SVG渲染器配置
使用SVG渲染器
typescript
// 初始化时指定
const chart = echarts.init(container, null, {
renderer: 'svg' // 默认是 'canvas'
});
chart.setOption(option);1
2
3
4
5
6
2
3
4
5
6
导出SVG代码
typescript
// 获取SVG字符串
const svgContent = chart.renderToSVGString();
// 下载SVG文件
const blob = new Blob([svgContent], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.download = 'chart.svg';
link.href = url;
link.click();
URL.revokeObjectURL(url);1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
完整导出函数
typescript
function exportAsSVG(chart: echarts.ECharts, filename: string = 'chart.svg') {
// 获取SVG内容
const svgContent = chart.renderToSVGString();
// 添加XML声明
const svgWithHeader = `<?xml version="1.0" encoding="UTF-8"?>\n${svgContent}`;
// 创建Blob
const blob = new Blob([svgWithHeader], {
type: 'image/svg+xml;charset=utf-8'
});
// 触发下载
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.download = filename;
link.href = url;
link.click();
// 清理
setTimeout(() => URL.revokeObjectURL(url), 100);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
🚀 导出与优化
SVG优化: 移除不必要的元素
typescript
function optimizeSVG(svgContent: string): string {
return svgContent
// 移除注释
.replace(/<!--[\s\S]*?-->/g, '')
// 移除多余空白
.replace(/\s+/g, ' ')
// 压缩颜色值
.replace(/#([a-f0-9])\1([a-f0-9])\2([a-f0-9])\3/gi, '#$1$2$3')
// 移除默认值
.replace(/fill="none"/g, '')
.trim();
}
// 使用
const optimizedSVG = optimizeSVG(chart.renderToSVGString());1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
嵌入字体
typescript
function embedFonts(svgContent: string): string {
const fontStyles = `
<defs>
<style>
@font-face {
font-family: 'CustomFont';
src: url('data:font/woff2;base64,...') format('woff2');
}
text {
font-family: 'CustomFont', sans-serif;
}
</style>
</defs>
`;
// 在<svg>标签后插入
return svgContent.replace('<svg>', `<svg>${fontStyles}`);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
💻 实战案例
案例1: 响应式SVG导出
typescript
class ResponsiveSVGExporter {
private chart: echarts.ECharts;
constructor(container: HTMLElement, option: any) {
this.chart = echarts.init(container, null, { renderer: 'svg' });
this.chart.setOption(option);
}
/**
* 导出不同尺寸的SVG
*/
exportMultipleSizes(baseFilename: string) {
const sizes = [
{ width: 800, height: 600, suffix: 'md' },
{ width: 1200, height: 900, suffix: 'lg' },
{ width: 1600, height: 1200, suffix: 'xl' }
];
sizes.forEach(size => {
// 调整尺寸
this.chart.resize({
width: size.width,
height: size.height
});
// 导出
const svgContent = this.chart.renderToSVGString();
this.downloadSVG(svgContent, `${baseFilename}-${size.suffix}.svg`);
});
}
private downloadSVG(content: string, filename: string) {
const blob = new Blob([content], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.download = filename;
link.href = url;
link.click();
URL.revokeObjectURL(url);
}
dispose() {
this.chart.dispose();
}
}
// 使用
const exporter = new ResponsiveSVGExporter(
document.getElementById('chart')!,
option
);
exporter.exportMultipleSizes('my-chart');1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
案例2: SVG转其他格式
typescript
class SVGConverter {
/**
* SVG转PNG
*/
static async svgToPng(svgContent: string, scale: number = 2): Promise<string> {
return new Promise((resolve, reject) => {
const img = new Image();
const svgBlob = new Blob([svgContent], { type: 'image/svg+xml' });
const url = URL.createObjectURL(svgBlob);
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width * scale;
canvas.height = img.height * scale;
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('无法获取Canvas上下文'));
return;
}
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
resolve(canvas.toDataURL('image/png'));
URL.revokeObjectURL(url);
};
img.onerror = reject;
img.src = url;
});
}
/**
* SVG转JPEG
*/
static async svgToJpeg(
svgContent: string,
quality: number = 0.9
): Promise<string> {
const pngURL = await this.svgToPng(svgContent);
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('无法获取Canvas上下文'));
return;
}
// 白色背景
ctx.fillStyle = '#fff';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
resolve(canvas.toDataURL('image/jpeg', quality));
};
img.onerror = reject;
img.src = pngURL;
});
}
}
// 使用
async function convertAndDownload() {
const svgContent = chart.renderToSVGString();
// 转PNG
const pngURL = await SVGConverter.svgToPng(svgContent);
downloadFile(pngURL, 'chart.png');
// 转JPEG
const jpegURL = await SVGConverter.svgToJpeg(svgContent, 0.95);
downloadFile(jpegURL, 'chart.jpg');
}
function downloadFile(url: string, filename: string) {
const link = document.createElement('a');
link.download = filename;
link.href = url;
link.click();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
案例3: 服务端SVG生成 (Node.js)
typescript
import { JSDOM } from 'jsdom';
import * as echarts from 'echarts';
/**
* 在服务端生成SVG
*/
function generateSVGOnServer(option: any): string {
// 创建虚拟DOM环境
const dom = new JSDOM('<!DOCTYPE html><div id="chart"></div>');
global.window = dom.window as any;
global.document = dom.window.document;
global.navigator = dom.window.navigator;
// 初始化图表
const container = dom.window.document.getElementById('chart');
const chart = echarts.init(container as any, null, { renderer: 'svg' });
// 设置option
chart.setOption(option);
// 获取SVG
const svgContent = chart.renderToSVGString();
// 清理
chart.dispose();
return svgContent;
}
// Express路由
import express from 'express';
const app = express();
app.get('/api/chart-svg', (req, res) => {
const option = JSON.parse(req.query.option as string);
try {
const svg = generateSVGOnServer(option);
res.setHeader('Content-Type', 'image/svg+xml');
res.send(svg);
} catch (error) {
res.status(500).json({ error: '生成失败' });
}
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
🎯 最佳实践总结
✅ DO - 推荐做法
使用SVG渲染器获得矢量图
typescriptrenderer: 'svg'1优化SVG减小文件大小
typescriptoptimizeSVG(svgContent)1提供多种格式选择
typescript// PNG / JPEG / SVG1
❌ DON'T - 避免做法
- 避免在大数据量时使用SVGtypescript
// ❌ 不好 - 10000+数据点 renderer: 'svg' // ✅ 好 - 使用Canvas renderer: 'canvas'1
2
3
4
5
🔗 相关资源
下一篇: 响应式适配
