UNPKG

visjs-network

Version:

A dynamic, browser-based network visualization library.

359 lines (320 loc) 11.4 kB
let util = require('../../util') var NetworkUtil = require('../NetworkUtil').default /** * The view */ class View { /** * @param {Object} body * @param {Canvas} canvas */ constructor(body, canvas) { this.body = body this.canvas = canvas this.animationSpeed = 1 / this.renderRefreshRate this.animationEasingFunction = 'easeInOutQuint' this.easingTime = 0 this.sourceScale = 0 this.targetScale = 0 this.sourceTranslation = 0 this.targetTranslation = 0 this.lockedOnNodeId = undefined this.lockedOnNodeOffset = undefined this.touchTime = 0 this.viewFunction = undefined this.body.emitter.on('fit', this.fit.bind(this)) this.body.emitter.on('animationFinished', () => { this.body.emitter.emit('_stopRendering') }) this.body.emitter.on('unlockNode', this.releaseNode.bind(this)) } /** * * @param {Object} [options={}] */ setOptions(options = {}) { this.options = options } /** * This function zooms out to fit all data on screen based on amount of nodes * @param {Object} [options={{nodes=Array}}] * @param {boolean} [initialZoom=false] | zoom based on fitted formula or range, true = fitted, default = false; */ fit(options = { nodes: [] }, initialZoom = false) { let range let zoomLevel options = Object.assign({}, options) if (options.nodes === undefined || options.nodes.length === 0) { options.nodes = this.body.nodeIndices } if (initialZoom === true) { // check if more than half of the nodes have a predefined position. If so, we use the range, not the approximation. let positionDefined = 0 for (let nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { let node = this.body.nodes[nodeId] if (node.predefinedPosition === true) { positionDefined += 1 } } } if (positionDefined > 0.5 * this.body.nodeIndices.length) { this.fit(options, false) return } range = NetworkUtil.getRange(this.body.nodes, options.nodes) let numberOfNodes = this.body.nodeIndices.length zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822 // this is obtained from fitting a dataset from 5 points with scale levels that looked good. // correct for larger canvasses. let factor = Math.min( this.canvas.frame.canvas.clientWidth / 600, this.canvas.frame.canvas.clientHeight / 600 ) zoomLevel *= factor } else { this.body.emitter.emit('_resizeNodes') range = NetworkUtil.getRange(this.body.nodes, options.nodes) let xDistance = Math.abs(range.maxX - range.minX) * 1.1 let yDistance = Math.abs(range.maxY - range.minY) * 1.1 let xZoomLevel = this.canvas.frame.canvas.clientWidth / xDistance let yZoomLevel = this.canvas.frame.canvas.clientHeight / yDistance zoomLevel = xZoomLevel <= yZoomLevel ? xZoomLevel : yZoomLevel } if (zoomLevel > 1.0) { zoomLevel = 1.0 } else if (zoomLevel === 0) { zoomLevel = 1.0 } let center = NetworkUtil.findCenter(range) let animationOptions = { position: center, scale: zoomLevel, animation: options.animation } this.moveTo(animationOptions) } // animation /** * Center a node in view. * * @param {number} nodeId * @param {number} [options] */ focus(nodeId, options = {}) { if (this.body.nodes[nodeId] !== undefined) { let nodePosition = { x: this.body.nodes[nodeId].x, y: this.body.nodes[nodeId].y } options.position = nodePosition options.lockedOnNode = nodeId this.moveTo(options) } else { console.log('Node: ' + nodeId + ' cannot be found.') } } /** * * @param {Object} options | options.offset = {x:number, y:number} // offset from the center in DOM pixels * | options.scale = number // scale to move to * | options.position = {x:number, y:number} // position to move to * | options.animation = {duration:number, easingFunction:String} || Boolean // position to move to */ moveTo(options) { if (options === undefined) { options = {} return } if (options.offset === undefined) { options.offset = { x: 0, y: 0 } } if (options.offset.x === undefined) { options.offset.x = 0 } if (options.offset.y === undefined) { options.offset.y = 0 } if (options.scale === undefined) { options.scale = this.body.view.scale } if (options.position === undefined) { options.position = this.getViewPosition() } if (options.animation === undefined) { options.animation = { duration: 0 } } if (options.animation === false) { options.animation = { duration: 0 } } if (options.animation === true) { options.animation = {} } if (options.animation.duration === undefined) { options.animation.duration = 1000 } // default duration if (options.animation.easingFunction === undefined) { options.animation.easingFunction = 'easeInOutQuad' } // default easing function this.animateView(options) } /** * * @param {Object} options | options.offset = {x:number, y:number} // offset from the center in DOM pixels * | options.time = number // animation time in milliseconds * | options.scale = number // scale to animate to * | options.position = {x:number, y:number} // position to animate to * | options.easingFunction = String // linear, easeInQuad, easeOutQuad, easeInOutQuad, * // easeInCubic, easeOutCubic, easeInOutCubic, * // easeInQuart, easeOutQuart, easeInOutQuart, * // easeInQuint, easeOutQuint, easeInOutQuint */ animateView(options) { if (options === undefined) { return } this.animationEasingFunction = options.animation.easingFunction // release if something focussed on the node this.releaseNode() if (options.locked === true) { this.lockedOnNodeId = options.lockedOnNode this.lockedOnNodeOffset = options.offset } // forcefully complete the old animation if it was still running if (this.easingTime != 0) { this._transitionRedraw(true) // by setting easingtime to 1, we finish the animation. } this.sourceScale = this.body.view.scale this.sourceTranslation = this.body.view.translation this.targetScale = options.scale // set the scale so the viewCenter is based on the correct zoom level. This is overridden in the transitionRedraw // but at least then we'll have the target transition this.body.view.scale = this.targetScale let viewCenter = this.canvas.DOMtoCanvas({ x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight }) let distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node x: viewCenter.x - options.position.x, y: viewCenter.y - options.position.y } this.targetTranslation = { x: this.sourceTranslation.x + distanceFromCenter.x * this.targetScale + options.offset.x, y: this.sourceTranslation.y + distanceFromCenter.y * this.targetScale + options.offset.y } // if the time is set to 0, don't do an animation if (options.animation.duration === 0) { if (this.lockedOnNodeId != undefined) { this.viewFunction = this._lockedRedraw.bind(this) this.body.emitter.on('initRedraw', this.viewFunction) } else { this.body.view.scale = this.targetScale this.body.view.translation = this.targetTranslation this.body.emitter.emit('_requestRedraw') } } else { this.animationSpeed = 1 / (60 * options.animation.duration * 0.001) || 1 / 60 // 60 for 60 seconds, 0.001 for milli's this.animationEasingFunction = options.animation.easingFunction this.viewFunction = this._transitionRedraw.bind(this) this.body.emitter.on('initRedraw', this.viewFunction) this.body.emitter.emit('_startRendering') } } /** * used to animate smoothly by hijacking the redraw function. * @private */ _lockedRedraw() { let nodePosition = { x: this.body.nodes[this.lockedOnNodeId].x, y: this.body.nodes[this.lockedOnNodeId].y } let viewCenter = this.canvas.DOMtoCanvas({ x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight }) let distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node x: viewCenter.x - nodePosition.x, y: viewCenter.y - nodePosition.y } let sourceTranslation = this.body.view.translation let targetTranslation = { x: sourceTranslation.x + distanceFromCenter.x * this.body.view.scale + this.lockedOnNodeOffset.x, y: sourceTranslation.y + distanceFromCenter.y * this.body.view.scale + this.lockedOnNodeOffset.y } this.body.view.translation = targetTranslation } /** * Resets state of a locked on Node */ releaseNode() { if (this.lockedOnNodeId !== undefined && this.viewFunction !== undefined) { this.body.emitter.off('initRedraw', this.viewFunction) this.lockedOnNodeId = undefined this.lockedOnNodeOffset = undefined } } /** * @param {boolean} [finished=false] * @private */ _transitionRedraw(finished = false) { this.easingTime += this.animationSpeed this.easingTime = finished === true ? 1.0 : this.easingTime let progress = util.easingFunctions[this.animationEasingFunction]( this.easingTime ) this.body.view.scale = this.sourceScale + (this.targetScale - this.sourceScale) * progress this.body.view.translation = { x: this.sourceTranslation.x + (this.targetTranslation.x - this.sourceTranslation.x) * progress, y: this.sourceTranslation.y + (this.targetTranslation.y - this.sourceTranslation.y) * progress } // cleanup if (this.easingTime >= 1.0) { this.body.emitter.off('initRedraw', this.viewFunction) this.easingTime = 0 if (this.lockedOnNodeId != undefined) { this.viewFunction = this._lockedRedraw.bind(this) this.body.emitter.on('initRedraw', this.viewFunction) } this.body.emitter.emit('animationFinished') } } /** * * @returns {number} */ getScale() { return this.body.view.scale } /** * * @returns {{x: number, y: number}} */ getViewPosition() { return this.canvas.DOMtoCanvas({ x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight }) } } export default View