import domResize from 'w-component-vue/src/js/domResize.mjs'
import isPropUnchanged from './isPropUnchanged.mjs'
import getChartOptInfo from './getChartOptInfo.mjs'
import cloneOptNoReflow from './cloneOptNoReflow.mjs'
import getContainerSize from './getContainerSize.mjs'
//ctors, 組件名稱對應之Highcharts建構函數名稱
let ctors = {
Highcharts: 'chart',
Highstock: 'stockChart',
Highmaps: 'mapChart',
HighchartsGantt: 'ganttChart',
}
/**
* 產生Highcharts繪圖組件(Vue2),取代vue-highcharts之genComponent
*
* vue-highcharts於options任一變更(含模板物件字面值每次重繪、資料點異動)皆重建整張圖,致圖例隱藏等使用者狀態遺失,且Highcharts每建一圖即新增一個不解除之ResizeObserver而隨更新累積;
* 本組件掛載時建立一次圖表,options變更以chart.update就地更新(內容相同之新物件不更新),銷毀時destroy
*
* 容器尺寸同步:以w-component-vue之v-domresize(wsemi domDetect,ResizeObserver,並處理元素消失與重建)偵測,以與Highcharts相同之方式量測容器並與圖表目前尺寸比較,不一致時於繪製前同步圖表尺寸,取代Highcharts內建reflow(防抖100ms,連續拖拉時不跟隨);
* 容器隱藏(寬或高為0)或尺寸與圖表一致時不重繪,options.chart.reflow給false則不同步,options.chart.width、height有給者該維度固定
*
* @param {String} name 輸入組件名稱字串,可為'Highcharts'、'Highstock'、'Highmaps'、'HighchartsGantt'
* @param {Object} Highcharts 輸入Highcharts物件,須已掛載對應模組(例如stock)
* @returns {Object} 回傳Vue組件物件,圖表實例掛於組件之chart屬性
*/
function genComponent(name, Highcharts) {
//ctor
let ctor = Highcharts[ctors[name]]
if (!ctor) {
//無對應建構函數(例如未掛載模組或於伺服器端)時回傳最小組件
return {
render: function(h) {
return h('div')
},
}
}
return {
name,
directives: {
domresize: domResize(),
},
props: {
options: {
type: Object,
default: () => ({}),
},
},
data: function() {
//resizeOpt為v-domresize之設定, 圖表建立後給予, reflow給false時為false(不建立偵測器)
return {
resizeOpt: false,
}
},
render: function(h) {
return h('div', {
directives: [{ name: 'domresize', value: this.resizeOpt }],
on: { domresize: this.onDomResize },
})
},
mounted: function() {
this.init()
},
beforeDestroy: function() {
this.clear()
},
watch: {
options: {
deep: true,
handler: function(nv, ov) {
let vo = this
//模板物件字面值每次重繪皆為新物件, 內容相同時不更新
if (isPropUnchanged(nv, ov)) {
return
}
//check
if (!vo.chart) {
return
}
//update, 就地更新以保留使用者互動狀態, oneToOne使series等依新設定增刪
vo.chartInfo = getChartOptInfo(nv)
vo.chart.update(cloneOptNoReflow(nv), true, true)
//resizeOpt, 以新refreshKey更換使v-domresize重新比較(例如移除固定寬度), 恢復或停用reflow時隨之重建或解除偵測器
vo.resizeOpt = vo.genResizeOpt()
},
},
},
methods: {
init: function() {
let vo = this
//chartInfo, chart, 掛this成非響應式屬性
vo.chartInfo = getChartOptInfo(vo.options)
vo.chart = ctor(vo.$el, cloneOptNoReflow(vo.options))
//resizeOpt
vo.resizeOpt = vo.genResizeOpt()
},
/**
* 產生v-domresize之設定, reflow給false時為false(內部使用)
*
* event為'resize'只聽容器尺寸; sync於ResizeObserver回呼內(繪製前)發出使圖表於同一幀重繪; tolerancePixel為0使1px之變化亦發出;
* throttle為100, 首次立即執行, 其後每100ms至多一次並保留最後一次; getSize以與Highcharts相同之方式量測容器; getBase為圖表目前尺寸, 故尺寸一致時不重繪;
* refreshKey每次產生皆不同, 故options更新後v-domresize即重新比較一次(v-domresize不於宿主重繪時自動重新比較)
*/
genResizeOpt: function() {
let vo = this
if (!vo.chartInfo || !vo.chartInfo.reflow) {
return false
}
vo.resizeSeq = (vo.resizeSeq || 0) + 1
return {
event: 'resize',
sync: true,
tolerancePixel: 0,
throttle: 100,
getSize: getContainerSize,
getBase: vo.getChartSize,
refreshKey: vo.resizeSeq,
}
},
/**
* 取得圖表目前尺寸, 供v-domresize比較, options.chart.width、height有給之維度不隨容器變化故回傳null(不比較), 尚無圖表時回傳null(內部使用)
*/
getChartSize: function() {
let vo = this
let chart = vo.chart
if (!chart || !vo.chartInfo) {
return null
}
return {
width: vo.chartInfo.fixedWidth ? null : chart.chartWidth,
height: vo.chartInfo.fixedHeight ? null : chart.chartHeight,
}
},
/**
* 容器尺寸與圖表不一致時(由v-domresize判定)同步圖表尺寸(內部使用)
*/
onDomResize: function() {
let vo = this
//check
let chart = vo.chart
if (!chart) {
return
}
//heightByWidth, 高度依寬度比例時, 重繪會使高度由內容決定之容器於ResizeObserver回呼內再變而觸發loop錯誤, 故延至下一幀
if (vo.chartInfo.heightByWidth) {
if (!vo.sizeFrame) {
vo.sizeFrame = window.requestAnimationFrame(() => {
vo.sizeFrame = null
if (vo.chart) {
vo.chart.setSize(undefined, undefined, false)
}
})
}
return
}
//setSize, 同Highcharts內建reflow之呼叫, 寬高依容器重算且不做動畫
chart.setSize(undefined, undefined, false)
},
clear: function() {
let vo = this
//sizeFrame
if (vo.sizeFrame) {
window.cancelAnimationFrame(vo.sizeFrame)
vo.sizeFrame = null
}
//chart, 容器尺寸監聽由v-domresize隨組件解除
if (vo.chart) {
vo.chart.destroy()
vo.chart = null
}
},
},
}
}
export default genComponent