前端数据可视化:基于Vue3封装 ECharts 的最佳实践
ECharts Vue 组件
这是一个基于 Vue 3 和 ECharts 的可复用图表组件,支持常见的图表功能,包括动态配置、自动调整大小、加载状态、空数据提示等。
🚀 功能特性
- ECharts 配置:支持传入完整的 ECharts 配置项 (
option
)。 - 动态数据更新:通过监听
option
的变化,动态更新图表。 - 自动调整大小:支持父容器尺寸变化时自动触发图表
resize
。 - 加载状态:支持显示加载状态。
- 空数据提示:支持无数据时显示自定义的空状态。
- 事件绑定:支持图表的
click
、resize
等事件。 - 灵活扩展:支持动态主题切换、渲染器类型选择(
canvas
或svg
)。 - 生命周期管理:支持图表实例的初始化、销毁、重置等操作。
- 暴露ECharts 实例:可获取ECharts实例,完成更为灵活、复杂的操作。
源码
入口 index.vue:
<template><div ref="rootRef" :class="props.className" :style="rootStyle"></div>
</template><script setup lang="ts">
import {onMounted,ref,watch,computed,defineEmits,defineProps,toRaw,shallowRef,onUnmounted
} from "vue";
import type { SetOptionOpts, ECharts as EChartsInstance } from "echarts/core";
import { echarts, ECOption } from "./core";export interface Props {/*** ECharts 配置项*/option: ECOption | any;/*** 是否有图表数据 (用于显示空状态)*/hasData?: boolean;/*** 根节点类名*/className?: string;/*** 主题名称或主题对象*/theme?: string | Record<string, unknown>;/*** 父容器尺寸变化时自动响应*/autoresize?: boolean;/*** 渲染器类型*/renderer?: "canvas" | "svg";/*** 图表容器宽度*/width?: string;/*** 图表容器高度*/height?: string;/*** 是否展示 Loading*/loading?: boolean;/*** setOption notMerge*/notMerge?: boolean;/*** setOption lazyUpdate*/lazyUpdate?: boolean;/*** setOption replaceMerge*/replaceMerge?: string | string[];/*** setOption 图表联动的 group 名称*/group?: string;
}const props = withDefaults(defineProps<Props>(), {autoresize: true,hasData: true,renderer: "canvas",width: "100%",height: "360px",loading: false,notMerge: false,lazyUpdate: true
});/*** 组件事件* @event ready - ECharts 实例已创建* @event resize - 图表触发了尺寸调整* @event click - 图表点击事件*/
const emit = defineEmits<{(e: "ready", instance: EChartsInstance): void;(e: "resize", size: { width: number; height: number }): void;(e: "click", params: unknown): void;
}>();const rootRef = ref<HTMLDivElement | null>(null);
const chartRef = shallowRef<EChartsInstance | null>(null);
// 监听rootRef容器尺寸变化
const observerRef = ref<ResizeObserver | null>(null);const rootStyle = computed(() => ({width: props.width,height: props.height
}));/*** 获取echarts实例*/
function getInstance(): EChartsInstance | null {return chartRef.value;
}/*** 设置图表配置* @param {ECOption} option - ECharts 配置* @param {SetOptionOpts} [opts] - setOption 选项*/
function setChartOption(option: ECOption, opts?: SetOptionOpts) {if (!chartRef.value) return;chartRef.value.setOption(toRaw(option), {notMerge: props.notMerge,lazyUpdate: props.lazyUpdate,replaceMerge: props.replaceMerge,...opts});
}/*** 触发图表 resize*/
function resize(width?: number, height?: number) {if (!chartRef.value) return;chartRef.value.resize({ width, height });const size = { width: chartRef.value.getWidth(), height: chartRef.value.getHeight() };emit("resize", size);
}/*** 销毁实例*/
function destroy() {if (observerRef.value) {observerRef.value.disconnect();observerRef.value = null;}if (chartRef.value) {chartRef.value.dispose();chartRef.value = null;}
}function showChartLoading() {if (!chartRef.value) return;chartRef.value.showLoading("default", {text: "数据加载中..."// color: "#409EFF",// textColor: "#999",// maskColor: "rgba(255, 255, 255, 0.8)",// zlevel: 0});
}/*** 初始化图表实例*/
function init() {if (!rootRef.value) return;destroy();chartRef.value = echarts.init(rootRef.value, props.theme, { renderer: props.renderer });if (props.group) chartRef.value.group = props.group;// 绑定常用事件chartRef.value.on("click", params => emit("click", params));// 初始配置if (props.option) setChartOption(props.option);if (props.loading) showChartLoading();emit("ready", chartRef.value);if (props.autoresize) {observerRef.value = new ResizeObserver(() => {resize();});observerRef.value.observe(rootRef.value);}
}/*** 监听 option 变化*/
watch(() => props.option,val => {if (!chartRef.value) return;if (val) setChartOption(val);},{ deep: true }
);/*** 监听 loading 和 hasData 变化*/
watch(() => [props.loading, props.hasData],val => {if (!chartRef.value) return;const [_loading, _hasData] = val;// loading 状态if (_loading) {showChartLoading();} else {chartRef.value.hideLoading();}// 空数据状态setChartOption(_hasData? {graphic: {style: {text: ""}}}: {graphic: {type: "text",left: "center",top: "middle",style: {text: "暂无数据",fontSize: 18,fill: "#4b5675"}}});}
);onMounted(init);onUnmounted(destroy);/*** 对外暴露方法* getInstance - 获取 ECharts 实例* setOption - 设置图表配置* resize - 触发图表 resize*/
defineExpose({ getInstance, setOption: setChartOption, resize });
</script><style scoped></style>
core 代码(提供一些基础配置、工具函数等):
// config.ts
// 为了按需引入,使用 ECharts 6 模块化 API
import * as echarts from "echarts/core";
import { CanvasRenderer } from "echarts/renderers";// 常用组件(按需注册)
import {GridComponent,LegendComponent,TitleComponent,TooltipComponent,DatasetComponent,ToolboxComponent,VisualMapComponent,DataZoomComponent,MarkPointComponent,MarkLineComponent,MarkAreaComponent,TimelineComponent,GraphicComponent
} from "echarts/components";// 常用图表(按需注册)
import {BarChart,LineChart,PieChart,ScatterChart,PictorialBarChart,RadarChart,FunnelChart,GaugeChart,HeatmapChart,MapChart,TreemapChart,CandlestickChart,BoxplotChart,SunburstChart,SankeyChart,GraphChart,ParallelChart,ThemeRiverChart,LinesChart,EffectScatterChart
} from "echarts/charts";import type {BarSeriesOption,LineSeriesOption,LinesSeriesOption,PieSeriesOption,ScatterSeriesOption,RadarSeriesOption,GaugeSeriesOption
} from "echarts/charts";
import type {TitleComponentOption,TooltipComponentOption,GridComponentOption,DatasetComponentOption,GraphicComponentOption,VisualMapComponentOption,DataZoomComponentOption,MarkAreaComponentOption,MarkLineComponentOption
} from "echarts/components";
import type { ComposeOption } from "echarts/core";echarts.use([// 渲染器CanvasRenderer,// 组件GridComponent,LegendComponent,TitleComponent,TooltipComponent,DatasetComponent,ToolboxComponent,VisualMapComponent,DataZoomComponent,// MarkPointComponent,MarkLineComponent,MarkAreaComponent,// TimelineComponent,GraphicComponent,// 图表BarChart,LineChart,PieChart,ScatterChart,// PictorialBarChart,// RadarChart,FunnelChart,// GaugeChart,// HeatmapChart,// MapChart,// TreemapChart,// CandlestickChart,// BoxplotChart,// SunburstChart,// SankeyChart,// GraphChart,// ParallelChart,// ThemeRiverChart,LinesChart// EffectScatterChart
]);type ECOption = ComposeOption<| BarSeriesOption| LineSeriesOption| LinesSeriesOption| PieSeriesOption// | RadarSeriesOption// | GaugeSeriesOption| TitleComponentOption| TooltipComponentOption| GridComponentOption| DatasetComponentOption| ScatterSeriesOption| GraphicComponentOption| VisualMapComponentOption| DataZoomComponentOption| MarkAreaComponentOption| MarkLineComponentOption
>;export { echarts, ECOption };// options.ts
/*** 提供图表的一些通用配置选项(可复用) 供业务图表选择使用*//*** x轴 坐标轴刻度标签的相关设置*/
export const xAxisLabel = {color: "#909399",fontSize: 11,rotate: 45// interval: 2 // 每隔2个显示一个标签,避免拥挤
};/*** 网格配置*/
export const grid = {left: 10,right: 70,top: 60,bottom: 80,containLabel: true
};// utils.ts
import { getCssVar } from "@/utils";/*** 图表主题配置*/
export const getChartTheme = () => ({/** 字体大小 */fontSize: 14,/** 字体颜色 */fontColor: getCssVar("--text-color"),/** 主题颜色 */themeColor: getCssVar("--el-color-primary"),/** 颜色组 */color: [getCssVar("--el-color-primary"),"#4ABEFF","#EDF2FF","#14DEBA","#FFAF20","#FA8A6C","#FFAF20"]
});/*** 计算Y轴刻度的函数* 规则:* Y轴:刻度最多5个(包含0),间隔=数据的Y轴最大值/4四舍五入,* 只保留整数位置(例:数据最大值100,那么刻度为0,25,50,75,100)* @param maxValue - 数据的最大值* @returns Y轴刻度数组*/
export function calculateYAxisTicks(maxValue: number): number[] {if (maxValue <= 0) return [0];const interval = maxValue > 1 ? Math.round(maxValue / 4) : +(maxValue / 4).toFixed(2);const ticks: number[] = [];for (let i = 0; i <= 4; i++) {ticks.push(i * interval);}// 确保最大值包含在内if (ticks[ticks.length - 1] !== maxValue) {ticks[ticks.length - 1] = maxValue;}return ticks;
}
📦 安装
确保你已经安装了 Vue 3 和 ECharts6:
npm install echarts
npm install vue
🔧 使用方法
1. 引入组件
<template><Chart:option="chartOption":loading="isLoading":hasData="hasData"@ready="onChartReady"@resize="onChartResize"@click="onChartClick"/>
</template><script setup>
import Chart from './Chart.vue';
import { ref } from 'vue';const chartOption = ref({title: { text: '示例图表' },tooltip: {},xAxis: { type: 'category', data: ['A', 'B', 'C'] },yAxis: { type: 'value' },series: [{ type: 'bar', data: [10, 20, 30] }]
});const isLoading = ref(false);
const hasData = ref(true);function onChartReady(instance) {console.log('图表实例已创建:', instance);
}function onChartResize(size) {console.log('图表尺寸调整:', size);
}function onChartClick(params) {console.log('图表点击事件:', params);
}
</script>
2. Props 说明
参数名 | 类型 | 默认值 | 说明 |
---|---|---|---|
option | ECOption 或 any | 无 | ECharts 的配置项,支持动态更新。 |
hasData | boolean | true | 是否有数据,用于显示空状态。 |
className | string | 无 | 根节点的类名。 |
theme | string 或 Record<string, unknown> | 无 | 图表的主题名称或主题对象。 |
autoresize | boolean | true | 父容器尺寸变化时是否自动触发图表 resize 。 |
renderer | "canvas" 或 "svg" | "canvas" | 渲染器类型,支持 canvas 或 svg 。 |
width | string | "100%" | 图表容器的宽度。 |
height | string | "360px" | 图表容器的高度。 |
loading | boolean | false | 是否显示加载状态。 |
notMerge | boolean | false | setOption 方法的 notMerge 参数,用于是否合并配置项。 |
lazyUpdate | boolean | true | setOption 方法的 lazyUpdate 参数,用于是否延迟更新。 |
replaceMerge | string 或 string[] | 无 | setOption 方法的 replaceMerge 参数,用于指定完全替换的配置部分。 |
group | string | 无 | 图表联动的 group 名称,用于实现多个图表之间的联动。 |
3. 事件说明
事件名 | 参数 | 说明 |
---|---|---|
ready | instance: EChartsInstance | 图表实例创建完成后触发。 |
resize | { width: number; height: number } | 图表触发尺寸调整时触发。 |
click | params: unknown | 图表触发点击事件时触发。 |
4. 方法说明
通过 defineExpose
暴露以下方法,供父组件调用:
方法名 | 参数 | 说明 |
---|---|---|
getInstance | 无 | 获取当前的 ECharts 实例。 |
setOption | option: ECOption | 设置图表配置,支持动态更新。 |
resize | width?: number, height?: number | 手动触发图表的 resize 方法。 |
5. 组件功能示例 demo
<template><div class="page-container page"><h2>通用图表组件演示</h2><div><ElRow><ElCol :span="12"><Chart:option="barOption":loading="loading"height="320px":has-data="hasData"@ready="onReady('bar')"/></ElCol><ElCol :span="12"><Chart :option="lineOption" height="320px" @ready="onReady('line')" /></ElCol></ElRow><Chart :option="barOption2" height="320px" @ready="onReady('line')" /><Chart :option="pieOption" height="320px" @ready="onReady('pie')" /><Chart :option="scatterOption" height="320px" @ready="onReady('scatter')" /></div></div>
</template><script setup lang="ts">
import { ref } from "vue";
import { ElRow, ElCol } from "element-plus";
import Chart from "@/components/Chart/inde.vue";
import { ECOption } from "@/components/Chart/core";
import { getCssVar } from "@/utils";const categories = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
const rand = (min: number, max: number) => Math.round(Math.random() * (max - min) + min);// 模拟数据
const barData = categories.map(() => rand(12, 200));
const lineData = categories.map(() => rand(10, 150));
const scatterData = Array.from({ length: 40 }).map(() => [rand(0, 100), rand(0, 100)]);const loading = ref(true);
const hasData = ref(false);const barOption = ref<ECOption>({});setTimeout(() => {barOption.value = {color: [getCssVar("--el-color-primary")],title: { text: "柱状图" },tooltip: { trigger: "axis" },xAxis: { type: "category", data: categories },yAxis: { type: "value" },series: [{ type: "bar", data: [], label: { show: true } }]};loading.value = false;hasData.value = false;setTimeout(() => {barOption.value.series![0].data = barData;hasData.value = true;}, 2000);
}, 3000);const lineOption = ref({title: { text: "折线图" },tooltip: { trigger: "axis" },xAxis: { type: "category", data: categories },yAxis: { type: "value" },series: [{ type: "line", data: lineData, smooth: true }]
});const pieOption = ref({color: ["#4ABEFF", "#EDF2FF", "#14DEBA", "#FFAF20", "#FA8A6C", "#FFAF20"],title: { text: "饼图" },tooltip: { trigger: "item" },legend: { top: "bottom" },series: [{type: "pie",radius: ["30%", "70%"],roseType: "radius",itemStyle: { borderRadius: 6 },data: categories.map((c, i) => ({ value: rand(20, 100), name: c }))}]
});const scatterOption = ref({title: { text: "散点图" },tooltip: { trigger: "item" },xAxis: {},yAxis: {},series: [{ type: "scatter", data: scatterData }]
});const barOption2 = ref<ECOption>({legend: {},tooltip: {},dataset: {dimensions: ["product", "2015", "2016", "2017"],source: [{ product: "Matcha Latte", 2015: 43.3, 2016: 85.8, 2017: 93.7 },{ product: "Milk Tea", 2015: 83.1, 2016: 73.4, 2017: 55.1 },{ product: "Cheese Cocoa", 2015: 86.4, 2016: 65.2, 2017: 82.5 },{ product: "Walnut Brownie", 2015: 72.4, 2016: 53.9, 2017: 39.1 }]},xAxis: { type: "category" },yAxis: {},// Declare several bar series, each will be mapped// to a column of dataset.source by default.series: [{ type: "bar" }, { type: "bar" }, { type: "bar" }]
});function onReady(name: string) {// 可在此联动、注册事件等
}
</script><style lang="scss" scoped>
.page {padding: 16px;
}
</style>