You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

378 lines
9.5 KiB
JavaScript

import Style from './style.class'
import transition from '../transition'
import {
deepClone,
getRotatePointPos,
getScalePointPos,
getTranslatePointPos,
checkPointIsInRect
} from '../plugin/util'
/**
* @description Class Graph
* @param {Object} graph Graph default configuration
* @param {Object} config Graph config
* @return {Graph} Instance of Graph
*/
export default class Graph {
constructor (graph, config) {
config = deepClone(config, true)
config = {animationFrame:24,...config};
const defaultConfig = {
/**
* @description Weather to render graph
* @type {Boolean}
* @default visible = true
*/
visible: true,
/**
* @description Whether to enable drag
* @type {Boolean}
* @default drag = false
*/
drag: false,
/**
* @description Whether to enable hover
* @type {Boolean}
* @default hover = false
*/
hover: false,
/**
* @description Graph rendering index
* Give priority to index high graph in rendering
* @type {Number}
* @example index = 1
*/
index: 1,
/**
* @description Animation delay time(ms)
* @type {Number}
* @default animationDelay = 0
*/
animationDelay: 0,
/**
* @description Number of animation frames
* @type {Number}
* @default animationFrame = 30
*/
animationFrame: 30,
/**
* @description Animation dynamic curve (Supported by transition)
* @type {String}
* @default animationCurve = 'linear'
* @link https://github.com/jiaming743/Transition
*/
animationCurve: 'linear',
/**
* @description Weather to pause graph animation
* @type {Boolean}
* @default animationPause = false
*/
animationPause: false,
/**
* @description Rectangular hover detection zone
* Use this method for hover detection first
* @type {Null|Array}
* @default hoverRect = null
* @example hoverRect = [0, 0, 100, 100] // [Rect start x, y, Rect width, height]
*/
hoverRect: null,
/**
* @description Mouse enter event handler
* @type {Function|Null}
* @default mouseEnter = null
*/
mouseEnter: null,
/**
* @description Mouse outer event handler
* @type {Function|Null}
* @default mouseOuter = null
*/
mouseOuter: null,
/**
* @description Mouse click event handler
* @type {Function|Null}
* @default click = null
*/
click: null
}
const configAbleNot = {
status: 'static',
animationRoot: [],
animationKeys: [],
animationFrameState: [],
cache: {}
}
if (!config.shape) config.shape = {}
if (!config.style) config.style = {}
const shape = Object.assign({}, graph.shape, config.shape)
Object.assign(defaultConfig, config, configAbleNot)
Object.assign(this, graph, defaultConfig)
this.shape = shape
this.style = new Style(config.style)
this.addedProcessor()
}
}
/**
* @description Processor of added
* @return {Undefined} Void
*/
Graph.prototype.addedProcessor = function () {
if (typeof this.setGraphCenter === 'function') this.setGraphCenter(null, this)
// The life cycle 'added"
if (typeof this.added === 'function') this.added(this)
}
/**
* @description Processor of draw
* @param {CRender} render Instance of CRender
* @param {Graph} graph Instance of Graph
* @return {Undefined} Void
*/
Graph.prototype.drawProcessor = function (render, graph) {
const { ctx } = render
const { shape } = graph
graph.style.initStyle(ctx,shape)
if (typeof this.beforeDraw === 'function') this.beforeDraw(this, render)
graph.draw(render, graph)
// ctx.draw(true)
if (typeof this.drawed === 'function') this.drawed(this, render)
graph.style.restoreTransform(ctx)
}
/**
* @description Processor of hover check
* @param {Array} position Mouse Position
* @param {Graph} graph Instance of Graph
* @return {Boolean} Result of hover check
*/
Graph.prototype.hoverCheckProcessor = function (position, { hoverRect, style, hoverCheck }) {
const { graphCenter, rotate, scale, translate } = style
if (graphCenter) {
if (rotate) position = getRotatePointPos(-rotate, position, graphCenter)
if (scale) position = getScalePointPos(scale.map(s => 1 / s), position, graphCenter)
if (translate) position = getTranslatePointPos(translate.map(v => v * -1), position)
}
if (hoverRect) return checkPointIsInRect(position, ...hoverRect)
return hoverCheck(position, this)
}
/**
* @description Processor of move
* @param {Event} e Mouse movement event
* @return {Undefined} Void
*/
Graph.prototype.moveProcessor = function (e) {
this.move(e, this)
if (typeof this.beforeMove === 'function') this.beforeMove(e, this)
if (typeof this.setGraphCenter === 'function') this.setGraphCenter(e, this)
if (typeof this.moved === 'function') this.moved(e, this)
}
/**
* @description Update graph state
* @param {String} attrName Updated attribute name
* @param {Any} change Updated value
* @return {Undefined} Void
*/
Graph.prototype.attr = function (attrName, change = undefined) {
if (!attrName || change === undefined) return false
const isObject = typeof this[attrName] === 'object'
if (isObject) change = deepClone(change, true)
const { render } = this
if (attrName === 'style') {
this.style.update(change)
} else if (isObject) {
Object.assign(this[attrName], change)
} else {
this[attrName] = change
}
if (attrName === 'index') render.sortGraphsByIndex()
render.drawAllGraph()
}
/**
* @description Update graphics state (with animation)
* Only shape and style attributes are supported
* @param {String} attrName Updated attribute name
* @param {Any} change Updated value
* @param {Boolean} wait Whether to store the animation waiting
* for the next animation request
* @return {Promise} Animation Promise
*/
Graph.prototype.animation = async function (attrName, change, wait = false) {
if (attrName !== 'shape' && attrName !== 'style') {
console.error('Only supported shape and style animation!')
return
}
change = deepClone(change, true)
if (attrName === 'style') this.style.colorProcessor(change)
const changeRoot = this[attrName]
const changeKeys = Object.keys(change)
const beforeState = {}
changeKeys.forEach(key => (beforeState[key] = changeRoot[key]))
const { animationFrame, animationCurve, animationDelay } = this
const animationFrameState = transition(animationCurve, beforeState, change, animationFrame, true)
this.animationRoot.push(changeRoot)
this.animationKeys.push(changeKeys)
this.animationFrameState.push(animationFrameState)
if (wait) return
if (animationDelay > 0) await delay(animationDelay)
const { render } = this
return new Promise(async resolve => {
await render.launchAnimation()
resolve()
})
}
/**
* @description Extract the next frame of data from the animation queue
* and update the graph state
* @return {Undefined} Void
*/
Graph.prototype.turnNextAnimationFrame = function (timeStamp) {
const { animationDelay, animationRoot, animationKeys, animationFrameState, animationPause } = this
if (animationPause) return
if (Date.now() - timeStamp < animationDelay) return
animationRoot.forEach((root, i) => {
animationKeys[i].forEach(key => {
root[key] = animationFrameState[i][0][key]
})
})
animationFrameState.forEach((stateItem, i) => {
stateItem.shift()
const noFrame = stateItem.length === 0
if (noFrame) animationRoot[i] = null
if (noFrame) animationKeys[i] = null
})
this.animationFrameState = animationFrameState.filter(state => state.length)
this.animationRoot = animationRoot.filter(root => root)
this.animationKeys = animationKeys.filter(keys => keys)
}
/**
* @description Skip to the last frame of animation
* @return {Undefined} Void
*/
Graph.prototype.animationEnd = function () {
const { animationFrameState, animationKeys, animationRoot, render } = this
animationRoot.forEach((root, i) => {
const currentKeys = animationKeys[i]
const lastState = animationFrameState[i].pop()
currentKeys.forEach(key => (root[key] = lastState[key]))
})
this.animationFrameState = []
this.animationKeys = []
this.animationRoot = []
return render.drawAllGraph()
}
/**
* @description Pause animation behavior
* @return {Undefined} Void
*/
Graph.prototype.pauseAnimation = function () {
this.attr('animationPause', true)
}
/**
* @description Try animation behavior
* @return {Undefined} Void
*/
Graph.prototype.playAnimation = function () {
const { render } = this
this.attr('animationPause', false)
return new Promise(async resolve => {
await render.launchAnimation()
resolve()
})
}
/**
* @description Processor of delete
* @param {CRender} render Instance of CRender
* @return {Undefined} Void
*/
Graph.prototype.delProcessor = function (render) {
const { graphs } = render
const index = graphs.findIndex(graph => graph === this)
if (index === -1) return
if (typeof this.beforeDelete === 'function') this.beforeDelete(this)
graphs.splice(index, 1, null)
if (typeof this.deleted === 'function') this.deleted(this)
}
/**
* @description Return a timed release Promise
* @param {Number} time Release time
* @return {Promise} A timed release Promise
*/
function delay (time) {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}