工具ECharts图表库实战
Breezli本文项目主干结构
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;
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 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 { const response = await fetch('/data.json');
if (!response.ok) { throw new Error('Failed to fetch data'); }
const data = await response.json();
const chartData = data.data.map((item: any) => ({ value: item.value, name: item.name, }))
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
myChart = echarts.init(chartContainer.value)
myChart.setOption(option)
loadData()
window.addEventListener('resize', resizeChart) })
|
真实前后端对接(动态请求)
使用 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'; import store from '@/store'; import router from '@/router';
const service: AxiosInstance = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || '/', timeout: 5000, });
service.interceptors.request.use( (config: AxiosRequestConfig) => { const token = store.state.user.token; if (token) { config.headers['Authorization'] = `Bearer ${token}`; } return config; }, (error) => { return Promise.reject(error); } );
service.interceptors.response.use( (response: AxiosResponse) => { const res = response.data;
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: 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') 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'); const data = response.data; ......
|
Pinia状态管理
如果你的应用中有多个组件需要共享数据(例如多个图表共享同一份数据),可以考虑使用 Vuex 或 Pinia 进行状态管理。这样可以避免重复的 API 请求,并且更容易管理数据流。
在 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);
app.use(createPinia());
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'); const data = response.data;
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;
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'); const data = response.data;
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'); const data = response.data;
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();
|