一、组件式
2、echarts组件
<template>
<div ref="container" :class="className" :style="style"></div>
</template>
<script setup lang="ts">
import {
nextTick,
onMounted,
onUnmounted,
ref,
shallowRef,
watch,
} from 'vue'
import echarts from './lib';
import { CanvasRenderer } from "echarts/renderers";
import type { EChartsOption } from "echarts";
import { getMaxWidth, getActualWidthOfChars, debounce } from './chartUtils'
echarts.use([CanvasRenderer])//渲染模式
const props = withDefaults(defineProps<{
option: EChartsOption,
style?: { height: string, width: string },
className?: string,
containLabel?: boolean,
onClick?: Function,
legendClick?: Function
}>(), {
option: () => ({}),
style: () => ({
height: '100%',
width: '100%'
}),
className: 'bar-chart',
containLabel: false,
})
const container = ref<HTMLElement | null>(null) // 容器元素
const instance = shallowRef<echarts.ECharts | null>(null) // 实例
// 初始化
const initChart = () => {
if (!container.value) return
//查看容器上是否已经挂载了echarts实例 , 如果已挂载就获取并使用该实例,如果未挂载就进行初始化
instance.value = echarts.getInstanceByDom(container.value) || null
if (!instance.value) {
instance.value = echarts.init(container.value)
}
drawChart()
// 侦听图表事件
addListeningChartEvent()
}
// 绘制图表
const drawChart = () => {
// 如果props.option或instance.value不存在,则直接返回
if (!props.option || !instance.value) return
// 是否开启自定义适应规则 目前只针对了type: bar、line 的图表 如果涉及到多y轴的情况请参考
if (props.containLabel) {
const { xAxis, grid, series }: any = props.option
let maxXAxisItem: (string | number)[] = []
if (Array.isArray(xAxis) && xAxis.length > 0 && xAxis) {
let xAxisArr: (number | string)[] = []
xAxis.forEach((el) => xAxisArr = [...xAxisArr, ...el.data])
maxXAxisItem = [xAxisArr[0], xAxisArr[xAxisArr.length - 1]]
} else if (xAxis) {
maxXAxisItem = [xAxis!.data[0], xAxis!.data[xAxis!.data.length - 1]]
}
let leftXAxisWidth = getActualWidthOfChars(maxXAxisItem[0].toString()) || 0
let rightXAxisWidth = getActualWidthOfChars(maxXAxisItem[1].toString()) || 0
let maxYAxisArr: number[] = []
if (Array.isArray(series) && series.length > 0 && series) {
series.forEach((el: any) => maxYAxisArr = [...maxYAxisArr, ...el.data])
} else if (series) {
maxYAxisArr = series.data
}
let leftYAxisWidth = getMaxWidth([Math.min(...maxYAxisArr), Math.max(...maxYAxisArr)])
// 处理过后的option
const newOption = {
...props.option,
textStyle: {
fontFamily: 'Inter, system-ui, Avenir, Helvetica, Arial, sans-serif',
},
grid: {
...grid,
containLabel: false,
left: leftYAxisWidth > leftXAxisWidth / 2 ? leftYAxisWidth : leftXAxisWidth / 2,
right: rightXAxisWidth / 2
}
}
instance.value.setOption(newOption, {
notMerge: true,
})
} else {
instance.value.setOption(props.option, {
notMerge: true,
})
}
}
// 图表自适应
const resize = debounce(() => {
instance.value?.resize({
animation: {
duration: 200,
},
})
}, 100)
// 重绘图表
watch(props, () => {
nextTick(() => {
drawChart()
})
})
// echarts事件绑定
const addListeningChartEvent = () => {
props.onClick && instance.value?.on('click', (e) => {
props.onClick!(e, instance.value, props.option)
})
props.legendClick && instance.value?.on('legendselectchanged', (e) => {
props.legendClick!(e, instance.value, props.option)
console.log();
})
}
onMounted(() => {
nextTick(() => {
initChart()
if (container.value) {
window.addEventListener('resize', resize)
}
})
})
onUnmounted(() => {
instance.value?.dispose()
window.removeEventListener('resize', resize)
})
// 定义要暴露给父组件的方法和属性
defineExpose({
getInstance: () => instance.value,
resize,
drawChart,
})
</script>
3、使用方式
<script setup lang="ts">
import { ref } from 'vue';
import BaseChart from './components/BaseChart.vue'
const option = ref<any>({
xAxis: [
{
type: 'category',
boundaryGap: false,
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
],
grid: {
bottom: '3%',
// containLabel: true
},
yAxis: {
type: 'value',
},
series: [
{
data: [85111112222211, 23, 224, 218, 224, 218, 135, 147, 260],
type: 'line'
}
]
})
const chartRef = ref<InstanceType<typeof BaseChart>>()
</script>
<template>
<div class="container">
<div class="a"></div>
<div class="b">
<BaseChart :option="option" ref="chartRef" :containLabel="true" />
</div>
</div>
</template>
<style scoped>
.container {
width: 100vw;
height: 100vh;
display: flex;
.a {
width: 200px;
height: 200px;
background-color: pink;
}
.b {
flex: 1;
background-color: #454545;
padding: 40px;
box-sizing: border-box;
}
}
</style>
二、函数式
1、hooks封装
import { nextTick, onMounted, onUnmounted, Ref, shallowRef } from "vue";
import { debounce, getActualWidthOfChars, getMaxWidth } from "./chartUtils";
import type { EChartsOption } from "echarts";
import { CanvasRenderer } from "echarts/renderers";
import echarts from './lib';
/**
*
* @param container echarts容器
* @param option
* @param containLabel 是否开启自定义的边距 目前只适配了折线图与柱状图,其余type请自行适配
* @returns
*/
const useEcharts = (
container: Ref<HTMLDivElement>,
option: EChartsOption = {},
containLabel: boolean = true,
) => {
const { theme } = { theme: 'default' };
// 渲染模式
echarts.use([CanvasRenderer]); //渲染模式
const instance = shallowRef<echarts.ECharts | null>(null);//echarts实例
const initChart = () => {
if (!container.value) return;
//查看容器上是否已经挂载了echarts实例 , 如果已挂载就获取并使用该实例,如果未挂载就进行初始化
instance.value = echarts.getInstanceByDom(container.value) || null;
if (!instance.value) {
instance.value = echarts.init(container.value, { theme });
}
setEchartsOption(option);
}
const setEchartsOption = (option: EChartsOption) => {
// 如果props.option或instance.value不存在,则直接返回
if (!option || !instance.value) return;
instance.value.clear()
if (containLabel) {
const { xAxis, grid, series }: any = option;
if (!xAxis || !grid || !series) return;
let maxXAxisItem: (string | number)[] = [];
if (Array.isArray(xAxis) && xAxis.length > 0 && xAxis) {
let xAxisArr: (number | string)[] = [];
xAxis.forEach((el) => xAxisArr = [...xAxisArr, ...el.data]);
maxXAxisItem = [xAxisArr[0], xAxisArr[xAxisArr.length - 1]];
} else if (xAxis) {
maxXAxisItem = [xAxis!.data[0], xAxis!.data[xAxis!.data.length - 1]];
}
let leftXAxisWidth = getActualWidthOfChars(maxXAxisItem[0].toString()) || 0;
let rightXAxisWidth = getActualWidthOfChars(maxXAxisItem[1].toString()) || 0;
let maxYAxisArr: number[] = [];
if (Array.isArray(series) && series.length > 0 && series) {
series.forEach((el: any) => maxYAxisArr = [...maxYAxisArr, ...el.data]);
} else if (series) {
maxYAxisArr = series.data;
}
let leftYAxisWidth = getMaxWidth([Math.min(...maxYAxisArr), Math.max(...maxYAxisArr)]);
// 处理过后的option
const newOption = {
...option,
textStyle: {
fontFamily: 'Inter, system-ui, Avenir, Helvetica, Arial, sans-serif',
},
grid: {
...grid,
containLabel: false,
left: leftYAxisWidth > leftXAxisWidth / 2 ? leftYAxisWidth : leftXAxisWidth / 2,
right: rightXAxisWidth / 2
}
};
instance.value.setOption(newOption, {
notMerge: true,
});
} else {
instance.value.setOption(option, {
notMerge: true,
});
}
hideLoading();
}
// 图表自适应
const resize = debounce(() => {
instance.value?.resize({
animation: {
duration: 200,
},
})
}, 100)
// 加载中
const showLoading = () => instance.value?.showLoading();
// 加载完毕
const hideLoading = () => instance.value?.hideLoading();
onMounted(() => {
nextTick(() => {
initChart();
if (container.value) {
window.addEventListener('resize', resize);
}
})
})
onUnmounted(() => {
instance.value?.dispose();
window.removeEventListener('resize', resize);
})
return {
getInstance: () => instance.value,
setEchartsOption,
showLoading
}
}
export default useEcharts
2、使用方式
<script setup lang="ts">
import { nextTick, onMounted, Ref, ref } from 'vue';
import useEcharts from './components/useEcharts';
const option = ref<any>({
xAxis: [
{
type: 'category',
boundaryGap: false,
data: ['Msssssssssson', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Su11111111111n'],
axisLabel: {
fontSize: 14
}
},
],
grid: {
bottom: '3%',
// containLabel: true
},
yAxis: {
type: 'value',
axisLabel: {
fontSize: 14
}
},
series: [
{
data: [1181111111111, 23, 224, 218, 224, 218, 135, 147, 260],
type: 'line'
}
]
})
// 定义一个ref,用于存储echarts实例的引用
const chartRef = ref<HTMLDivElement | null>(null)
// 使用useEcharts函数,获取setEchartsOption、showLoading和getInstance方法
const { setEchartsOption, showLoading, getInstance } = useEcharts(chartRef as Ref<HTMLDivElement>)
// 在组件挂载后执行
onMounted(() => {
// 在下一个tick中执行
nextTick(() => {
// 显示加载动画
showLoading()
// 设置定时器,1秒后执行
setTimeout(() => {
// 设置echarts的配置项
setEchartsOption(option.value)
// 获取echarts实例,并绑定点击事件
getInstance()?.on('click', (params) => {
console.log(params, '1');
})
}, 1000)
})
})
</script>
<template>
<div class="container">
<div class="a"></div>
<div class="b">
<div ref="chartRef" style="width: 100%; height:100%"></div>
</div>
</div>
</template>
<style scoped>
.container {
width: 100vw;
height: 100vh;
display: flex;
.list {
display: flex;
flex-wrap: wrap;
width: 100%;
height: 100%;
.item {
width: 50px;
height: 50px;
border: 2px solid pink;
box-sizing: border-box;
--n: 5;
/* 一行几个 */
--space: calc(100% - var(--n) * 50px);
/* 一行减去item的宽度后剩下的间距 */
--leftRight: calc(var(--space) / var(--n) / 2);
/* 每个item左右的间距 */
margin: 10px var(--leftRight);
}
}
.a {
width: 200px;
height: 200px;
background-color: pink;
}
.b {
flex: 1;
background-color: #454545;
padding: 40px;
box-sizing: border-box;
}
}
</style>
三、公共函数
1、chartUtils.ts
import { intervalScaleNiceTicks } from 'echarts/lib/scale/helper.js';
interface optionType {
size: number,
family: string
}
const family = 'Inter, system-ui, Avenir, Helvetica, Arial, sans-serif';
const fontSize = 14;
/**
* 三位一个逗号
* @param data
* @param decimalPlaces
* @returns {string}
*/
// 定义一个函数,用于格式化数字
const formatNumber = (data: any, decimalPlaces: number) => {
// 如果数字为0,则直接返回0
if (data == 0) {
return 0;
} else {
// 将数字转换为字符串,并保留指定的小数位数,然后以小数点分割成整数部分和小数部分
const [integerPart, decimalPart] = Number(data)
.toFixed(decimalPlaces)
.split('.');
// 将整数部分每三位添加一个逗号
const formattedIntegerPart = integerPart.replace(
/\B(?=(\d{3})+(?!\d))/g,
','
);
// 如果有小数部分,则添加小数点和小数部分,否则只返回整数部分
const formattedDecimalPart = decimalPart ? '.' + decimalPart : '';
// 返回格式化后的数字
return formattedIntegerPart + formattedDecimalPart;
}
}
/**
* 获取chart 中文本的宽度
* @param text 文本
* @param {*} options 文字样式
* @returns {number}
* */
// 定义一个函数,用于获取字符串的实际宽度
const getActualWidthOfChars = (text: string, options: optionType = { size: fontSize, family: family }) => {
// 从options中获取字体大小和字体类型,如果没有传入,则使用默认值
const { size = 14, family = 'Inter, system-ui, Avenir, Helvetica, Arial, sans-serif' } = options;
// 创建一个canvas元素
let canvas: HTMLCanvasElement = document.createElement('canvas');
// 获取canvas的上下文
const ctx = canvas.getContext('2d');
// 如果上下文存在
if (ctx) {
// 设置字体大小和字体类型
ctx.font = `${size}px ${family}`;
// 获取字符串的度量信息
const metrics = ctx.measureText(text);
// 计算字符串的实际宽度
const actual = Math.abs(metrics.actualBoundingBoxLeft) + Math.abs(metrics.actualBoundingBoxRight);
// canvas = null;
// 返回字符串的最大宽度
return Math.max(metrics.width, actual)
}
}
/**
* 获取chart 图表 y轴数字 最长文本宽度
* @param {*} extend 数据的范围
* @param {*} option 文字样式
* @returns {number}
*/
// 定义一个函数,用于获取最大宽度
const getMaxWidth = (extend: any, option: optionType = { size: fontSize, family: family }) => {
// 定义一个变量,用于存储最大宽度
let LMaxWidth = 0;
// 调用intervalScaleNiceTicks函数,获取刻度范围和间隔
const lTick = intervalScaleNiceTicks(extend, 5);
// 如果刻度范围的最小值小于0,则将最小值减去间隔
const min = lTick.niceTickExtent[0] < 0 ? lTick.niceTickExtent[0] - lTick.interval : lTick.niceTickExtent[0];
// 将刻度范围的最大值加上间隔
const max = lTick.niceTickExtent[1] + lTick.interval;
// 定义一个变量,用于存储最大长度值
let MaxLengthValue = 0
// 遍历刻度范围,获取每个刻度的长度,并更新最大长度值
for (let i = min; i <= max; i += lTick.interval) {
const len = i.toString().length
MaxLengthValue = len < MaxLengthValue.toString().length ? MaxLengthValue : i
}
// 调用getActualWidthOfChars函数,获取最大长度值的实际宽度
LMaxWidth = getActualWidthOfChars(formatNumber(MaxLengthValue, 1).toString(), option) || 0;
// 返回最大宽度
return LMaxWidth;
}
/**
* 防抖
* @param fn 回调函数
* @param delay 延迟时间
*/
const debounce = (fn: Function, delay: number) => {
let timer: ReturnType<typeof setTimeout> | null = null;
return function (...args: any[]) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(args);
}, delay);
};
};
export { getMaxWidth, getActualWidthOfChars, debounce }
2、lib.ts
import * as echarts from 'echarts/core';
import {
BarChart,
LineChart,
PieChart,
MapChart,
PictorialBarChart,
RadarChart,
ScatterChart,
GaugeChart
} from 'echarts/charts';
import {
TitleComponent,
TooltipComponent,
GridComponent,
PolarComponent,
AriaComponent,
ParallelComponent,
LegendComponent,
RadarComponent,
ToolboxComponent,
DataZoomComponent,
VisualMapComponent,
TimelineComponent,
CalendarComponent,
GraphicComponent
} from 'echarts/components';
echarts.use([
LegendComponent,
TitleComponent,
TooltipComponent,
GridComponent,
PolarComponent,
AriaComponent,
ParallelComponent,
BarChart,
LineChart,
PieChart,
MapChart,
GaugeChart,
RadarChart,
PictorialBarChart,
RadarComponent,
ToolboxComponent,
DataZoomComponent,
VisualMapComponent,
TimelineComponent,
CalendarComponent,
GraphicComponent,
ScatterChart
]);
export default echarts;