返回

初步接触可视化Echarts

什么是Echarts

ECharts(全称:Enterprise Charts)是由百度开源的一个基于 JavaScript 的可视化图表库,专门用于构建交互性强、表现丰富的数据可视化图表。


它能做什么?

ECharts 提供了多种常用和高级图表类型,适用于数据分析、仪表盘、报表系统、可视化大屏等场景,例如:

  • 折线图(line)
  • 柱状图(bar)
  • 饼图(pie)
  • 散点图(scatter)
  • 地图(map,支持中国地图和世界地图)
  • 仪表盘(gauge)
  • 热力图(heatmap)
  • 自定义图形(graph、tree、sunburst 等)

它的核心特点:

特性 说明
高性能 基于 Canvas 渲染,适合大数据量可视化
响应式 能适应不同屏幕尺寸,自适应缩放
交互性强 支持 tooltip、点击事件、缩放、图例控制等
主题丰富 提供多种主题,也可自定义
开源免费 使用 Apache-2.0 协议,商业项目也可免费用

安装方式(前端项目):

如果你用的是 Nuxt、Vue、React 等框架,常见引入方式有两种:

npm/yarn 安装:

1
2
3
yarn add echarts
# 或者
npm install echarts

直接在 HTML 中引用(CDN):

1
<script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>

其他类似图表库

虽然 ECharts 很强大,但近几年市面上也涌现了不少其它优秀的图表库,例如:

  • AntV/G2 系列:蚂蚁金服出品,视觉效果更现代,支持更多数据关系分析,且生态体系逐步完善。

  • D3.js:功能灵活,适合自定义程度非常高的可视化开发,不过上手难度和开发成本较高。

  • Highcharts、Chart.js:Highcharts 商业授权较为严格,而 Chart.js 则适合中小型数据量和基本交互需求。

如何写一个图表

图表步骤

  1. 初始化 echarts 实例

    1
    2
    3
    4
    
    let myChart = null
    onMounted(()=>{
        myChart = echarts.init(target.value)
    })
    
  2. 构建 option 配置对象

    1
    2
    
    const options = {
    }
    
  3. 通过 实例.setOption(option) 挂载到对应盒子上

    1
    
    myChart.setOption(options)
    

图表配置

柱形图

 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
const options = {
 // X轴展示数据
 xAxis:{
     show: false,
     // x方向作为值展示
     type: 'value',
     // 设置最大值的配置
     max: function (value) {
         // 动态最大值设置为所有数据中的最大值1.2倍,防止样式问题
         return parseInt(value.max * 1.2)
     }
 },
 // y轴展示数据
 yAxis:{
     // 作为列名展示
     type: 'category',
     // 列名数组,默认从下到上
     data: props.data.regions?.map((item) => item.name),
     // 反向展示
     inverse: true,
     // 不展示线
     axisLine: { show: false },
     // 不展示刻度
     axisTick: { show: false },
     // 展示列名并且label颜色设置
     axisLabel: { color: '#9eb1cd' },
 },
 // 图表绘制的位置,对应上下左右
 grid:{
     top: 0,
     right: 0,
     left: 0,
     bottom: 0,
     // 标签包含进去
     containLabel: true
 },
 // 核心配置
 series:[
     {
         // 柱形图
         type: 'bar',
         // 展示的数据
         data: props.data.regions?.map((item) => ({
             name: item.name,
             value: item.value
         })),
         // 柱状条背景开关
         showBackground: true,
         // 柱状条背景色
         backGroundStyle: {
             color: 'rgba(180, 180, 180, 0.2)'
         },
         // 每个轴的样式
         itemStyle: {
             color: '#5d98CE',
             barBorderRadius: 10,
             shadowColor: 'rgba(0,0,0,0.3)',
             shadowBlur: 5,
         },
         // 柱子宽度
         barWidth: 12,
         // 字体的显示
         label: {
             show: true,
             position: 'right',
             textStyle: {
                 color: '#fff',
             }
            	// 为数据加上百分比,c代表数据
             formatter:'{c}%'
         }
     }
 ],
}

雷达图

 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
