專案中用到了很多echart圖表,進行了簡單的元件封裝,主要包含以下功能:
- 建立圖表例項,渲染圖表
- 支援傳入自定義函式,可拿到圖表例項,實現個性化功能
- 支援配置更新後圖表自動重新整理,可配置是清空後再重新整理
- loading狀態控制
- resize時圖表更新
- 支援餅圖預設高亮功能
實現
資源引入
- echart資源按需引入
- 第三方元件引入(echarts-liquidfill,水波紋圖表)
/* 即下文中的 @/modules/echartPlugin */
// https://echarts.apache.org/handbook/zh/basics/import#%E6%8C%89%E9%9C%80%E5%BC%95%E5%85%A5-echarts-%E5%9B%BE%E8%A1%A8%E5%92%8C%E7%BB%84%E4%BB%B6
import * as echarts from "echarts/core";
import {
BarChart,
// 系列型別的定義字尾都為 SeriesOption
BarSeriesOption,
PieChart,
PieSeriesOption,
LineChart,
LineSeriesOption,
LinesChart,
LinesSeriesOption,
EffectScatterChart,
EffectScatterSeriesOption,
} from "echarts/charts";
import {
TitleComponent,
// 元件型別的定義字尾都為 ComponentOption
TitleComponentOption,
TooltipComponent,
TooltipComponentOption,
DatasetComponent,
DatasetComponentOption,
GridComponent,
GridComponentOption,
DataZoomComponent,
DataZoomComponentOption,
LegendComponent,
LegendComponentOption,
GeoComponent,
GeoComponentOption,
} from "echarts/components";
import { CanvasRenderer } from "echarts/renderers";
import "echarts-liquidfill";
// 透過 ComposeOption 來組合出一個只有必須元件和圖表的 Option 型別
export type ECOption = echarts.ComposeOption<
| BarSeriesOption
| TitleComponentOption
| TooltipComponentOption
| GridComponentOption
| DatasetComponentOption
| DataZoomComponentOption
| PieSeriesOption
| LegendComponentOption
| GeoComponentOption
| LinesSeriesOption
| LineSeriesOption
| EffectScatterSeriesOption
>;
// https://www.npmjs.com/package/echarts-liquidfill
export interface LiquidFillOption {
series: {
type: "liquidFill";
data: number[];
color?: string[];
radius?: string;
center?: [string, string];
label?: {
color?: string;
insideColor?: string;
fontSize?: number;
formatter?: (param: {
borderColor: string;
color: string;
data: number;
dataIndex: number;
dataType: undefined;
name: string;
value: number;
}) => string | number;
};
shape?:
| "circle"
| "rect"
| "roundRect"
| "triangle"
| "diamond"
| "pin"
| "arrow";
[name: string]: unknown;
}[];
[name: string]: unknown;
}
// 註冊必須的元件
echarts.use([
TitleComponent,
TooltipComponent,
GridComponent,
BarChart,
LinesChart,
CanvasRenderer,
DatasetComponent,
DataZoomComponent,
PieChart,
LegendComponent,
GeoComponent,
LineChart,
EffectScatterChart,
]);
export default echarts;
元件封裝
<template>
<div class="h-echart-wrapper" ref="chartWrapperDom">
<div class="h-echart" ref="chartDom">loading</div>
</div>
</template>
<script lang="ts" src="./index.ts"></script>
<style lang="less" scoped>
.h-echart-wrapper {
height: 100%;
}
.h-echart {
height: 100%;
width: 100%;
text-align: center;
}
</style>
import {
defineComponent,
onMounted,
onUnmounted,
PropType,
ref,
watch,
toRaw,
} from "vue";
import echarts, { ECOption, LiquidFillOption } from "@/modules/echartPlugin";
import ResizeObserver from "resize-observer-polyfill";
export default defineComponent({
name: "h-echart",
props: {
// echart配置
options: {
type: Object as PropType<ECOption | LiquidFillOption>,
required: true,
},
// 餅圖是否需要預設高亮
needDefaultHighLight: {
type: Boolean,
default: false,
},
loading: Boolean,
// 自定義函式,會暴露echart例項出去,可以實現個性化操作
customFn: Function as PropType<
(echartInstance: null | echarts.ECharts) => void
>,
// 更新圖表之前是否先清空
clearBeforeUpdate: Boolean,
},
setup(props) {
const chartWrapperDom = ref<null | HTMLElement>(null);
const chartDom = ref<null | HTMLElement>(null);
// WARN: echarts5 例項用響應式物件存放時會導致功能tooltip功能異常
let echartInstance: null | echarts.ECharts = null;
let chartWrapperResize: null | ResizeObserver = null;
let highlightName: string | null = null;
let firstRender = true;
const setOptions = (options?: ECOption | LiquidFillOption) => {
echartInstance &&
options &&
echartInstance.setOption(toRaw(options), {
notMerge: true,
});
if (props.needDefaultHighLight && firstRender) {
firstRender = false;
const _options = props.options as ECOption;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (_options.series && _options.series[0] && _options.series[0].data) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const name = _options.series[0].data[0].name as string;
setTimeout(() => {
// 預設高亮
echartInstance &&
echartInstance.dispatchAction({
type: "highlight",
seriesIndex: 0,
name,
});
highlightName = name;
}, 600);
}
}
};
watch(
() => props.loading,
(newLoading) => {
if (newLoading !== undefined && echartInstance) {
newLoading
? echartInstance.showLoading({
textColor: "rgb(255 255 255 / 0%)",
showSpinner: false,
zlevel: 0,
})
: echartInstance.hideLoading();
}
}
);
const init = () => {
chartDom.value && (echartInstance = echarts.init(chartDom.value));
props.customFn && props.customFn(echartInstance);
if (props.needDefaultHighLight && echartInstance) {
echartInstance.on("mouseover", function (e) {
if (e.name !== highlightName) {
echartInstance!.dispatchAction({
type: "downplay",
seriesIndex: 0,
name: highlightName,
});
}
});
echartInstance.on("mouseout", function (e) {
highlightName = e.name;
echartInstance!.dispatchAction({
type: "highlight",
seriesIndex: 0,
name: e.name,
});
});
}
setOptions(props.options);
};
onMounted(() => {
// 初始化圖表例項
setTimeout(init, 300);
// 觀察包裹層變化,進行圖表resize
if (chartWrapperDom.value) {
chartWrapperResize = new ResizeObserver(() => {
echartInstance && echartInstance.resize();
});
chartWrapperResize.observe(chartWrapperDom.value);
}
});
// 觀察者清理
onUnmounted(() => {
chartWrapperResize?.disconnect();
});
watch(
() => props,
// 配置變化,重新設定
(newVal) => {
if (newVal.clearBeforeUpdate) {
echartInstance && echartInstance.clear();
}
setOptions(toRaw(newVal.options));
},
{ immediate: true, deep: true }
);
return {
chartDom,
chartWrapperDom,
};
},
});
元件註冊及全域性型別宣告
/* ./components/index.ts */
import { App } from "vue";
import HEchart from "./h-echart";
import HIframeKeepAlive from "./h-iframe-keep-alive/index.vue";
export default function useCompoments(app: App<Element>) {
app &&
app.component &&
[
HEchart,
HIframeKeepAlive,
].forEach((_component) => {
app.component(_component.name, _component);
});
}
// 宣告全域性元件型別
// https://github.com/johnsoncodehk/volar/blob/master/extensions/vscode-vue-language-features/README.md
declare module "@vue/runtime-core" {
export interface GlobalComponents {
HEchart: typeof HEchart;
HIframeKeepAlive: typeof HIframeKeepAlive;
}
}
import useCompoments from "./components";
const app = createApp(App).use(router);
tempApp = app;
// 註冊所自定義元件
useCompoments(app);
使用
<div class="chart-wrapper">
<h-echart :options="boardPieOptions" needDefaultHighLight />
</div>
const boardPieOptions = computed(() => {
return getBoardPieOptions(props.arrivalNodeStats.types);
});