<template>
<!-- ref=$self為供外部存取繪圖容器之用; 寬高由外部以style給予 -->
<div ref="$self" class="w-echarts-vue" v-domresize="resizeOpt" @domresize="onDomResize"></div>
</template>
<script>
import domResize from 'w-component-vue/src/js/domResize.mjs'
import iniEcharts from '../js/iniEcharts.mjs'
import parseAutoresize from '../js/parseAutoresize.mjs'
import isPropUnchanged from '../js/isPropUnchanged.mjs'
import getContainerSize from '../js/getContainerSize.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為節流毫秒數(取整,0為不節流)預設100,onResize為圖表依新尺寸重繪後之通知函數(呼叫時已完成繪製,可直接讀取新版面),容器尺寸與圖表一致或容器隱藏(寬或高為0)時不重繪亦不通知,僅啟用狀態或throttle改變時才重新綁定監聽,預設true
* @vue-event {Object} * 對外轉發echarts之全部事件,例如click、mouseover、legendselectchanged、datazoom、finished等,回傳事件參數物件;zrender事件以zr:前綴綁定,例如@zr:click
*/
export default {
directives: {
domresize: domResize(),
},
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為echarts實例, 不可放入data, 否則Vue會對其深層響應化(遞迴defineProperty), 造成效能問題, 故於init時直接掛this成非響應式屬性
//resizeOpt為v-domresize之設定, 僅於autoresize之啟用狀態或throttle改變時更換
return {
resizeOpt: this.genResizeOpt(this.autoresize),
}
},
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(nv, ov) {
//模板物件字面值每次重繪皆為新物件, 內容相同時不重建
if (isPropUnchanged(nv, ov)) {
return
}
this.reinit()
},
initOptions: {
deep: true,
handler: function(nv, ov) {
//模板物件字面值每次重繪皆為新物件, 內容相同時不重建
if (isPropUnchanged(nv, ov)) {
return
}
this.reinit()
},
},
group: function(nv) {
let vo = this
if (vo.chart) {
vo.chart.group = nv || ''
}
},
loading: function() {
this.updateLoading()
},
loadingOptions: {
deep: true,
handler: function(nv, ov) {
//模板物件字面值每次重繪皆為新物件, 內容相同時不重新顯示
if (isPropUnchanged(nv, ov)) {
return
}
this.updateLoading()
},
},
autoresize: function(nv, ov) {
let vo = this
//僅於啟用狀態或節流毫秒數改變時更換設定(v-domresize隨之重建偵測器); onResize於觸發時才讀取, 故模板物件字面值(每次重繪皆為新物件)或更換onResize皆不需更換
let arNew = parseAutoresize(nv)
let arOld = parseAutoresize(ov)
if (arNew.enabled === arOld.enabled && arNew.throttle === arOld.throttle) {
return
}
vo.resizeOpt = vo.genResizeOpt(nv)
},
},
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()
},
/**
* 銷毀並重建圖表, 供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()
}
},
/**
* 產生v-domresize之設定, autoresize停用時為false(不建立偵測器)(內部使用)
*
* 以w-component-vue之v-domresize(wsemi domDetect: ResizeObserver, 並處理元素消失與重建, 不支援時退回輪詢)監聽容器尺寸:
* event為'resize'只聽容器尺寸, 視窗改變而容器不變時不重繪; sync於ResizeObserver回呼內(繪製前)發出使圖表於同一幀重繪; tolerancePixel為0使1px之變化亦發出;
* getSize以與echarts相同之方式量測容器(clientWidth扣除padding), 故只改padding而外框不變者亦重繪; getBase為圖表目前尺寸,
* 故尺寸一致時(含掛載時與由隱藏轉顯示時)不重繪而不中斷進場動畫, 且與回報次序無關, 同一幀內之尺寸變化(圖剛建立、父組件重繪、捲軸出現)不會遺漏
*/
genResizeOpt: function(autoresize) {
let vo = this
let ar = parseAutoresize(autoresize)
if (!ar.enabled) {
return false
}
return {
event: 'resize',
sync: true,
tolerancePixel: 0,
throttle: ar.throttle,
getSize: getContainerSize,
getBase: vo.getChartSize,
}
},
/**
* 取得圖表目前尺寸, 供v-domresize比較, initOptions指定寬高(非'auto')之維度不隨容器變化故回傳null(不比較), 尚無圖表時回傳null(內部使用)
*/
getChartSize: function() {
let vo = this
//chart
let chart = vo.chart
if (!chart) {
return null
}
//fixedWidth, fixedHeight
let io = vo.initOptions || {}
let fixedWidth = io.width != null && io.width !== 'auto'
let fixedHeight = io.height != null && io.height !== 'auto'
return {
width: fixedWidth ? null : chart.getWidth(),
height: fixedHeight ? null : chart.getHeight(),
}
},
/**
* 容器尺寸與圖表不一致時(由v-domresize判定)重繪(內部使用)
*/
onDomResize: function() {
let vo = this
//chart
let chart = vo.chart
if (!chart) {
return
}
//resize
chart.resize()
//flush, echarts之resize不會同步繪製重排後之畫面(待下一幀), 於此立即繪製,
//使onResize內可直接讀取新尺寸之畫面元素(例如getZr().storage.getDisplayList()), 並避免ResizeObserver回呼後之該幀顯示舊版面
chart.getZr().flush()
//onResize, 於觸發時才讀取, 故更換onResize不需重綁
let { onResize } = parseAutoresize(vo.autoresize)
if (onResize) {
onResize()
}
},
/**
* 銷毀圖表(內部使用), 容器尺寸監聽由v-domresize隨組件解除
*/
clear: function() {
let vo = this
//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>