ECharts的自动播放和自适应,以vue3为例

523 阅读6分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第3篇文章,点击查看活动详情

如何在vue3中优雅的对ECharts进行自适应和自动播放

ECharts基础使用

引入ECharts

我这边使用的是按需引入(ECharts安装引入介绍)。在src/plugins下新建echarts.js,引入需要用到的图表和组件。

import * as echarts from "echarts/core";
import { BarChart, PieChart, LineChart } from "echarts/charts";
import { TooltipComponent, MarkPointComponent, GridComponent } from "echarts/components";
import { CanvasRenderer, SVGRenderer } from "echarts/renderers";

// 注册必须的组件
echarts.use([BarChart, PieChart, TooltipComponent, CanvasRenderer, SVGRenderer, LineChart, MarkPointComponent, GridComponent]);
export default echarts;

基础使用 useEcharts.js

这个函数用于echarts的基础渲染,当前主要有:创建echarts实例设置实例的option更新option销毁实例重载实例
入参说明:

  • option echarts配置
  • [fnOption] 本函数配置
    • renderer 渲染模式:canvas/svg。一般来说使用svg,内存占用低。 (渲染模式说明
    • initImmediately 是否在mounted立即创建echarts实例
    • chartRef 渲染元素

return:

  • chartObj 实例对象
  • chartRef 渲染元素
  • chartInit 实例创建函数
  • chartShow 图表显示函数
  • chartUpdate 图表更新函数
  • chartReload 图表重载函数
import echarts from "@/plugins/echarts";
import { ref, shallowRef, onMounted, onBeforeUnmount } from "vue";
/**
 * @description: 基础使用echarts
 * @param {*} option echarts option
 * @param {*}  fnOption.renderer svg/canvas
 * @param {*}  fnOption.initImmediately 是否在mounted立即init
 * @param {*} fnOption.chartRef 用来渲染echarts的元素的ref
 * @return {*} {chartObj,chartRef,chartInit,chartShow,chartUpdate,chartReload}
 */
export default (option = {}, fnOption = { renderer: "svg", initImmediately: true, chartRef: ref(null) }) => {
	const { renderer = "svg", initImmediately = true, chartRef = ref(null)} = fnOption;
	const chartObj = shallowRef(null); 
	/**
	 * @description: 载入echarts
	 * @return {*}
	 */
	const init = (themeName = "customed") => {
		if (chartRef.value) {
			chartObj.value = echarts.init(chartRef.value, themeName, { renderer });
		}
	};
	/**
	 * @description: 销毁echarts
	 * @return {*}
	 */
	const dispose = () => {
		if (chartObj.value) chartObj.value?.dispose();
	};
	/**
	 * @description: 设置Option - 创建新的
	 * @return {*}
	 */
	const setOption = () => {
		chartObj.value?.setOption(option, true);
	};
	/**
	 * @description: 更新Option - 合并数据 (普通合并)- 更新 @see https://echarts.apache.org/zh/api.html#echartsInstance.setOption
	 * @return {*}
	 */
	const updateOption = () => {
		chartObj.value?.setOption(option);
	};
	/**
	 * @description: 重载chart。销毁 - init - setoption
	 * @return {*}
	 */
	const chartReload = () => {
		dispose();
		init();
		setOption();
	};
	onMounted(() => {
		if (initImmediately) {
			init();
		}
	});
	onBeforeUnmount(() => {
		dispose();
	});
	return {
		chartObj,
		chartRef,
		chartInit: init,
		chartShow: setOption,
		chartUpdate: updateOption,
		chartReload,
	};
};

在.vue文件中使用:

<template>
	<div class="home">
		<div class="chart" ref="chartRef"></div>
	</div>
</template>

<script setup>
import useEcharts from "@/use/useEcharts";
import { onMounted } from "vue";
const options = {
	xAxis: {
		type: "category",
		data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
	},
	yAxis: {
		type: "value",
	},
	tooltip: {
		trigger: "axis",
		textStyle: {
			fontSize: 16,
		},
	},
	series: [],
};
const { chartShow, chartRef, chartUpdate, chartObj } = useEcharts(options);
onMounted(() => {
	chartShow();
	setTimeout(() => {
		option.series.push({
			data: [150, 230, 224, 218, 135, 147, 260],
			type: "line",
		});
		chartUpdate();
	}, 700);
});
</script>

效果:
image.png
这样子,一个基础的图表使用函数就有啦~有些项目中可能需要用到实例的其他API,得根据具体需求对useEcharts进行修改

自动播放 useEchartsAutoplay.js

一般来说,一张图表如果就那么安静的摆在那总感觉缺少了些什么,所以得让它“动”起来。数据可视化,那就让数据的显示动起来吧💡一个可视化项目中会有很多图表,如果一个个地去写播放会很麻烦。所以这个函数就用于echarts图表实例的自动播放,在需要使用的地方进行引入就行啦!
入参说明:

  • chartObj 图表实例对象
  • [option] 播放参数
    • delay 播放间隔 ms
    • shopTip 播放是否显示提示框
    • showHighlight 播放是否显示高亮
    • customPlayFn 播放时执行的自定义函数

return:

  • activeIndex 当前播放的数据索引
  • isSuspend 是否暂停
  • createAutoPlay 创建自动播放。这个函数的入参是数据组长度,也是播放的一个轮回长度。(不同配置的图表的数据在option中的位置不同,并且会有用于美化图表的占位数据存在,所以需要输入真正显示的数据组长度)
import { computed, onBeforeUnmount, ref } from "vue";

export default (chartObj, option = { delay: 3000, showTip: false, showHighlight: false, customPlayFn: null }) => {
	const { delay = 3000, showTip = false, showHighlight = false, customPlayFn = null } = option;
	let timer = null, // 循环器对象
		maxIndex = 0; // 最大索引
	const activeIndex = ref(0), // 当前播放到的数据索引
		isSuspend = ref(false), // 是否暂停
		preIndex = computed(() => {
			return activeIndex.value - 1 < 0 ? maxIndex : activeIndex.value - 1;
		}), // 上一个播放索引
		/**
		 * @description: 创建自动播放
		 * @param {number} dataLength 数据长度
		 * @return {*}
		 */
		createAutoPlay = (dataLength) => {
			// 如果计时器已存在,清空计时器。防止有多个自动播放存在
			if (timer) {
				clearAutoplay();
			}
			// 最大索引
			maxIndex = dataLength - 1;
			// 设置echarts的鼠标事件 鼠标移入暂停播放 移出继续播放
			setMouseoverEvent();
			autoplayFn();
			timer = setInterval(() => {
				if (!isSuspend.value) {
					activeNext();
					autoplayFn();
				}
			}, delay);
		};
	/**
	 * @description: 播放处理
	 */
	function autoplayFn() {
		if (customPlayFn) {
			customPlayFn();
		}
		showTip && handleShowTip(activeIndex.value);
		if (showHighlight) {
			// 关闭上一个高亮再显示下一个高亮
			handleHideHighlight(preIndex.value);
			handleShowHighlight(activeIndex.value);
		}
	}
	/**
	 * @description: 显示提示框
	 * @param {*} dataIndex
	 * @param {*} seriesIndex
	 */
	function handleShowTip(dataIndex = 0, seriesIndex = 0) {
		chartObj.value?.dispatchAction({
			type: "showTip",
			seriesIndex: seriesIndex,
			playState: true,
			dataIndex: dataIndex,
		});
	}
	/**
	 * @description: 显示高亮
	 * @param {*} dataIndex
	 * @param {*} seriesIndex
	 */
	function handleShowHighlight(dataIndex = 0, seriesIndex = 0) {
		chartObj.value?.dispatchAction({
			type: "highlight",
			playState: true,
			seriesIndex: seriesIndex,
			dataIndex: dataIndex,
		});
	}
	/**
	 * @description: 关闭高亮
	 * @param {*} dataIndex
	 * @param {*} seriesIndex
	 */
	function handleHideHighlight(dataIndex = 0, seriesIndex = 0) {
		chartObj.value?.dispatchAction({
			type: "downplay",
			playState: true,
			seriesIndex: seriesIndex,
			dataIndex: dataIndex,
		});
	}
	/**
	 * @description: 激活下一个
	 * @return {*}
	 */
	function activeNext() {
		activeIndex.value = activeIndex.value + 1 > maxIndex ? 0 : activeIndex.value + 1;
	}
	/**
	 * @description: 设置echarts的鼠标事件
	 * @return {*}
	 */
	function setMouseoverEvent() {
		// 鼠标移入 暂停
		chartObj.value?.on("mouseover", () => {
			isSuspend.value = true;
		});
		// 鼠标移出 不暂停
		chartObj.value?.on("globalout", () => {
			isSuspend.value = false;
		});
	}
	/**
	 * @description: 清除自动播放
	 * @return {*}
	 */
	function clearAutoplay() {
		activeIndex.value = 0;
		clearInterval(timer);
	}
	onBeforeUnmount(() => {
		if (timer) clearInterval(timer);
	});
	return {
		activeIndex,
		isSuspend,
		createAutoPlay,
	};
};

这个函数简单的实现了创建自动播放播放时显示提示框/高亮卸载时清除播放鼠标移入移出时停止播放/继续播放。(不同配置的图表的实现提示框或高亮或其他行为可能不一样,可以根据具体需求来考虑是将行为添加在useEchartsAutoplay.js中或以customPlayFn入参)

<script setup>
import useEcharts from "@/use/useEcharts";
import useEchartsAutoplay from "@/use/useEchartsAutoplay";
const options = {
  .....
};
const { chartShow, chartRef, chartUpdate, chartObj } = useEcharts(options);
const { createAutoPlay } = useEchartsAutoplay(chartObj, { showTip: true });
onMounted(() => {
	chartShow();
	setTimeout(() => {
		option.series.push({
			data: [150, 230, 224, 218, 135, 147, 260],
			type: "line",
		});
		chartUpdate();
    // 创建自动播放 入参为数据长度
    createAutoPlay(7);
	}, 700);
});
</script>

4621AEB0-5875-490D-A0C8-FCAB40DEC22F-4309-00000476B1106271.gif

ECharts自适应

一般写项目时,代码中还是会以px为主,方便直接复制设计稿上的尺寸,然后通过postcss-pxtorempostcss-px-to-viewport转成rem/vw。但是呢,echarts中的尺寸配置没法通过这种方式转。如果为了自适应要在每个配置中写转换的话这也太太太麻烦了🙉,而且有些属性不支持rem/vw单位,如果有resize需求的话还得写监听,天哪,想想就头大。为了提高开发舒适度🐟,我得想法子把这些麻烦的东西都封在一起💡,不要在我写业务的时候出现,在开发时直接使用设计稿的尺寸就好☆´∀`☆☕️ 既然要使用设计稿尺寸开发,那么为了在环境中自适应,需要先算出当前的最小缩放%。 我先在public/config.js中定义了一个全局变量。这个文件会在html头部引入。

let WIN_SCALE = 1;

创建一个rem.js(用vw的话可以去掉rem相关部分),简单的计算了下,设计稿为1920*1080,在main.js中引入该文件:

import { debounce } from "lodash-es";
import { appEventbus } from "./EventBus";
// 基准大小
const baseSize = 16;
// 设置 rem 函数
function setRem() {
	const scaleWidth = document.documentElement.clientWidth / 1920;
	const scaleHeight = document.documentElement.clientHeight / 1080;
	WIN_SCALE = Math.min(scaleWidth, scaleHeight);
	// 设置页面根节点字体大小, 字体大小最小为12
	let fontSize = baseSize * WIN_SCALE > 12 ? baseSize * WIN_SCALE : 12;
	document.documentElement.style.fontSize = fontSize + "px";
}
//初始化
setRem();
//改变窗口大小时重新设置
const handleResize = debounce(function () {
	setRem();
	appEventbus.$emit("resize");
}, 1000);
window.onresize = handleResize;

上面的rem.js中引入了一个EventBus,用于事件发布和订阅。在window.onresize(记得加防抖)的时候重新计算WIN_SCALE以及发布resize事件。至此,我们就拥有了当前的WIN_SCALE,以及resize事件的发布。只需要在关键点订阅resize事件然后通过WIN_SCALE计算当前大小。 计算嘛,当然是要写一个通用函数啦:

export function setIntSize(val) {
	const res = parseInt(val * WIN_SCALE);
	return res === 0 ? 1 : res;
}

通过主题实现自适应 useEchartsTheme

ECharts可以注册主题,主题中可以定制图表中元素大小。那么我们就可以直接在主题配置中计算自适应后的大小,然后注册到ECharts。在resize事件发布的时候重新计算并再次注册。(也可以直接更改主题对象里的属性值,不跑注册操作也行,但是我懒得写那么多计算,我选择从头再来一遍🌝)
创建文件 useEchartsTheme.js

import { setIntSize } from "@/libs/utils";
export default () => {
	const px12 = setIntSize(12),
		px14 = setIntSize(14),
		px2 = setIntSize(2),
		px16 = setIntSize(16),
		px1 = setIntSize(1),
		px10 = setIntSize(10),
		px8 = setIntSize(8);
	return {
		color: ["#07C3FF", "#FFB821", "#FF6A98", "#F2877F", "#958EED", "#FFA5D6", "#fc8452", "#9a60b4", "#ea7ccc"],
		backgroundColor: "rgba(0, 0, 0, 0)",
		textStyle: {},
		bar: {
			itemStyle: {
				barBorderWidth: "0",
				barBorderColor: "#ccc",
			},
			barMaxWidth: px16,
		},
		pie: {
			itemStyle: {
				borderWidth: "0",
				borderColor: "#ccc",
			},
			label: {
				fontSize: px14,
			},
		},
		// 类目轴
		categoryAxis: {
			// 轴线
			axisLine: {
				show: true,
				lineStyle: {
					color: "#D9E7FF",
					width: px2,
				},
			},
			// 刻度线
			axisTick: {
				show: false,
			},
			axisLabel: {
				show: true,
				color: "#D9E7FF",
				fontSize: px12,
			},
			splitLine: {
				show: false,
			},
		},
		// 数值轴
		valueAxis: {
			// 轴线
			axisLine: {
				show: false,
			},
			// 刻度线
			axisTick: {
				show: false,
			},
			axisLabel: {
				show: true,
				color: "rgba(217,231,255,0.85)",
				fontSize: px12,
			},
			splitLine: {
				show: true,
				lineStyle: {
					color: ["#D9E7FF"],
					opacity: "0.3",
					width: px2,
				},
			},
			nameTextStyle: {
				color: "rgba(217,231,255,0.85)",
				fontSize: px12,
			},
		},
		// 图例
		legend: {
			itemGap: px8,
			itemWidth: px8,
			itemHeight: px8,
			textStyle: {
				color: "#fff",
				fontSize: px14,
			},
			lineStyle: {
				width: px2,
			},
		},
		// 提示框
		tooltip: {
			axisPointer: {
				lineStyle: {
					color: "#ccc",
					width: px1,
				},
				crossStyle: {
					color: "#ccc",
					width: px1,
				},
			},
			textStyle: {
				color: "#D9E7FF",
				fontSize: px12,
			},
			backgroundColor: "rgba(3, 41, 55, 0.8)",
			borderColor: "#4ba9f5",
			padding: [px10, px10],
			borderWidth: px1,
			extraCssText: `border-radius:${px8}px;`,
		},
	};
};

在引入ECharts的文件中使用plugins/echarts.js:

import * as echarts from "echarts/core";
import { BarChart, PieChart, LineChart } from "echarts/charts";
import { TooltipComponent, MarkPointComponent, GridComponent } from "echarts/components";
import { CanvasRenderer, SVGRenderer } from "echarts/renderers";
import useEchartsTheme from "@/use/useEchartsTheme";
import { appEventbus } from "@/libs/EventBus";

// 注入自定义主题
echarts.registerTheme("customed", useEchartsTheme());
// 注册必须的组件
echarts.use([BarChart, PieChart, TooltipComponent, CanvasRenderer, SVGRenderer, LineChart, MarkPointComponent, GridComponent]);
async function handleResize() {
	await echarts.registerTheme("customed", useEchartsTheme());
	appEventbus.$emit("update-theme");
}
appEventbus.$on("resize", handleResize);
export default echarts;

为什么加await?因为我希望等其他同步的resize订阅执行完之后,再发布update-theme事件。为什么要这么做呢,后面会说~
这样子,echarts主题就更新完了,接下去只要重载echarts就行了!
修改useEcharts.js,添加对update-theme事件的订阅。当update-theme事件发布时,会先销毁实例,然后创建实例,设置option。

import { appEventbus } from "@/libs/EventBus";
export default (option = {}, fnOption = { renderer: "svg", initImmediately: true, chartRef: ref(null), resize: true }) => {
	/**
	 * @description: 重载chart。销毁 - init - setoption
	 * @return {*}
	 */
	const chartReload = () => {
		dispose();
		init();
		setOption();
	};
  ...
	appEventbus.$on("update-theme", () => {
		chartReload();
	});
	return {
    ...
	};
};

option中的自适应 useSelfAdaptionObject

option中的自适应实现,我是封装了一个自适应对象函数:
入参:

  • optionObj option对象
  • sizeProp 需要计算的属性path数组

返回:

  • optionObj option对象

init() 用于初始化计算(窗口打开时的计算),handleResize 用于处理窗口大小变换的计算。主要思路就是将原始值存在Map中,然后直接计算修改optionObj对象属性值。

import { appEventbus } from "@/libs/EventBus";
import { setIntSize } from "@/libs/utils";
import { has, get, set } from "lodash-es";
export default (optionObj, sizeProp = []) => {
	const originalSizeMap = new Map();
	function init() {
		console.log("useEchartsOption -- init");
		sizeProp.forEach((item) => {
			if (has(optionObj, item)) {
				originalSizeMap.set(item, get(optionObj, item));
				set(optionObj, item, setIntSize(get(optionObj, item)));
			}
		});
	}
	function handleResize() {
		console.log("useEchartsOption -- handleResize");
		sizeProp.forEach((item) => {
			if (originalSizeMap.has(item) && has(optionObj, item)) {
				set(optionObj, item, setIntSize(originalSizeMap.get(item)));
			}
		});
	}
	appEventbus.$on("resize", handleResize);
	init();
	return optionObj;
};

使用:

<template>
	<div class="home">
		<div class="chart" ref="chartRef"></div>
	</div>
</template>

<script setup>
import useEcharts from "@/use/useEcharts";
import useEchartsAutoplay from "@/use/useEchartsAutoplay";
import useSelfAdaptionObject from "@/use/useSelfAdaptionObject";
import { onMounted } from "vue";
const options = {
	xAxis: {
		type: "category",
		data: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
	},
	yAxis: {
		type: "value",
	},
	tooltip: {
		trigger: "axis",
		textStyle: {
			fontSize: 16,
		},
	},
	series: [],
};
const option = useSelfAdaptionObject(options, ["tooltip.textStyle.fontSize"]);
const { chartShow, chartRef, chartUpdate, chartObj } = useEcharts(option);
const { createAutoPlay } = useEchartsAutoplay(chartObj, { showTip: true });
onMounted(() => {
	chartShow();
	setTimeout(() => {
		option.series.push({
			data: [150, 230, 224, 218, 135, 147, 260],
			type: "line",
			markPoint: useSelfAdaptionObject(
				{
					data: [
						{ type: "max", name: "Max" },
						{ type: "min", name: "Min" },
					],
					symbolSize: 50,
					label: {
						fontSize: 16,
					},
				},
				["label.fontSize", "symbolSize"]
			),
		});

		chartUpdate();
		createAutoPlay(7);
	}, 700);
});
</script>

EC514ABA-E175-4BAF-AF1B-7AB9DC6CC730-4309-000008ECFE0C8532.gif

await说明

因为option中也会通过订阅resize事件来重新计算大小。所以如果上面不加await的话,同步执行下去会导致update-theme事件先发布,setOption比option计算提前。
不加await:主题更新->update-theme事件->setOption->option更新
加了await:主题更新->option更新->update-theme事件->setOption。

结尾

这样子,一个简单的echarts自适应和自动播放就完成啦!根据需求的图表再进行相应的完善就能用到项目中去了!✨✨并且在开发时只要复制设计稿的像素就行了,nice!
文本完整代码仓库