worker-echarts
Version:
worker for echarts
410 lines (394 loc) • 11.7 kB
JavaScript
import { stringify } from './save-json'
import MyWorker from "web-worker:./index.worker"
function copyByKeys (data, keys) {
const result = {}
keys.forEach(x => {
if (x in data) { result[x] = data[x] }
})
return result
}
const mouseEventKeys = [
'clientX', 'clientY', 'offsetX', 'offsetY',
'button', 'which', 'wheelDelta', 'detail'
]
const mouseEventNames = [
'click', 'dblclick', 'mousewheel', 'mouseout',
'mouseup', 'mousedown', 'contextmenu'
]
/**
* @description WorkerEcharts Class
*/
class WorkerECharts {
constructor () {
this._worker = new MyWorker();
this._eventTarget = document.createDocumentFragment()
this._eventsMap = {}
this._promise = Promise.resolve(undefined)
//this._worker.postMessage({ type: "window", args: window })
this._worker.addEventListener('message', e => {
console.assert(Array.isArray(e.data), 'Unknown message type posted: ', e)
const [msgType, data] = e.data
switch (msgType) {
case 'event': {
const { type } = data
delete data.type
console.log('type', type, data)
this._eventTarget.dispatchEvent(Object.assign(new Event(type), data))
break
}
case 'open': {
open(...data)
break
}
case 'saveAsImage': {
const $a = document.createElement('a')
$a.download = `${data.title}.${data.type}`
$a.target = '_blank'
$a.href = this._canvas.toDataURL('image/' + data.type)
$a.click()
break
}
case 'tooltip': {
const { type, param } = data
switch (type) {
case 'init': {
const container = this._canvas.parentElement
const div = this._tooltip = document.createElement('div')
const appendToBody = param.appendToBody
Object.assign(div.style, {
position: 'absolute',
left: 0,
top: 0,
zIndex: 9999999,
display: 'none',
whiteSpace: 'pre-wrap',
transitionProperty: 'transform',
transitionTimingFunction: 'cubic-bezier(0.23, 1, 0.32, 1)',
borderRadius: '4px',
borderStyle: 'solid',
pointerEvents: 'none',
contain: 'content'
})
if (appendToBody) {
document.body.append(div)
} else {
container.style.position = 'relative'
container.appendChild(div)
}
break
}
case 'show': {
const div = this._tooltip
Object.assign(div.style, param)
if (this._tooltip.parentElement !== document.body) {
div.style.display = 'block'
}
if (param.extraCssText) {
div.style.cssText += param.extraCssText
}
break
}
case 'setContent':
this._tooltip.innerHTML = param
break
case 'moveTo':
const { style } = this._tooltip
if (this._tooltip.parentElement === document.body) {
const container = this._canvas.parentElement
const { left, top } = container.getBoundingClientRect()
style.display = 'block'
style.transform = `translate(${left + param[0]}px, ${top + param[1]}px)`
} else {
style.transform = `translate(${param[0]}px, ${param[1]}px)`
}
break
case 'hide':
this._tooltip.style.display = 'none'
break
case 'dispose':
this._tooltip.remove()
this._tooltip = null
break
}
break
}
case 'setCursor': {
this._canvas.style.cursor = data
}
}
})
}
/**
* 注册主题
* @param {string} name light or dark
* @param {Object} theme options
* @returns Promise对象
*/
registerTheme (name, theme) {
return this.postMessage({
type: 'registerTheme',
args: [name, JSON.stringify(theme)]
})
}
/**
* 重置x轴单位
* @param {number} point
* @returns Promise对象
*/
resizeAxisUnit (point) {
return this.postMessage({
type: 'resizeAxisUnit',
args: [point]
})
}
/**
* 绑定事件
* @param type {String} 监听echart类型
* @param listener {function} 监听事件回调
* @param isMust {Boolean} 是否重置
* @returns {Object} - {type,lisenter}对象
*/
async on (type, listener, isMust) {
console.log('this._eventsMap', this._eventsMap[type])
if (!this._eventsMap[type] > 0 || isMust) {
this._eventsMap[type] = 0
await this.postMessage({
type: 'addEventListener',
args: [type]
})
}
console.assert(this._eventsMap[type] >= 0, 'Something must be wrong')
this._eventTarget.addEventListener(type, listener)
++this._eventsMap[type]
return { type, listener }
}
/**
* 解绑事件
* @param {Object} indicator -- { type:String,listener:function}
* @returns null
*/
async off (indicator) {
if (!indicator.type) { return }
const { type, listener } = indicator
if (this._eventsMap[type] === 1) {
await this.postMessage({
type: 'removeEventListener',
args: [type]
})
}
console.assert(this._eventsMap[type] > 0, 'Something must be wrong')
this._eventTarget.removeEventListener(type, listener)
indicator.type = null
indicator.listener = null
--this._eventsMap[type]
}
/**
* 初始化
* @param {dom | canvas} div -dom对象或者canvas对象
* @param {string} theme -echart 主题
*/
async init (div, theme) {
div.innerHTML = ''
const canvas = this._canvas = document.createElement('canvas')
canvas.style.cssText = 'width: 100%; height: 100%; margin: 0; user-select: none; border: 0;'
canvas.width = div.clientWidth
canvas.height = div.clientHeight
div.appendChild(canvas)
const offscreen = canvas.transferControlToOffscreen()
await this.postMessage({
type: 'init',
args: [offscreen, theme, { devicePixelRatio }]
}, [offscreen])
// In order not to push too many (mousemove) events in queue,
// we will prevent new events from responding before
// previous event is handled.
let blockEvent = false
canvas.addEventListener('mousemove', e => {
if (blockEvent) {
// console.warn('Blocking mousemove event', e)
return
}
blockEvent = true
this.postMessage({
type: 'event',
args: [e.type, copyByKeys(e, mouseEventKeys)]
}).then(() => blockEvent = false)
}, { passive: true })
mouseEventNames.forEach(eventType => {
canvas.addEventListener(eventType, e => {
this.postMessage({
type: 'event',
args: [e.type, copyByKeys(e, mouseEventKeys)]
})
}, { passive: true })
})
}
/**
* 调用echart方法
* @param {string} methodName -echart方法名 resize、dispatchAction
* @param {object} args -配置参数options
* @returns Promise对象
*/
callMethod (methodName, ...args) {
return this.postMessage({
type: 'callMethod',
args: [methodName, ...args]
})
}
/**
* 调用echart的setOption
* @param {Object} option -echart 配置参数options
* @param {Object} args -其他配置 { notMerge?: boolean;replaceMerge?: string | string[];lazyUpdate?: boolean;}
* @returns Promise对象
*/
setOption (option, ...args) {
return this.postMessage({
type: 'setOption',
args: [stringify(option, /^[\$_]/), ...args]
})
}
/**
* 获取当前echart的options
* @returns Promise对象
*/
getOption () {
return this.postMessage({
type: 'getOption',
args: []
})
}
/**
* 获取当前坐标数据开始点起始点
* @returns Promise对象
*/
getAxisGrid () {
return this.postMessage({
type: 'getAxisGrid',
args: []
})
}
/**
* 获取当前横坐标数据
* @returns Promise对象
*/
getEchartXInterval () {
return this.postMessage({
type: 'getEchartXInterval',
args: []
})
}
/**
* 获取当前纵坐标数据
* @returns Promise对象
*/
getEchartYInterval () {
return this.postMessage({
type: 'getEchartYInterval',
args: []
})
}
/**
* 启动或关闭 toolbox 中 dataZoom 的刷选状态。
* @returns promise对象
*/
enlargeZoom () {
return this.postMessage({
type: 'enlargeZoom',
args: []
})
}
/**
* 重置坐标轴
* @returns promise对象
*/
restoreZoom () {
return this.postMessage({
type: 'restoreZoom',
args: []
})
}
/**
* 重置坐标轴
* params {Object}- {type: String,dataZoomIndex: Number, start: Number,end: Number,startValue: Number,endValue: Number}
* @returns promise对象
*/
chartDataZoom (params) {
return this.postMessage({
type: 'chartDataZoom',
args: [params]
})
}
/**
* 调用echart 的dispatchAction
* @param {Object} payload -配置参数
* @returns promise对象
*/
dispatchAction (payload) {
return this.callMethod('dispatchAction', payload)
}
/**
* 调用echart的resize方法
* @param opts {Object} -配置参数
* @returns Promise对象
*/
resize (opts = {}) {
return this.callMethod('resize', opts)
}
/**
* 清除echart
* @returns Promise对象
*/
clean () {
return this.postMessage({
type: 'clean',
args: []
})
}
/**
* 清除worker
*/
dispose () {
// It's an noop of dispose method in worker
this._worker.terminate()
this._tooltip?.remove()
}
/**
* 将消息发送到工作线程;返回的承诺在onmessage消息返回时被解析
* @param {Object} message - message对象 {type:String,args: Array<any>}
*
* @returns Promise对象
*/
postMessage (message, transfer) {
this._promise = this._promise.catch(() => { }).then(() => {
return new Promise((resolve, reject) => {
this._worker.addEventListener('message', function onMessage (e) {
console.assert(Array.isArray(e.data), 'Unknown message type posted: ', e)
const [type, data] = e.data
switch (type) {
case 'resolve': {
resolve(data)
this.removeEventListener('message', onMessage)
break
}
case 'reject': {
reject(data)
this.removeEventListener('message', onMessage)
break
}
case 'error': {
const [name, msg, stack] = data
const error = new self[name](msg)
error.stack = stack
reject(error)
this.removeEventListener('message', onMessage)
break
}
}
})
this._worker.postMessage(message, transfer)
})
})
return this._promise
}
}
export default WorkerECharts