components/WEchartsVue.vue

<template>
    <!-- ref=$self為供外部存取繪圖容器之用; 寬高由外部以style給予 -->
    <div ref="$self" class="w-echarts-vue"></div>
</template>

<script>
import { throttle } from 'echarts/core'
import iniEcharts from '../js/iniEcharts.mjs'


//echarts, 直接引用echarts與echarts-gl不使用cdn
//注意: 本組件直接以echarts原生API實作生命週期(init, setOption, resize, dispose)與事件轉發,
//不使用vue-echarts, 因其runtime依賴vue-demi, 而vue-demi出廠為Vue3模式須靠postinstall切換,
//npm>=12/pnpm>=10/bun預設封鎖依賴套件install scripts, 下游以src方式引用本套件時會靜默停在Vue3模式而壞掉;
//直接實作後全依賴樹零install script, 下游任意套件管理器與打包器皆零設定可用
let echarts = iniEcharts()


/**
 * echarts繪圖組件, 已內含echarts全量圖表與組件, 以及echarts-gl之3D圖表(bar3D, line3D, scatter3D, surface等), 不須自cdn額外引用
 *
 * 注意: 組件本身不帶預設高度, 須由外部給予寬高(例如style="width:620px; height:300px;"), 否則因高度為0而看不見圖形
 *
 * @vue-prop {Object} [options={}] 輸入echarts設定物件,即echarts之option,預設{}。變更時自動重繪:給予新物件(參照改變)採整體置換(notMerge),原物件內變更(mutation)採合併更新,與vue-echarts語意一致
 * @vue-prop {String|Object} [theme=''] 輸入echarts主題字串或主題設定物件,字串為已使用echarts.registerTheme註冊之主題名稱,變更時會自動銷毀重建圖表,預設''
 * @vue-prop {Object} [initOptions={}] 輸入echarts初始化設定物件,即echarts.init之opts,可設定renderer('canvas'或'svg')、locale、devicePixelRatio等,變更時會自動銷毀重建圖表,預設{}
 * @vue-prop {Object} [updateOptions={}] 輸入echarts更新設定物件,即echarts之setOption之opts,可設定notMerge、replaceMerge、lazyUpdate等,預設{}
 * @vue-prop {String} [group=''] 輸入圖表群組名稱字串,供echarts.connect跨圖表連動使用,預設''
 * @vue-prop {Boolean} [manualUpdate=false] 輸入是否關閉options之深度監聽布林值,若為true則變更options不會自動重繪,須自行取得實例呼叫setOption更新,適用於資料量龐大時,預設false
 * @vue-prop {Boolean} [loading=false] 輸入是否顯示讀取中動畫布林值,預設false
 * @vue-prop {Object} [loadingOptions={}] 輸入讀取中動畫設定物件,即echarts之showLoading之opts,可設定text、color、maskColor等,預設{}
 * @vue-prop {Boolean|Object} [autoresize=true] 輸入是否於容器尺寸改變時自動重繪布林值,亦可給物件設定{throttle,onResize},throttle為節流毫秒數預設100,預設true
 * @vue-event {Object} * 對外轉發echarts之全部事件,例如click、mouseover、legendselectchanged、datazoom、finished等,回傳事件參數物件;zrender事件以zr:前綴綁定,例如@zr:click
 */
