ECharts图表库实战

本文项目主干结构

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
├── public
│ └── data.json # 数据文件(静态拆分数据时使用)

├── src
│ ├── utils # 工具
│ │ └── request.ts # axios二次封装
│ │
│ ├── store # Pinia仓库
│ │ └── chartStore.ts # 管理图表相关的数据
│ │
│ ├── services # API 服务
│ │ └── api.ts # 模拟后端 API 请求
│ │
│ ├── components # 组件
│ │ ├── chart # 图表组件
│ │ │ ├── category # 图表分类
│ │ │ │ └── rose.vue # 玫瑰图组件
│ │ │ │ └── bar.vue # 柱状图组件
│ │ │ │ └── line.vue # 折线图组件
│ │ │ │ └── pie.vue # 饼状图组件
│ │ │ │ └── scaatter # 散点图组件
│ │ │ │
│ │ │ ├── data # 数据处理逻辑
│ │ │ │ └── chartData.ts # 数据获取和处理
│ │ │ │
│ │ │ └── chart.vue # 所有图表汇总
│ │ │
│ │ └── other-components # 其他组件
│ │
│ ├── App.vue # 根组件
│ ├── main.ts # 入口文件
│ │
│ └── types # 类型定义
│ └── chart.d.ts # ECharts 数据类型定义

├── vite.config.ts # Vite 配置文件
└── package.json # 项目依赖

Examples - Apache ECharts

Echarts官网→所有示例→选择需要导入的图表→完整代码

本文以基础南丁格尔玫瑰图作示例

Examples - Apache ECharts

官网源码(原生JS)

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
import * as echarts from 'echarts';

var chartDom = document.getElementById('main')!;
var myChart = echarts.init(chartDom);
var option;

option = {
legend: {
top: 'bottom'
},
toolbox: {
show: true,
feature: {
mark: { show: true },
dataView: { show: true, readOnly: false },
restore: { show: true },
saveAsImage: { show: true }
}
},
series: [
{
name: 'Nightingale Chart',
type: 'pie',
radius: [50, 250],
center: ['50%', '50%'],
roseType: 'area',
itemStyle: {
borderRadius: 8
},
data: [
{ value: 40, name: 'rose 1' },
{ value: 38, name: 'rose 2' },
{ value: 32, name: 'rose 3' },
{ value: 30, name: 'rose 4' },
{ value: 28, name: 'rose 5' },
{ value: 26, name: 'rose 6' },
{ value: 22, name: 'rose 7' },
{ value: 18, name: 'rose 8' }
]
}
]
};

option && myChart.setOption(option);

转Vue组件(静态数据)

rose.vue

template

1
2
3
<template>
<div ref="chartContainer" class="chart-container"></div>
</template>

script

引入
1
import * as echarts from 'echarts';

其他按需引入

1
import { onMounted, ref, onUnmounted } from 'vue'; //更多的看自己需求
变量

JS源码

1
var chartDom = document.getElementById('main')!;

转vue组件

1
2
// 获取图表容器的引用
const chartContainer = ref<HTMLElement | null>(null);

JS源码

1
var myChart = echarts.init(chartDom);

转vue组件

1
2
// 定义图表实例
let myChart: echarts.ECharts | null = null;
option

JS源码

1
2
3
4
5
6
7
8
9
var option;
option = {
//这段直接复制过来//
legend: {
top: 'bottom'
},
...
//这段直接复制过来//
};

转vue组件

1
2
3
4
5
6
7
8
9
// 定义图表选项
const option: echarts.EChartsOption = {
//这段直接复制过来//
legend: {
top: 'bottom'
},
...
//这段直接复制过来//
};
函数

JS源码

1
option && myChart.setOption(option);

转vue组件(优化版)

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
// 在组件挂载时初始化图表
onMounted(() => {
if (!chartContainer.value) return;

// 初始化 ECharts 实例
myChart = echarts.init(chartContainer.value);

// 设置图表选项
myChart.setOption(option);

// 监听窗口大小变化,自动调整图表大小
window.addEventListener('resize', resizeChart);
});