88
89
90
91
92
93
94
95
96
97
98
99
    const option = {
        // 雷达坐标系配置
        radar: {
            // 坐标系(外部)名字配置
            name: {
                textStyle: {
                    color: '#05D5FF',
                    fontSize: 14
                }
            },
            // 雷达图形状
            shape: 'polygon',
            center: ['50%', '50%'],
            radius: '80%',
            // 起始位置(开始角度)
            startAngle: 120,
            // 轴线配置
            axisLine: {
                lineStyle: {
                    color: 'rgba(5,213,255,0.8)'
                }
            },
            // 网格线,为true才能形成闭环
            splitLine: {
                show: true,
                lineStyle: {
                    width: 1,
                    color: 'rgba(5,213,255,0.8)'
                }
            },
            // 指示器,就是name部分的坐标系(外部)名字
            indicator: props.data.risks.map((item) => ({
                name: item.name,
                max: 100
            })),
            // 拆分区域,false网格内颜色区分更明显
            splitArea: {
                show: false
            }
        },
        // 坐标极点
        polar: {
            center: ['50%', '50%'],
            radius: '0%'
        },
        // 坐标角度
        angleAxis: {
            min: 0,
            // 分割线分割间隔
            interval: 5,
            // 刻度显示是否为逆时针增长,
            clockwise: false
        },
        // 径向轴
        radiusAxis: {
            min: 0,
            interval: 20,
            splitLine: {
                show: false
            }
        },
        // 图表核心配置
        series: [
            {
                type: 'radar',
                // 指定形状
                symbol: 'circle',
                // 内部数据拐角大小
                symbolSize: 10,
                itemStyle: {
                    normal: {
                        color: '#05D5FF'
                    }
                },
                areaStyle: {
                    normal: {
                        color: '#05D5FF',
                        opacity: 0.5
                    }
                },
                lineStyle: {
                    width: 2,
                    color: '#'
                },
                label: {
                    normal: {
                        // 内部数据文本
                        show: true,
                        color: '#fff'
                    }
                },
                data: [
                    {
                        value: props.data.risks.map((item) => item.value)
                    }
                ]
            }
        ]
    }

环形图

  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
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
const getSeriesData = () => {
const series = []
props.data.abnormals.forEach((item, index) => {
 // 上层
 series.push({
     name: item.name,
     type: 'pie',
     // 逆时针排列
     clockWise: false,
     // 取消悬浮动画
     hoverAnimation: false,
     // 实现半径依次递减
     radius: [73 - index * 15 + '%', 68 - index * 15 + '%'],
     center: ['50%', '50%'],
     label: {
         show: false
     },
     data: [
         {
             value: item.value,
             name: item.name
         },
         {
             value: 1000,
             itemStyle: {
                 color: 'rgba(0,0,0,0)',
                 borderWidth: 0,
             },
             tooltip: {
                 show: false
             },
             hoverAnimation: false
         }
     ]
 })
 // 底层
 series.push({
     name: item.name,
     type: 'pie',
     // 不希望收到任何事件
     silent: true,
     z: 1,
     // 逆时针排列
     clockWise: false,
     // 取消悬浮动画
     hoverAnimation: false,
     // 实现半径依次递减
     radius: [73 - index * 15 + '%', 68 - index * 15 + '%'],
     center: ['50%', '50%'],
     label: {
         show: false
     },
     data: [
         {
             value: 7.5,
             name: item.name,
             itemStyle: {
                 color: 'rgba(3,31,62)',
                 borderWidth: 0,
             },
             tooltip: {
                 show: false,
             },
             hoverAnimation: false
         },
         {
             value: 2.5,
             itemStyle: {
                 color: 'rgba(0,0,0,0)',
                 borderWidth: 0,
             },
             tooltip: {
                 show: false,
             },
             hoverAnimation: false
         }
     ]
 })
});
return series
}
const option = {
 // 图例配置
 legend: {
     show: true,
     // 图例颜色块形状
     icon: 'circle',
     top: '8%',
     left: '52%',
     // 图例数据文本
     data: props.data.abnormals.map((item) => item.name),
     // 负数以列展示
     width: -5,
     // 色块宽高
     itemWidth: 10,
     itemHeight: 10,
     // 图例间距
     itemGap: 6,
     textStyle: {
         fontSize: 12,
         lineHeight: 5,
         color: '#fff'
     }
 },
 // 悬浮提示层
 tooltip: {
     show: true,
     // 触发器
     trigger: 'item',
     // 展示内容,a代表系列名,b代表数据名,c代表数据值,d代表百分比比例
     formatter: '{a}<br>{b}:{c}({d}%)'
 },
 yAxis: [
     {
         type: 'category',
         // 反向展示
         inverse: true,
         axisLine: {
             show: false
         }
     }
 ],
 xAxis: [
     {
         show: false
     }
 ],
 // 由于饼图过多,通过方法进行配置
 series: getSeriesData()
}

