当前位置: 首页 > ops >正文

前端数据可视化:基于Vue3封装 ECharts 的最佳实践

ECharts Vue 组件

这是一个基于 Vue 3 和 ECharts 的可复用图表组件,支持常见的图表功能,包括动态配置、自动调整大小、加载状态、空数据提示等。


🚀 功能特性

  • ECharts 配置:支持传入完整的 ECharts 配置项 (option)。
  • 动态数据更新:通过监听 option 的变化,动态更新图表。
  • 自动调整大小:支持父容器尺寸变化时自动触发图表 resize
  • 加载状态:支持显示加载状态。
  • 空数据提示:支持无数据时显示自定义的空状态。
  • 事件绑定:支持图表的 clickresize 等事件。
  • 灵活扩展:支持动态主题切换、渲染器类型选择(canvassvg)。
  • 生命周期管理:支持图表实例的初始化、销毁、重置等操作。
  • 暴露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 说明

参数名类型默认值说明
optionECOptionanyECharts 的配置项,支持动态更新。
hasDatabooleantrue是否有数据,用于显示空状态。
classNamestring根节点的类名。
themestringRecord<string, unknown>图表的主题名称或主题对象。
autoresizebooleantrue父容器尺寸变化时是否自动触发图表 resize
renderer"canvas""svg""canvas"渲染器类型,支持 canvassvg
widthstring"100%"图表容器的宽度。
heightstring"360px"图表容器的高度。
loadingbooleanfalse是否显示加载状态。
notMergebooleanfalsesetOption 方法的 notMerge 参数,用于是否合并配置项。
lazyUpdatebooleantruesetOption 方法的 lazyUpdate 参数,用于是否延迟更新。
replaceMergestringstring[]setOption 方法的 replaceMerge 参数,用于指定完全替换的配置部分。
groupstring图表联动的 group 名称,用于实现多个图表之间的联动。

3. 事件说明

事件名参数说明
readyinstance: EChartsInstance图表实例创建完成后触发。
resize{ width: number; height: number }图表触发尺寸调整时触发。
clickparams: unknown图表触发点击事件时触发。

4. 方法说明

通过 defineExpose 暴露以下方法,供父组件调用:

方法名参数说明
getInstance获取当前的 ECharts 实例。
setOptionoption: ECOption设置图表配置,支持动态更新。
resizewidth?: 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>
http://www.xdnf.cn/news/19437.html

相关文章:

  • Prometheus Alertmanager 告警组件学习
  • GD32F303在移植FreeRTOS时,系统卡死在Systick_Handler B.的处理方法
  • 164.在 Vue3 中使用 OpenLayers 加载 Esri 地图(多种形式)
  • 后端Web实战-多表操作员工列表查询
  • Spring Bean生命周期的完全指南
  • 面试常考css:三列布局实现方式
  • Interceptor拦截器入门知识及其工作原理
  • 虚拟化技术是什么?电脑Bios中的虚拟化技术怎么开启
  • S32K3平台FEE 应用笔记
  • C++ 多线程实战 01|为什么需要线程:并发 vs 并行,进程 vs 线程
  • 6 种可行的方法:小米手机备份到电脑并恢复
  • js语言编写科技风格博客网站-详细源码
  • AI-调查研究-66-机器人 机械臂 软件算法体系:轨迹规划·视觉定位·力控策略
  • 网络层和数据链路层
  • 智能对话系统优化方案:解决响应偏差与个性化缺失问题
  • OpenStack网络类型解析
  • 超越Transformer:语言模型未来的认知革命与架构重构
  • 手写MyBatis第47弹:Interceptor接口设计与Invocation上下文传递机制--MyBatis动态代理生成与方法拦截的精妙实现
  • uniApp 混合开发全指南:原生与跨端的协同方案
  • shell编程基础入门-3
  • Ansible之playbook剧本
  • 【Spark Core】(三)RDD的持久化
  • nrf52840 解锁
  • Linux部署OSM本地服务测试环境
  • Ubuntu 25.10 Snapshot4 发布。
  • 电动两轮车手机导航投屏方案调研报告
  • 「日拱一码」076 深度学习——自然语言处理NLP
  • SOME/IP-SD中IPv4端点选项与IPv4 SD端点选项
  • Coze源码分析-工作空间-资源库-前端源码
  • 掌握正则表达式与文本处理:提升 Shell 编程效率的关键技巧