// 在组件卸载时清理图表实例
onUnmounted(() => {
if (myChart) {
window.removeEventListener('resize', resizeChart);
myChart.dispose();
}
});

// 自定义函数:调整图表大小
const resizeChart = () => {
if (myChart) {
myChart.resize();
}
};

style

1
2
3
4
.chart-container {
width: 100%;
height: 100%; /* 根据需要调整高度 */
}

抽离数据写法(静态数据)

【注:这里是从本地调用数据的写法,需要】

public / data.json

(模拟后端传来的数据)

1
2
3
4
5
6
7
8
9
10
11
12
{
"data": [
{ "value": 40, "name": "rose 1" },
{ "value": 38, "name": "rose 2" },
{ "value": 32, "name": "rose 3" },
{ "value": 30, "name": "rose 4" },
{ "value": 28, "name": "rose 5" },
{ "value": 26, "name": "rose 6" },
{ "value": 22, "name": "rose 7" },
{ "value": 18, "name": "rose 8" }
]
}

图表组件修改

option / series 部分

1
2
3
4
5
6
7
8
9
10
data: [
{ value: 40, name: 'rose 1' },
{ value: 38, name: 'rose 2' },
{ value: 32, name: 'rose 3' },
{ value: 30, name: 'rose 4' },
{ value: 28, name: 'rose 5' },
{ value: 26, name: 'rose 6' },
{ value: 22, name: 'rose 7' },
{ value: 18, name: 'rose 8' },
],

改为

1
data: [], // 初始为空,稍后从 data.json 加载
添加异步加载函数
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
// 异步加载数据
const loadData = async () => {
try {
// 使用 fetch 从 public/data.json 获取数据
const response = await fetch('/data.json');

// 添加报错提示
if (!response.ok) {
throw new Error('Failed to fetch data');
}

// 提取数据
const data = await response.json();

// 将 JSON 数据转换为 ECharts 需要的格式(根据数据键值对自行修改)
const chartData = data.data.map((item: any) => ({
value: item.value,
name: item.name,
}))

// 替换图表选项中‘data’项
option.series[0].data = chartData;

// 如果图表已经初始化,则重新设置选项
if (myChart) {
myChart.setOption(option);
}
} catch (error) {
console.error('Error loading data:', error);
}
};
调用函数 loadData

在 onMounted 中的 setOption 后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
onMounted(() => {
if (!chartContainer.value) return

// 初始化 ECharts 实例
myChart = echarts.init(chartContainer.value)

// 设置图表选项
myChart.setOption(option)

// 加载数据并更新图表
loadData()

// 监听窗口大小变化,自动调整图表大小
window.addEventListener('resize', resizeChart)
})

真实前后端对接(动态请求)

使用 axios

1
pnpm install axios
axios二次封装

模板

src/utils/request.ts

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
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { ElMessage } from 'element-plus'; // Element Plus 组件(主要是使用Message消息弹出框)
import store from '@/store'; // 引入 Pinia Store
import router from '@/router'; // 引入 Vue Router

// 创建 axios 实例
const service: AxiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/', // 从环境变量中获取 API 基础路径
timeout: 5000, // 请求超时时间
});

// 请求拦截器
service.interceptors.request.use(
(config: AxiosRequestConfig) => {
// 在发送请求之前做些什么
// 例如,添加身份验证 token
const token = store.state.user.token; // 假设你有用户信息存储在 Pinia 中
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
(error) => {
// 对请求错误做些什么
return Promise.reject(error);
}
);

// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse) => {
// 对响应数据做点什么
const res = response.data;

// 如果接口返回的状态码不是 200,说明请求失败
if (res.code !== 200) {
ElMessage.error(res.message || 'Error');
return Promise.reject(new Error(res.message || 'Error'));
} else {
return res;
}
},
(error) => {
// 对响应错误做点什么
if (error.response) {
switch (error.response.status) {
case 401:
// 清除 Token 并跳转到登录页面
store.dispatch('user/logout');
router.push('/login');
break;
case 403:
ElMessage.error('Forbidden');
break;
case 404:
ElMessage.error('Not Found');
break;
case 500:
ElMessage.error('Server Error');
break;
default:
ElMessage.error(error.message);
}
} else {
ElMessage.error('Network Error');
}
return Promise.reject(error);
}
);