export default {
    props: {
        options: {
            type: Object,
            default: () => {},
        },
        theme: {
            type: [String, Object],
            default: () => '',
        },
        initOptions: {
            type: Object,
            default: () => {},
        },
        updateOptions: {
            type: Object,
            default: () => {},
        },
        group: {
            type: String,
            default: () => '',
        },
        manualUpdate: {
            type: Boolean,
            default: () => false,
        },
        loading: {
            type: Boolean,
            default: () => false,
        },
        loadingOptions: {
            type: Object,
            default: () => {},
        },
        autoresize: {
            type: [Boolean, Object],
            default: () => true,
        },
    },
    data: function() {
        //chart與resizeObserver為echarts實例與瀏覽器原生物件, 不可放入data,
        //否則Vue會對其深層響應化(遞迴defineProperty), 造成效能問題, 故於init時直接掛this成非響應式屬性
        return {
        }
    },
    mounted: function() {
        //console.log('mounted')

        let vo = this

        //init
        vo.init()

    },
    beforeDestroy: function() {
        //console.log('beforeDestroy')

        let vo = this

        //clear
        vo.clear()

    },
    watch: {

        options: {
            deep: true,
            handler: function(nv, ov) {
                let vo = this

                //manualUpdate, 關閉自動重繪
                if (vo.manualUpdate) {
                    return
                }

                //check
                if (!vo.chart) {
                    return
                }

                //setOption, 參照改變(給予新物件)採整體置換, 原物件內變更採合併更新, 與vue-echarts語意一致
                vo.chart.setOption(nv || {}, { notMerge: nv !== ov, ...vo.updateOptions })

            },
        },

        theme: function() {
            this.reinit()
        },

        initOptions: {
            deep: true,
            handler: function() {
                this.reinit()
            },
        },

        group: function(nv) {
            let vo = this
            if (vo.chart) {
                vo.chart.group = nv || ''
            }
        },

        loading: function() {
            this.updateLoading()
        },

        loadingOptions: {
            deep: true,
            handler: function() {
                this.updateLoading()
            },
        },

        autoresize: function() {
            let vo = this
            vo.clearResize()
            vo.bindResize()
        },

    },
    computed: {
    },
    methods: {

        /**
         * 初始化圖表, 建立echarts實例並綁定事件, 讀取中動畫與尺寸監聽(內部使用)
         */
        init: function() {
            let vo = this

            //el
            let el = vo.$refs['$self']
            if (!el) {
                return
            }

            //init
            let chart = echarts.init(el, vo.theme || null, vo.initOptions || {})

            //chart, 掛this成非響應式屬性
            vo.chart = chart

            //group
            if (vo.group) {
                chart.group = vo.group
            }

            //setOption
            chart.setOption(vo.options || {}, vo.updateOptions || {})

            //事件轉發, 由$listeners綁定echarts事件, zr:前綴綁定zrender事件
            Object.keys(vo.$listeners || {}).forEach((key) => {

                //k, 剝除Vue事件修飾前綴(~為once, &為passive, !為capture)
                let k = key.replace(/^[~!&]+/, '')

                //fn
                let fn = vo.$listeners[key]

                //on
                if (k.indexOf('zr:') === 0) {
                    chart.getZr().on(k.slice(3), fn)
                }
                else {
                    chart.on(k, fn)
                }

            })

            //updateLoading
            vo.updateLoading()

            //bindResize
            vo.bindResize()

        },

        /**
         * 銷毀並重建圖表, 供theme或initOptions變更時使用(內部使用)
         */
        reinit: function() {
            let vo = this

            //clear
            vo.clear()

            //init
            vo.init()

        },

        /**
         * 更新讀取中動畫(內部使用)
         */
        updateLoading: function() {
            let vo = this

            //check
            if (!vo.chart) {
                return
            }

            //showLoading, hideLoading
            if (vo.loading) {
                vo.chart.showLoading('default', vo.loadingOptions || {})
            }
            else {
                vo.chart.hideLoading()
            }

        },

        /**
         * 綁定容器尺寸監聽, 尺寸改變時自動重繪(內部使用)
         */
        bindResize: function() {
            let vo = this

            //check
            if (!vo.autoresize || !vo.chart) {
                return
            }

            //el
            let el = vo.$refs['$self']
            if (!el) {
                return
            }

            //wait, onResize
            let opt = (vo.autoresize === true) ? {} : vo.autoresize
            let wait = Number.isFinite(opt.throttle) ? opt.throttle : 100
            let onResize = opt.onResize

            //callback, 重繪並通知外部
            let callback = () => {
                if (!vo.chart) {
                    return
                }
                vo.chart.resize()
                if (typeof onResize === 'function') {
                    onResize()
                }
            }
            if (wait > 0) {
                callback = throttle(callback, wait)
            }

            //resizeObserver, 掛this成非響應式屬性, 首次observe必觸發一次callback故跳過
            let bFirst = true
            vo.resizeObserver = new ResizeObserver(() => {
                if (bFirst) {
                    bFirst = false
                    return
                }
                callback()
            })
            vo.resizeObserver.observe(el)

        },

        /**
         * 解除容器尺寸監聽(內部使用)
         */
        clearResize: function() {
            let vo = this

            //disconnect
            if (vo.resizeObserver) {
                vo.resizeObserver.disconnect()
                vo.resizeObserver = null
            }

        },

        /**
         * 銷毀圖表與監聽(內部使用)
         */
        clear: function() {
            let vo = this

            //clearResize
            vo.clearResize()

            //dispose
            if (vo.chart) {
                vo.chart.dispose()
                vo.chart = null
            }

        },

        /**
         * 取得內部echarts實例, 供外部呼叫echarts原生方法(setOption, resize, getDataURL, dispatchAction等)之用
         * 因echarts實例於組件掛載後才建立, 故初始化前呼叫會回傳null
         *
         * @returns {Object|null} 回傳echarts實例物件, 若尚未初始化則回傳null
         */
        getChart: function() {
            let vo = this
            return vo.chart || null
        },

    },
}
</script>

<style scoped>
.w-echarts-vue {
    display: block;
}
</style>