关系图

  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
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
    const option = {
        xAxis: {
            show: false,
            type: 'value'
        },
        yAxis: {
            show: false,
            type: 'value'
        },
        series: [
            {
                type: 'graph',
                // 表示不需要任何布局类型
                layout: 'none',
                // 该系列需要使用的坐标系:“二维直角坐标系”
                coordinateSystem: 'cartesian2d',
                // 节点大小
                symbolSize: 26,
                z: 3,
                // 边界的萧条标签文字
                edgeLabel: {
                    // 默认
                    normal: {
                        show: true,
                        color: '#fff',
                        testStyle: {
                            fontSize: 14
                        },
                        // 文本模拟内容
                        formatter: function (params) {
                            return params.data.speed
                        }
                    }
                },
                label: {
                    normal: {
                        show: true,
                        position: 'bottom',
                        color: '#5E5E5E'
                    }
                },
                // 边两端的标记类型,数据流动图标类型
                edgeSymbol: ['none', 'arrow'],
                edgeSymbolSize: 8,
                data: props.data.relations.map(item => {
                    if (item.id !== 0) {
                        return {
                            name: item.name,
                            category: 0,
                            active: true,
                            speed: `${item.speed}kb/s`,
                            value: item.value,
                        }
                    } else {
                        return {
                            name: item.name,
                            value: item.value,
                            symbolSize: 100,
                            itemStyle: {
                                color: {
                                    colorStops: [
                                        {
                                            offset: 0,
                                            color: '#157eff'
                                        },
                                        {
                                            offset: 1,
                                            color: '#35c2ff'
                                        },
                                    ]
                                }
                            },
                            label: {
                                normal: {
                                    fontSize: 14
                                }
                            }
                        }
                    }
                }),
                // 节点间的数据关系
                links: props.data.relations.map((item, index) => ({
                    source: item.source,
                    target: item.target,
                    speed: `${item.speed}kb/s`,
                    lineStyle: {
                        normal: {
                            color: '#12b5d0',
                            // 曲线率
                            curveness: 0.2
                        }
                    },
                    label: {
                        show: true,
                        position: 'middle',
                        // 是否对文字进行偏移
                        offset: [10, 0]
                    }
                }))
            }, {
                type: 'lines',
                // 该系列需要使用的坐标系:“二维直角坐标系”
                coordinateSystem: 'cartesian2d',
                z: 1,
                // 线条的特效配置
                effect: {
                    show: true,
                    smooth: false,
                    trailLength: 0,
                    symbol: 'arrow',
                    color: 'rgba(55,155,255,0.6)',
                    symbolSize: 12
                },
                lineStyle: {
                    normal: {
                        curveness: 0.2
                    }
                },
                data: [
                    [{ coord: [0, 300] }, { coord: [50, 200] }],
                    [{ coord: [0, 100] }, { coord: [50, 200] }],
                    [{ coord: [50, 200] }, { coord: [100, 100] }],
                    [{ coord: [50, 200] }, { coord: [100, 300] }]
                ]
            }
        ]
    }

词云图

  • 需要安装依赖后导入import("echarts-wordcloud")

    注意:借助浏览器的插件在Nuxt框架下需要判断让它在客户端导入,不然会报错!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const option ={
        series: [
            {
                type: 'wordCloud',
                // 文字大小范围
                sizeRange: [8, 46],
                // 文字旋转
                rotationRange: [0, 0],
                gridSize: 0,
                layoutAnimation: true,
                textStyle: {
                    color: randomRGB
                },
                // 高亮字体
                emphasis: {
                    textStyle: {
                        fontWeight: 'bold',
                        color: '#000'
                    }
                },
                data: props.data.datas
            }
        ]
    }

封装composable用于操作echarts生成

  • 按需加载插件,节省初始包体积。

  • 避免 SSR 报错(import 仅在客户端执行)。

  • 清理监听器:避免内存泄漏。

  • 销毁 ECharts 实例:释放资源,避免内存不断积累。

  • 正确时机:组件即将卸载时执行,最适合做 cleanup。

 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
// 导入 echarts 主库
import * as echarts from "echarts"
// 声明 process.client(为了让 TypeScript 不报错)
// 在 Nuxt 中,process.client 表示是否处于客户端运行环境
declare const process: {
  client: boolean;
}

// 导出一个 composable,用于封装 echarts 的初始化和操作逻辑
export const useEcharts = () => {
  // 图表容器的 DOM 引用(挂载到页面上的 <div ref="chartRef">)
  const chartRef = ref<HTMLElement | null>(null)

  // 图表实例对象(echarts.init 的返回值)
  let chartInstance: echarts.ECharts | null = null

  // 初始化图表,支持普通图表或词云(wordcloud)
  const initChart = async (type?: "wordcloud") => {
    if (!chartRef.value) return // 容器还没挂载,跳过

    // 如果是词云图表,并且是在客户端环境下,动态加载 echarts-wordcloud 插件
    if (type === "wordcloud" && process.client) {
      await import("echarts-wordcloud")
    }

    // 初始化 echarts 实例,绑定到 DOM 容器上
    chartInstance = echarts.init(chartRef.value)
  }

  // 设置图表的配置项(option)
  const setOptions = (options: any) => {
    chartInstance?.setOption(options)
  }

  // 响应窗口大小变化,重置图表大小
  const resize = () => {
    chartInstance?.resize()
  }

  // 组件挂载时绑定 resize 事件监听
  onMounted(() => {
    if (process.client) {
      window.addEventListener("resize", resize)
    }
  })

  // 组件销毁前解绑事件监听,并销毁 echarts 实例释放资源
  onBeforeUnmount(() => {
    if (process.client) {
      window.removeEventListener("resize", resize)
      chartInstance?.dispose()
    }
  })

  // 返回方法和 DOM 引用,供组件内使用
  return {
    chartRef,     // DOM 引用,组件中通过 ref="chartRef" 绑定
    initChart,    // 初始化图表方法
    setOptions,   // 设置图表配置项方法
    resize        // 手动触发图表 resize
  }
}

用nuxt3写echarts开发注意事项

SSR渲染问题

  • 图表组件依赖 DOM 和浏览器环境,不能 SSR,用 <ClientOnly> 包裹图表部分,完美规避服务端渲染的问题。
使用 Hugo 构建
主题 StackJimmy 设计