export default service;

现在可以在任何地方通过 import request from '@/utils/request' 来使用封装后的 axios 实例

1
import request from '@/utils/request';
函数 loadData修改

静态获取

1
2
3
4
5
6
7
8
9
const loadData = async () => {
try {
/////更改处////
const response = await fetch('/data.json') // 使用 fetch 从 public/data.json 获取数据
if (!response.ok) { throw new Error('Failed to fetch data') }
/////更改处////

const data = await response.json()
......

动态对接

1
2
3
4
5
6
7
8
9
10
import request from '@/utils/request';

const loadData = async () => {
try {
/////更改处////
const response = await axios.get('/api/data'); // 假设后端 API 地址为 /api/data
/////更改处////

const data = response.data;
......

Pinia状态管理

如果你的应用中有多个组件需要共享数据(例如多个图表共享同一份数据),可以考虑使用 Vuex 或 Pinia 进行状态管理。这样可以避免重复的 API 请求,并且更容易管理数据流。

1
pnpm install pinia
main.ts 中注册Pinia
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { createApp } from 'vue';
import App from './App.vue';
import { createPinia } from 'pinia';
import router from './router';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';

const app = createApp(App);

// 注册 Pinia
app.use(createPinia());

// 注册 Vue Router 和 Element Plus
app.use(router);
app.use(ElementPlus);

app.mount('#app');
chartStore.ts
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
import { defineStore } from 'pinia';
import { ref } from 'vue';
import request from '@/utils/request';

interface ChartState {
chartData: Array<{ value: number; name: string }>;
isLoading: boolean;
error: string | null;
}

export const useChartStore = defineStore('chart', {
state: (): ChartState => ({
chartData: [], // 图表数据
isLoading: false, // 加载状态
error: null, // 错误信息
}),
actions: {
async fetchChartData() {
this.isLoading = true;
this.error = null;

try {
const response = await request.get('/api/data'); // 假设后端 API 地址为 /api/data
const data = response.data;

// 将 JSON 数据转换为 ECharts 需要的格式
this.chartData = data.map((item: any) => ({
value: item.sales,
name: item.category,
}));
} catch (error) {
this.error = 'Failed to load chart data';
console.error('Error loading data:', error);
} finally {
this.isLoading = false;
}
},
},
});

这样就可以在组件中直接调用现成的数据逻辑了

rose.vue
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
<script setup lang="ts">
import { onMounted, onUnmounted, ref, watch } from 'vue';
import * as echarts from 'echarts';
import { useChartStore } from '@/store/chartStore';// 调用自定义Store

const chartContainer = ref<HTMLElement | null>(null);
let myChart: echarts.ECharts | null = null;

// 获取 Pinia Store
const chartStore = useChartStore();

// 定义图表选项
const option: echarts.EChartsOption = {
legend: {
top: 'bottom',
},
toolbox: {
show: true,
feature: {
mark: { show: true },
dataView: { show: true, readOnly: false },
restore: { show: true },
saveAsImage: { show: true },
},
},
series: [
{
name: '销量',
type: 'pie',
radius: [50, 250],
center: ['50%', '50%'],
roseType: 'area',
itemStyle: {
borderRadius: 8,
},
data: [], // 初始为空,稍后从 Pinia Store 获取
},
],
};

// 监听 Pinia Store 中的数据变化
watch(
() => chartStore.chartData,
(newData) => {
if (myChart && newData.length > 0) {
option.series[0].data = newData;
myChart.setOption(option);
}
}
);

onMounted(() => {
if (!chartContainer.value) return;
// 初始化 ECharts 实例
myChart = echarts.init(chartContainer.value);
// 设置初始图表选项(此时 series.data 为空)
myChart.setOption(option);
// 加载数据
chartStore.fetchChartData();
// 监听窗口大小变化,自动调整图表大小
window.addEventListener('resize', resizeChart);
});

// 在组件卸载时移除事件监听器
onUnmounted(() => {
if (myChart) {
window.removeEventListener('resize', resizeChart);
myChart.dispose();
}
});

// 自定义函数:调整图表大小
const resizeChart = () => {
if (myChart) {
myChart.resize();
}
};
</script>
更多组件
1
根据上述步骤实现官网源码转Vue组件,插入到category #图表分类 文件夹下即可

加载动画设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
onMounted(() => {
if (!chartContainer.value) return;

// 初始化 ECharts 实例
myChart = echarts.init(chartContainer.value);

// 显示加载动画
myChart.showLoading();

// 加载数据并更新图表
loadData().finally(() => {
// 无论成功还是失败,都隐藏加载动画
myChart?.hideLoading();
});

// 监听窗口大小变化,自动调整图表大小
window.addEventListener('resize', resizeChart);
});

缓存与性能优化

缓存数据

如果数据不会频繁变化,可以考虑使用浏览器的缓存机制(如 localStorage 或 sessionStorage)来缓存数据,减少不必要的 API 请求。

chartStore.ts

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
import { defineStore } from 'pinia';
import { ref } from 'vue';
import request from '@/utils/request';

interface ChartState {
chartData: Array<{ value: number; name: string }>;
isLoading: boolean;
error: string | null;
}

export const useChartStore = defineStore('chart', {
state: (): ChartState => ({
chartData: [], // 图表数据
isLoading: false, // 加载状态
error: null, // 错误信息
}),
actions: {
async fetchChartData() {
this.isLoading = true;
this.error = null;

// 检查是否有缓存数据
const cachedData = localStorage.getItem('chartData');
if (cachedData) {
this.chartData = JSON.parse(cachedData);
this.isLoading = false;
return;
}

try {
const response = await request.get('/api/data'); // 假设后端 API 地址为 /api/data
const data = response.data;

// 将 JSON 数据转换为 ECharts 需要的格式
this.chartData = data.map((item: any) => ({
value: item.sales,
name: item.category,
}));

// 缓存数据
localStorage.setItem('chartData', JSON.stringify(this.chartData));
} catch (error) {
this.error = 'Failed to load chart data';
console.error('Error loading data:', error);
} finally {
this.isLoading = false;
}
},
},
});
按需加载

大型图表或复杂的数据集,可以考虑分页加载数据或懒加载

chartStore.ts

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
export const useChartStore = defineStore('chart', {
state: (): ChartState => ({
chartData: [],
isLoading: false,
error: null,
}),
actions: {
async fetchChartData(forceRefresh: boolean = false) {
this.isLoading = true;
this.error = null;

// 如果不需要强制刷新且有缓存数据,则直接使用缓存
if (!forceRefresh) {
const cachedData = localStorage.getItem('chartData');
if (cachedData) {
this.chartData = JSON.parse(cachedData);
this.isLoading = false;
return;
}
}

try {
const response = await request.get('/api/data'); // 假设后端 API 地址为 /api/data
const data = response.data;

// 将 JSON 数据转换为 ECharts 需要的格式
this.chartData = data.map((item: any) => ({
value: item.sales,
name: item.category,
}));

// 缓存数据
localStorage.setItem('chartData', JSON.stringify(this.chartData));
} catch (error) {
this.error = 'Failed to load chart data';
console.error('Error loading data:', error);
} finally {
this.isLoading = false;
}
},
},
});

rose.vue

调用 fetchChartData 时传递参数

1
2
3
4
5
// 默认情况下使用缓存数据
chartStore.fetchChartData();

// 如果需要强制刷新数据(例如用户点击了“刷新”按钮),可以传递 true
// chartStore.fetchChartData(true);