我报名参加金石计划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>
效果:
这样子,一个基础的图表使用函数就有啦~有些项目中可能需要用到实例的其他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>
ECharts自适应
一般写项目时,代码中还是会以px为主,方便直接复制设计稿上的尺寸,然后通过postcss-pxtorem或postcss-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>
await说明
因为option中也会通过订阅resize事件来重新计算大小。所以如果上面不加await的话,同步执行下去会导致update-theme事件先发布,setOption比option计算提前。
不加await:主题更新->update-theme事件->setOption->option更新
加了await:主题更新->option更新->update-theme事件->setOption。
结尾
这样子,一个简单的echarts自适应和自动播放就完成啦!根据需求的图表再进行相应的完善就能用到项目中去了!✨✨并且在开发时只要复制设计稿的像素就行了,nice!
文本完整代码仓库