var Emitter = require('emitter-component'); var Hammer = require('../module/hammer'); var keycharm = require('keycharm'); var util = require('../util'); var hammerUtil = require('../hammerUtil'); var DataSet = require('../DataSet'); var DataView = require('../DataView'); var dotparser = require('./dotparser'); var gephiParser = require('./gephiParser'); var Groups = require('./Groups'); var Images = require('./Images'); var Node = require('./Node'); var Edge = require('./Edge'); var Popup = require('./Popup'); var MixinLoader = require('./mixins/MixinLoader'); var Activator = require('../shared/Activator'); var locales = require('./locales'); // Load custom shapes into CanvasRenderingContext2D require('./shapes'); import { PhysicsEngine } from './modules/PhysicsEngine' import { ClusterEngine } from './modules/Clustering' import { CanvasRenderer } from './modules/CanvasRenderer' import { Canvas } from './modules/Canvas' import { View } from './modules/View' /** * @constructor Network * Create a network visualization, displaying nodes and edges. * * @param {Element} container The DOM element in which the Network will * be created. Normally a div element. * @param {Object} data An object containing parameters * {Array} nodes * {Array} edges * @param {Object} options Options */ function Network (container, data, options) { if (!(this instanceof Network)) { throw new SyntaxError('Constructor must be called with the new operator'); } this._initializeMixinLoaders(); // render and calculation settings this.initializing = true; this.triggerFunctions = {add:null,edit:null,editEdge:null,connect:null,del:null}; var customScalingFunction = function (min,max,total,value) { if (max == min) { return 0.5; } else { var scale = 1 / (max - min); return Math.max(0,(value - min)*scale); } }; // set constant values this.defaultOptions = { nodes: { customScalingFunction: customScalingFunction, mass: 1, radiusMin: 10, radiusMax: 30, radius: 10, shape: 'ellipse', image: undefined, widthMin: 16, // px widthMax: 64, // px fontColor: 'black', fontSize: 14, // px fontFace: 'verdana', fontFill: undefined, fontStrokeWidth: 0, // px fontStrokeColor: '#ffffff', fontDrawThreshold: 3, scaleFontWithValue: false, fontSizeMin: 14, fontSizeMax: 30, fontSizeMaxVisible: 30, value: 1, level: -1, color: { border: '#2B7CE9', background: '#97C2FC', highlight: { border: '#2B7CE9', background: '#D2E5FF' }, hover: { border: '#2B7CE9', background: '#D2E5FF' } }, group: undefined, borderWidth: 1, borderWidthSelected: undefined }, edges: { customScalingFunction: customScalingFunction, widthMin: 1, // widthMax: 15,// width: 1, widthSelectionMultiplier: 2, hoverWidth: 1.5, value:1, style: 'line', color: { color:'#848484', highlight:'#848484', hover: '#848484' }, opacity:1.0, fontColor: '#343434', fontSize: 14, // px fontFace: 'arial', fontFill: 'white', fontStrokeWidth: 0, // px fontStrokeColor: 'white', labelAlignment:'horizontal', arrowScaleFactor: 1, dash: { length: 10, gap: 5, altLength: undefined }, inheritColor: "from", // to, from, false, true (== from) useGradients: false // release in 4.0 }, configurePhysics:false, navigation: { enabled: false }, keyboard: { enabled: false, speed: {x: 10, y: 10, zoom: 0.02}, bindToWindow: true }, dataManipulation: { enabled: false, initiallyVisible: false }, hierarchicalLayout: { enabled:false, levelSeparation: 150, nodeSpacing: 100, direction: "UD", // UD, DU, LR, RL layout: "hubsize" // hubsize, directed }, smoothCurves: { enabled: true, dynamic: true, type: "continuous", roundness: 0.5 }, locale: 'en', locales: locales, tooltip: { delay: 300, fontColor: 'black', fontSize: 14, // px fontFace: 'verdana', color: { border: '#666', background: '#FFFFC6' } }, dragNetwork: true, dragNodes: true, zoomable: true, hover: false, hideEdgesOnDrag: false, hideNodesOnDrag: false, width : '100%', height : '100%', selectable: true, useDefaultGroups: true }; this.constants = util.extend({}, this.defaultOptions); // containers for nodes and edges this.body = { nodes: {}, nodeIndices: [], supportNodes: {}, supportNodeIndices: [], edges: {}, data: { nodes: null, // A DataSet or DataView edges: null // A DataSet or DataView }, functions:{ createNode: this._createNode.bind(this), createEdge: this._createEdge.bind(this) }, emitter: { on: this.on.bind(this), off: this.off.bind(this), emit: this.emit.bind(this), once: this.once.bind(this) }, eventListeners: { onTap: function() {}, onTouch: function() {}, onDoubleTap: function() {}, onHold: function() {}, onDragStart: function() {}, onDrag: function() {}, onDragEnd: function() {}, onMouseWheel: function() {}, onPinch: function() {}, onMouseMove: function() {}, onRelease: function() {} }, container: container }; // modules this.view = new View(this.body); this.renderer = new CanvasRenderer(this.body); this.clustering = new ClusterEngine(this.body); this.physics = new PhysicsEngine(this.body); this.canvas = new Canvas(this.body); this.renderer.setCanvas(this.canvas); this.view.setCanvas(this.canvas); this.hoverObj = {nodes:{},edges:{}}; this.controlNodesActive = false; this.navigationHammers = []; this.manipulationHammers = []; // Node variables var me = this; this.groups = new Groups(); // object with groups this.images = new Images(); // object with images this.images.setOnloadCallback(function (status) { me._requestRedraw(); }); // keyboard navigation variables this.xIncrement = 0; this.yIncrement = 0; this.zoomIncrement = 0; // loading all the mixins: // load the force calculation functions, grouped under the physics system. //this._loadPhysicsSystem(); // create a frame and canvas // load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it) // load the selection system. (mandatory, required by Network) this._loadSelectionSystem(); // load the selection system. (mandatory, required by Network) //this._loadHierarchySystem(); // apply options this.setOptions(options); // other vars this.cachedFunctions = {}; this.startedStabilization = false; this.stabilized = false; this.stabilizationIterations = null; this.draggingNodes = false; // position and scale variables and objects this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw this.scale = 1; // defining the global scale variable in the constructor // create event listeners used to subscribe on the DataSets of the nodes and edges this.nodesListeners = { 'add': function (event, params) { me._addNodes(params.items); me.start(); }, 'update': function (event, params) { me._updateNodes(params.items, params.data); me.start(); }, 'remove': function (event, params) { me._removeNodes(params.items); me.start(); } }; this.edgesListeners = { 'add': function (event, params) { me._addEdges(params.items); me.start(); }, 'update': function (event, params) { me._updateEdges(params.items); me.start(); }, 'remove': function (event, params) { me._removeEdges(params.items); me.start(); } }; // properties for the animation this.moving = true; this.renderTimer = undefined; // Scheduling function. Is definded in this.start(); // load data (the disable start variable will be the same as the enabled clustering) this.setData(data, this.constants.hierarchicalLayout.enabled); // hierarchical layout if (this.constants.hierarchicalLayout.enabled == true) { this._setupHierarchicalLayout(); } else { // zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here. if (this.constants.stabilize == false) { this.zoomExtent({duration:0}, true, this.constants.clustering.enabled); } } if (this.constants.stabilize == false) { this.initializing = false; } var me = this; // this event will trigger a rebuilding of the cache of colors, nodes etc. this.on("_dataChanged", function () { me._updateNodeIndexList(); me.physics._updateCalculationNodes(); me._markAllEdgesAsDirty(); if (me.initializing !== true) { me.moving = true; me.start(); } }) this.on("_newEdgesCreated", this._createBezierNodes.bind(this)); //this.on("stabilizationIterationsDone", function () {me.initializing = false; me.start();}.bind(this)); } // Extend Network with an Emitter mixin Emitter(Network.prototype); Network.prototype._createNode = function(properties) { return new Node(properties, this.images, this.groups, this.constants) } Network.prototype._createEdge = function(properties) { return new Edge(properties, this.body, this.constants) } /** * Update the this.body.nodeIndices with the most recent node index list * @private */ Network.prototype._updateNodeIndexList = function() { this.body.supportNodeIndices = Object.keys(this.body.supportNodes) this.body.nodeIndices = Object.keys(this.body.nodes); }; /** * Set nodes and edges, and optionally options as well. * * @param {Object} data Object containing parameters: * {Array | DataSet | DataView} [nodes] Array with nodes * {Array | DataSet | DataView} [edges] Array with edges * {String} [dot] String containing data in DOT format * {String} [gephi] String containing data in gephi JSON format * {Options} [options] Object with options * @param {Boolean} [disableStart] | optional: disable the calling of the start function. */ Network.prototype.setData = function(data, disableStart) { if (disableStart === undefined) { disableStart = false; } // unselect all to ensure no selections from old data are carried over. this._unselectAll(true); // we set initializing to true to ensure that the hierarchical layout is not performed until both nodes and edges are added. this.initializing = true; if (data && data.dot && (data.nodes || data.edges)) { throw new SyntaxError('Data must contain either parameter "dot" or ' + ' parameter pair "nodes" and "edges", but not both.'); } // clean up in case there is anyone in an active mode of the manipulation. This is the same option as bound to the escape button. if (this.constants.dataManipulation.enabled == true) { this._createManipulatorBar(); } // set options this.setOptions(data && data.options); // set all data if (data && data.dot) { // parse DOT file if(data && data.dot) { var dotData = dotparser.DOTToGraph(data.dot); this.setData(dotData); return; } } else if (data && data.gephi) { // parse DOT file if(data && data.gephi) { var gephiData = gephiParser.parseGephi(data.gephi); this.setData(gephiData); return; } } else { this._setNodes(data && data.nodes); this._setEdges(data && data.edges); } if (disableStart == false) { if (this.constants.hierarchicalLayout.enabled == true) { this._resetLevels(); this._setupHierarchicalLayout(); } else { // find a stable position or start animating to a stable position this.physics.startSimulation() } } else { this.initializing = false; } }; /** * Set options * @param {Object} options */ Network.prototype.setOptions = function (options) { if (options) { var prop; var fields = ['nodes','edges','smoothCurves','hierarchicalLayout','navigation', 'keyboard','dataManipulation','onAdd','onEdit','onEditEdge','onConnect','onDelete','clickToUse' ]; // extend all but the values in fields util.selectiveNotDeepExtend(fields,this.constants, options); util.selectiveNotDeepExtend(['color'],this.constants.nodes, options.nodes); util.selectiveNotDeepExtend(['color','length'],this.constants.edges, options.edges); this.groups.useDefaultGroups = this.constants.useDefaultGroups; this.physics.setOptions(options.physics); this.canvas.setOptions(this.constants); if (options.onAdd) {this.triggerFunctions.add = options.onAdd;} if (options.onEdit) {this.triggerFunctions.edit = options.onEdit;} if (options.onEditEdge) {this.triggerFunctions.editEdge = options.onEditEdge;} if (options.onConnect) {this.triggerFunctions.connect = options.onConnect;} if (options.onDelete) {this.triggerFunctions.del = options.onDelete;} util.mergeOptions(this.constants, options,'smoothCurves'); util.mergeOptions(this.constants, options,'hierarchicalLayout'); util.mergeOptions(this.constants, options,'clustering'); util.mergeOptions(this.constants, options,'navigation'); util.mergeOptions(this.constants, options,'keyboard'); util.mergeOptions(this.constants, options,'dataManipulation'); if (options.dataManipulation) { this.editMode = this.constants.dataManipulation.initiallyVisible; } // TODO: work out these options and document them if (options.edges) { if (options.edges.color !== undefined) { if (util.isString(options.edges.color)) { this.constants.edges.color = {}; this.constants.edges.color.color = options.edges.color; this.constants.edges.color.highlight = options.edges.color; this.constants.edges.color.hover = options.edges.color; } else { if (options.edges.color.color !== undefined) {this.constants.edges.color.color = options.edges.color.color;} if (options.edges.color.highlight !== undefined) {this.constants.edges.color.highlight = options.edges.color.highlight;} if (options.edges.color.hover !== undefined) {this.constants.edges.color.hover = options.edges.color.hover;} } this.constants.edges.inheritColor = false; } if (!options.edges.fontColor) { if (options.edges.color !== undefined) { if (util.isString(options.edges.color)) {this.constants.edges.fontColor = options.edges.color;} else if (options.edges.color.color !== undefined) {this.constants.edges.fontColor = options.edges.color.color;} } } } if (options.nodes) { if (options.nodes.color) { var newColorObj = util.parseColor(options.nodes.color); this.constants.nodes.color.background = newColorObj.background; this.constants.nodes.color.border = newColorObj.border; this.constants.nodes.color.highlight.background = newColorObj.highlight.background; this.constants.nodes.color.highlight.border = newColorObj.highlight.border; this.constants.nodes.color.hover.background = newColorObj.hover.background; this.constants.nodes.color.hover.border = newColorObj.hover.border; } } if (options.groups) { for (var groupname in options.groups) { if (options.groups.hasOwnProperty(groupname)) { var group = options.groups[groupname]; this.groups.add(groupname, group); } } } if (options.tooltip) { for (prop in options.tooltip) { if (options.tooltip.hasOwnProperty(prop)) { this.constants.tooltip[prop] = options.tooltip[prop]; } } if (options.tooltip.color) { this.constants.tooltip.color = util.parseColor(options.tooltip.color); } } if ('clickToUse' in options) { if (options.clickToUse) { if (!this.activator) { this.activator = new Activator(this.frame); this.activator.on('change', this._createKeyBinds.bind(this)); } } else { if (this.activator) { this.activator.destroy(); delete this.activator; } } } if (options.labels) { throw new Error('Option "labels" is deprecated. Use options "locale" and "locales" instead.'); } // (Re)loading the mixins that can be enabled or disabled in the options. // load the force calculation functions, grouped under the physics system. // load the navigation system. //this._loadNavigationControls(); //// load the data manipulation system //this._loadManipulationSystem(); //// configure the smooth curves //this._configureSmoothCurves(); // bind hammer this.canvas._bindHammer(); // bind keys. If disabled, this will not do anything; //this._createKeyBinds(); this._markAllEdgesAsDirty(); this.canvas.setSize(this.constants.width, this.constants.height); if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { this._resetLevels(); this._setupHierarchicalLayout(); } if (this.initializing !== true) { this.moving = true; this.start(); } } }; /** * Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin * @private */ Network.prototype._createKeyBinds = function() { return; //var me = this; //if (this.keycharm !== undefined) { // this.keycharm.destroy(); //} // //if (this.constants.keyboard.bindToWindow == true) { // this.keycharm = keycharm({container: window, preventDefault: false}); //} //else { // this.keycharm = keycharm({container: this.frame, preventDefault: false}); //} // //this.keycharm.reset(); // //if (this.constants.keyboard.enabled && this.isActive()) { // this.keycharm.bind("up", this._moveUp.bind(me) , "keydown"); // this.keycharm.bind("up", this._yStopMoving.bind(me), "keyup"); // this.keycharm.bind("down", this._moveDown.bind(me) , "keydown"); // this.keycharm.bind("down", this._yStopMoving.bind(me), "keyup"); // this.keycharm.bind("left", this._moveLeft.bind(me) , "keydown"); // this.keycharm.bind("left", this._xStopMoving.bind(me), "keyup"); // this.keycharm.bind("right",this._moveRight.bind(me), "keydown"); // this.keycharm.bind("right",this._xStopMoving.bind(me), "keyup"); // this.keycharm.bind("=", this._zoomIn.bind(me), "keydown"); // this.keycharm.bind("=", this._stopZoom.bind(me), "keyup"); // this.keycharm.bind("num+", this._zoomIn.bind(me), "keydown"); // this.keycharm.bind("num+", this._stopZoom.bind(me), "keyup"); // this.keycharm.bind("num-", this._zoomOut.bind(me), "keydown"); // this.keycharm.bind("num-", this._stopZoom.bind(me), "keyup"); // this.keycharm.bind("-", this._zoomOut.bind(me), "keydown"); // this.keycharm.bind("-", this._stopZoom.bind(me), "keyup"); // this.keycharm.bind("[", this._zoomIn.bind(me), "keydown"); // this.keycharm.bind("[", this._stopZoom.bind(me), "keyup"); // this.keycharm.bind("]", this._zoomOut.bind(me), "keydown"); // this.keycharm.bind("]", this._stopZoom.bind(me), "keyup"); // this.keycharm.bind("pageup",this._zoomIn.bind(me), "keydown"); // this.keycharm.bind("pageup",this._stopZoom.bind(me), "keyup"); // this.keycharm.bind("pagedown",this._zoomOut.bind(me),"keydown"); // this.keycharm.bind("pagedown",this._stopZoom.bind(me), "keyup"); //} // //if (this.constants.dataManipulation.enabled == true) { // this.keycharm.bind("esc",this._createManipulatorBar.bind(me)); // this.keycharm.bind("delete",this._deleteSelected.bind(me)); //} }; /** * Cleans up all bindings of the network, removing it fully from the memory IF the variable is set to null after calling this function. * var network = new vis.Network(..); * network.destroy(); * network = null; */ Network.prototype.destroy = function() { this.start = function () {}; this.redraw = function () {}; this.renderTimer = false; // cleanup physicsConfiguration if it exists this._cleanupPhysicsConfiguration(); // remove keybindings this.keycharm.reset(); // clear hammer bindings this.hammer.dispose(); // clear events this.off(); this._recursiveDOMDelete(this.containerElement); } Network.prototype._recursiveDOMDelete = function(DOMobject) { while (DOMobject.hasChildNodes() == true) { this._recursiveDOMDelete(DOMobject.firstChild); DOMobject.removeChild(DOMobject.firstChild); } } /** * Get the pointer location from a touch location * @param {{pageX: Number, pageY: Number}} touch * @return {{x: Number, y: Number}} pointer * @private */ Network.prototype._getPointer = function (touch) { return { x: touch.pageX - util.getAbsoluteLeft(this.frame.canvas), y: touch.pageY - util.getAbsoluteTop(this.frame.canvas) }; }; /** * On start of a touch gesture, store the pointer * @param event * @private */ Network.prototype._onTouch = function (event) { if (new Date().valueOf() - this.touchTime > 100) { this.drag.pointer = this._getPointer(event.gesture.center); this.drag.pinched = false; this.pinch.scale = this._getScale(); // to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) this.touchTime = new Date().valueOf(); this._handleTouch(this.drag.pointer); } }; /** * handle drag start event * @private */ Network.prototype._onDragStart = function (event) { this._handleDragStart(event); }; /** * This function is called by _onDragStart. * It is separated out because we can then overload it for the datamanipulation system. * * @private */ Network.prototype._handleDragStart = function(event) { // in case the touch event was triggered on an external div, do the initial touch now. if (this.drag.pointer === undefined) { this._onTouch(event); } var node = this._getNodeAt(this.drag.pointer); // note: drag.pointer is set in _onTouch to get the initial touch location this.drag.dragging = true; this.drag.selection = []; this.drag.translation = this._getTranslation(); this.drag.nodeId = null; this.draggingNodes = false; if (node != null && this.constants.dragNodes == true) { this.draggingNodes = true; this.drag.nodeId = node.id; // select the clicked node if not yet selected if (!node.isSelected()) { this._selectObject(node,false); } this.emit("dragStart",{nodeIds:this.getSelection().nodes}); // create an array with the selected nodes and their original location and status for (var objectId in this.selectionObj.nodes) { if (this.selectionObj.nodes.hasOwnProperty(objectId)) { var object = this.selectionObj.nodes[objectId]; var s = { id: object.id, node: object, // store original x, y, xFixed and yFixed, make the node temporarily Fixed x: object.x, y: object.y, xFixed: object.xFixed, yFixed: object.yFixed }; object.xFixed = true; object.yFixed = true; this.drag.selection.push(s); } } } }; /** * handle drag event * @private */ Network.prototype._onDrag = function (event) { this._handleOnDrag(event) }; /** * This function is called by _onDrag. * It is separated out because we can then overload it for the datamanipulation system. * * @private */ Network.prototype._handleOnDrag = function(event) { if (this.drag.pinched) { return; } // remove the focus on node if it is focussed on by the focusOnNode this.releaseNode(); var pointer = this._getPointer(event.gesture.center); var me = this; var drag = this.drag; var selection = drag.selection; if (selection && selection.length && this.constants.dragNodes == true) { // calculate delta's and new location var deltaX = pointer.x - drag.pointer.x; var deltaY = pointer.y - drag.pointer.y; // update position of all selected nodes selection.forEach(function (s) { var node = s.node; if (!s.xFixed) { node.x = me._XconvertDOMtoCanvas(me._XconvertCanvasToDOM(s.x) + deltaX); } if (!s.yFixed) { node.y = me._YconvertDOMtoCanvas(me._YconvertCanvasToDOM(s.y) + deltaY); } }); // start _animationStep if not yet running if (!this.moving) { this.moving = true; this.start(); } } else { // move the network if (this.constants.dragNetwork == true) { // if the drag was not started properly because the click started outside the network div, start it now. if (this.drag.pointer === undefined) { this._handleDragStart(event); return; } var diffX = pointer.x - this.drag.pointer.x; var diffY = pointer.y - this.drag.pointer.y; this._setTranslation( this.drag.translation.x + diffX, this.drag.translation.y + diffY ); this._redraw(); } } }; /** * handle drag start event * @private */ Network.prototype._onDragEnd = function (event) { this._handleDragEnd(event); }; Network.prototype._handleDragEnd = function(event) { this.drag.dragging = false; var selection = this.drag.selection; if (selection && selection.length) { selection.forEach(function (s) { // restore original xFixed and yFixed s.node.xFixed = s.xFixed; s.node.yFixed = s.yFixed; }); this.moving = true; this.start(); } else { this._redraw(); } if (this.draggingNodes == false) { this.emit("dragEnd",{nodeIds:[]}); } else { this.emit("dragEnd",{nodeIds:this.getSelection().nodes}); } } /** * handle tap/click event: select/unselect a node * @private */ Network.prototype._onTap = function (event) { var pointer = this._getPointer(event.gesture.center); this.pointerPosition = pointer; this._handleTap(pointer); }; /** * handle doubletap event * @private */ Network.prototype._onDoubleTap = function (event) { var pointer = this._getPointer(event.gesture.center); this._handleDoubleTap(pointer); }; /** * handle long tap event: multi select nodes * @private */ Network.prototype._onHold = function (event) { var pointer = this._getPointer(event.gesture.center); this.pointerPosition = pointer; this._handleOnHold(pointer); }; /** * handle the release of the screen * * @private */ Network.prototype._onRelease = function (event) { var pointer = this._getPointer(event.gesture.center); this._handleOnRelease(pointer); }; /** * Handle pinch event * @param event * @private */ Network.prototype._onPinch = function (event) { var pointer = this._getPointer(event.gesture.center); this.drag.pinched = true; if (!('scale' in this.pinch)) { this.pinch.scale = 1; } // TODO: enabled moving while pinching? var scale = this.pinch.scale * event.gesture.scale; this._zoom(scale, pointer) }; /** * Zoom the network in or out * @param {Number} scale a number around 1, and between 0.01 and 10 * @param {{x: Number, y: Number}} pointer Position on screen * @return {Number} appliedScale scale is limited within the boundaries * @private */ Network.prototype._zoom = function(scale, pointer) { if (this.constants.zoomable == true) { var scaleOld = this._getScale(); if (scale < 0.00001) { scale = 0.00001; } if (scale > 10) { scale = 10; } var preScaleDragPointer = null; if (this.drag !== undefined) { if (this.drag.dragging == true) { preScaleDragPointer = this.DOMtoCanvas(this.drag.pointer); } } // + this.frame.canvas.clientHeight / 2 var translation = this._getTranslation(); var scaleFrac = scale / scaleOld; var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x), "y" : this._YconvertDOMtoCanvas(pointer.y)}; this._setScale(scale); this._setTranslation(tx, ty); if (preScaleDragPointer != null) { var postScaleDragPointer = this.canvasToDOM(preScaleDragPointer); this.drag.pointer.x = postScaleDragPointer.x; this.drag.pointer.y = postScaleDragPointer.y; } this._redraw(); if (scaleOld < scale) { this.emit("zoom", {direction:"+"}); } else { this.emit("zoom", {direction:"-"}); } return scale; } }; /** * Event handler for mouse wheel event, used to zoom the timeline * See http://adomas.org/javascript-mouse-wheel/ * https://github.com/EightMedia/hammer.js/issues/256 * @param {MouseEvent} event * @private */ Network.prototype._onMouseWheel = function(event) { // retrieve delta var delta = 0; if (event.wheelDelta) { /* IE/Opera. */ delta = event.wheelDelta/120; } else if (event.detail) { /* Mozilla case. */ // In Mozilla, sign of delta is different than in IE. // Also, delta is multiple of 3. delta = -event.detail/3; } // If delta is nonzero, handle it. // Basically, delta is now positive if wheel was scrolled up, // and negative, if wheel was scrolled down. if (delta) { // calculate the new scale var scale = this._getScale(); var zoom = delta / 10; if (delta < 0) { zoom = zoom / (1 - zoom); } scale *= (1 + zoom); // calculate the pointer location var gesture = hammerUtil.fakeGesture(this, event); var pointer = this._getPointer(gesture.center); // apply the new scale this._zoom(scale, pointer); } // Prevent default actions caused by mouse wheel. event.preventDefault(); }; /** * Mouse move handler for checking whether the title moves over a node with a title. * @param {Event} event * @private */ Network.prototype._onMouseMoveTitle = function (event) { var gesture = hammerUtil.fakeGesture(this, event); var pointer = this._getPointer(gesture.center); var popupVisible = false; // check if the previously selected node is still selected if (this.popup !== undefined) { if (this.popup.hidden === false) { this._checkHidePopup(pointer); } // if the popup was not hidden above if (this.popup.hidden === false) { popupVisible = true; this.popup.setPosition(pointer.x + 3,pointer.y - 5) this.popup.show(); } } // if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over if (this.constants.keyboard.bindToWindow == false && this.constants.keyboard.enabled == true) { this.frame.focus(); } // start a timeout that will check if the mouse is positioned above an element if (popupVisible === false) { var me = this; var checkShow = function () { me._checkShowPopup(pointer); }; if (this.popupTimer) { clearInterval(this.popupTimer); // stop any running calculationTimer } if (!this.drag.dragging) { this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay); } } /** * Adding hover highlights */ if (this.constants.hover == true) { // removing all hover highlights for (var edgeId in this.hoverObj.edges) { if (this.hoverObj.edges.hasOwnProperty(edgeId)) { this.hoverObj.edges[edgeId].hover = false; delete this.hoverObj.edges[edgeId]; } } // adding hover highlights var obj = this._getNodeAt(pointer); if (obj == null) { obj = this._getEdgeAt(pointer); } if (obj != null) { this._hoverObject(obj); } // removing all node hover highlights except for the selected one. for (var nodeId in this.hoverObj.nodes) { if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) { this._blurObject(this.hoverObj.nodes[nodeId]); delete this.hoverObj.nodes[nodeId]; } } } this.redraw(); } }; /** * Check if there is an element on the given position in the network * (a node or edge). If so, and if this element has a title, * show a popup window with its title. * * @param {{x:Number, y:Number}} pointer * @private */ Network.prototype._checkShowPopup = function (pointer) { var obj = { left: this._XconvertDOMtoCanvas(pointer.x), top: this._YconvertDOMtoCanvas(pointer.y), right: this._XconvertDOMtoCanvas(pointer.x), bottom: this._YconvertDOMtoCanvas(pointer.y) }; var id; var previousPopupObjId = this.popupObj === undefined ? "" : this.popupObj.id; var nodeUnderCursor = false; var popupType = "node"; if (this.popupObj == undefined) { // search the nodes for overlap, select the top one in case of multiple nodes var nodes = this.body.nodes; var overlappingNodes = []; for (id in nodes) { if (nodes.hasOwnProperty(id)) { var node = nodes[id]; if (node.isOverlappingWith(obj)) { if (node.getTitle() !== undefined) { overlappingNodes.push(id); } } } } if (overlappingNodes.length > 0) { // if there are overlapping nodes, select the last one, this is the // one which is drawn on top of the others this.popupObj = this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; // if you hover over a node, the title of the edge is not supposed to be shown. nodeUnderCursor = true; } } if (this.popupObj === undefined && nodeUnderCursor == false) { // search the edges for overlap var edges = this.body.edges; var overlappingEdges = []; for (id in edges) { if (edges.hasOwnProperty(id)) { var edge = edges[id]; if (edge.connected === true && (edge.getTitle() !== undefined) && edge.isOverlappingWith(obj)) { overlappingEdges.push(id); } } } if (overlappingEdges.length > 0) { this.popupObj = this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; popupType = "edge"; } } if (this.popupObj) { // show popup message window if (this.popupObj.id != previousPopupObjId) { if (this.popup === undefined) { this.popup = new Popup(this.frame, this.constants.tooltip); } this.popup.popupTargetType = popupType; this.popup.popupTargetId = this.popupObj.id; // adjust a small offset such that the mouse cursor is located in the // bottom left location of the popup, and you can easily move over the // popup area this.popup.setPosition(pointer.x + 3, pointer.y - 5); this.popup.setText(this.popupObj.getTitle()); this.popup.show(); } } else { if (this.popup) { this.popup.hide(); } } }; /** * Check if the popup must be hidden, which is the case when the mouse is no * longer hovering on the object * @param {{x:Number, y:Number}} pointer * @private */ Network.prototype._checkHidePopup = function (pointer) { var pointerObj = { left: this._XconvertDOMtoCanvas(pointer.x), top: this._YconvertDOMtoCanvas(pointer.y), right: this._XconvertDOMtoCanvas(pointer.x), bottom: this._YconvertDOMtoCanvas(pointer.y) }; var stillOnObj = false; if (this.popup.popupTargetType == 'node') { stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj); if (stillOnObj === true) { var overNode = this._getNodeAt(pointer); stillOnObj = overNode.id == this.popup.popupTargetId; } } else { if (this._getNodeAt(pointer) === null) { stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj); } } if (stillOnObj === false) { this.popupObj = undefined; this.popup.hide(); } }; /** * Set a data set with nodes for the network * @param {Array | DataSet | DataView} nodes The data containing the nodes. * @private */ Network.prototype._setNodes = function(nodes) { var oldNodesData = this.body.data.nodes; if (nodes instanceof DataSet || nodes instanceof DataView) { this.body.data.nodes = nodes; } else if (Array.isArray(nodes)) { this.body.data.nodes = new DataSet(); this.body.data.nodes.add(nodes); } else if (!nodes) { this.body.data.nodes = new DataSet(); } else { throw new TypeError('Array or DataSet expected'); } if (oldNodesData) { // unsubscribe from old dataset util.forEach(this.nodesListeners, function (callback, event) { oldNodesData.off(event, callback); }); } // remove drawn nodes this.body.nodes = {}; if (this.body.data.nodes) { // subscribe to new dataset var me = this; util.forEach(this.nodesListeners, function (callback, event) { me.body.data.nodes.on(event, callback); }); // draw all new nodes var ids = this.body.data.nodes.getIds(); this._addNodes(ids); } this._updateSelection(); }; /** * Add nodes * @param {Number[] | String[]} ids * @private */ Network.prototype._addNodes = function(ids) { var id; for (var i = 0, len = ids.length; i < len; i++) { id = ids[i]; var data = this.body.data.nodes.get(id); var node = new Node(data, this.images, this.groups, this.constants); this.body.nodes[id] = node; // note: this may replace an existing node if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) { var radius = 10 * 0.1*ids.length + 10; var angle = 2 * Math.PI * Math.random(); if (node.xFixed == false) {node.x = radius * Math.cos(angle);} if (node.yFixed == false) {node.y = radius * Math.sin(angle);} } this.moving = true; } this._updateNodeIndexList(); if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { this._resetLevels(); this._setupHierarchicalLayout(); } this.physics._updateCalculationNodes(); this._reconnectEdges(); this._updateValueRange(this.body.nodes); }; /** * Update existing nodes, or create them when not yet existing * @param {Number[] | String[]} ids * @private */ Network.prototype._updateNodes = function(ids,changedData) { var nodes = this.body.nodes; for (var i = 0, len = ids.length; i < len; i++) { var id = ids[i]; var node = nodes[id]; var data = changedData[i]; if (node) { // update node node.setProperties(data, this.constants); } else { // create node node = new Node(properties, this.images, this.groups, this.constants); nodes[id] = node; } } this.moving = true; if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { this._resetLevels(); this._setupHierarchicalLayout(); } this._updateNodeIndexList(); this._updateValueRange(nodes); this._markAllEdgesAsDirty(); }; Network.prototype._markAllEdgesAsDirty = function() { for (var edgeId in this.body.edges) { this.body.edges[edgeId].colorDirty = true; } } /** * Remove existing nodes. If nodes do not exist, the method will just ignore it. * @param {Number[] | String[]} ids * @private */ Network.prototype._removeNodes = function(ids) { var nodes = this.body.nodes; // remove from selection for (var i = 0, len = ids.length; i < len; i++) { if (this.selectionObj.nodes[ids[i]] !== undefined) { this.body.nodes[ids[i]].unselect(); this._removeFromSelection(this.body.nodes[ids[i]]); } } for (var i = 0, len = ids.length; i < len; i++) { var id = ids[i]; delete nodes[id]; } this._updateNodeIndexList(); if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { this._resetLevels(); this._setupHierarchicalLayout(); } this.physics._updateCalculationNodes(); this._reconnectEdges(); this._updateSelection(); this._updateValueRange(nodes); }; /** * Load edges by reading the data table * @param {Array | DataSet | DataView} edges The data containing the edges. * @private * @private */ Network.prototype._setEdges = function(edges) { var oldEdgesData = this.body.data.edges; if (edges instanceof DataSet || edges instanceof DataView) { this.body.data.edges = edges; } else if (Array.isArray(edges)) { this.body.data.edges = new DataSet(); this.body.data.edges.add(edges); } else if (!edges) { this.body.data.edges = new DataSet(); } else { throw new TypeError('Array or DataSet expected'); } if (oldEdgesData) { // unsubscribe from old dataset util.forEach(this.edgesListeners, function (callback, event) { oldEdgesData.off(event, callback); }); } // remove drawn edges this.body.edges = {}; if (this.body.data.edges) { // subscribe to new dataset var me = this; util.forEach(this.edgesListeners, function (callback, event) { me.body.data.edges.on(event, callback); }); // draw all new nodes var ids = this.body.data.edges.getIds(); this._addEdges(ids); } this._reconnectEdges(); }; /** * Add edges * @param {Number[] | String[]} ids * @private */ Network.prototype._addEdges = function (ids) { var edges = this.body.edges, edgesData = this.body.data.edges; for (var i = 0, len = ids.length; i < len; i++) { var id = ids[i]; var oldEdge = edges[id]; if (oldEdge) { oldEdge.disconnect(); } var data = edgesData.get(id, {"showInternalIds" : true}); edges[id] = new Edge(data, this.body, this.constants); } this.moving = true; this._updateValueRange(edges); this._createBezierNodes(); this.physics._updateCalculationNodes(); if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { this._resetLevels(); this._setupHierarchicalLayout(); } }; /** * Update existing edges, or create them when not yet existing * @param {Number[] | String[]} ids * @private */ Network.prototype._updateEdges = function (ids) { var edges = this.body.edges; var edgesData = this.body.data.edges; for (var i = 0, len = ids.length; i < len; i++) { var id = ids[i]; var data = edgesData.get(id); var edge = edges[id]; if (edge) { // update edge edge.disconnect(); edge.setProperties(data); edge.connect(); } else { // create edge edge = new Edge(data, this.body, this.constants); this.body.edges[id] = edge; } } this._createBezierNodes(); if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { this._resetLevels(); this._setupHierarchicalLayout(); } this.moving = true; this._updateValueRange(edges); }; /** * Remove existing edges. Non existing ids will be ignored * @param {Number[] | String[]} ids * @private */ Network.prototype._removeEdges = function (ids) { var edges = this.body.edges; // remove from selection for (var i = 0, len = ids.length; i < len; i++) { if (this.selectionObj.edges[ids[i]] !== undefined) { edges[ids[i]].unselect(); this._removeFromSelection(edges[ids[i]]); } } for (var i = 0, len = ids.length; i < len; i++) { var id = ids[i]; var edge = edges[id]; if (edge) { if (edge.via != null) { delete this.body.supportNodes[edge.via.id]; } edge.disconnect(); delete edges[id]; } } this.moving = true; this._updateValueRange(edges); if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) { this._resetLevels(); this._setupHierarchicalLayout(); } this.physics._updateCalculationNodes(); }; /** * Reconnect all edges * @private */ Network.prototype._reconnectEdges = function() { var id, nodes = this.body.nodes, edges = this.body.edges; for (id in nodes) { if (nodes.hasOwnProperty(id)) { nodes[id].edges = []; } } for (id in edges) { if (edges.hasOwnProperty(id)) { var edge = edges[id]; edge.from = null; edge.to = null; edge.connect(); } } }; /** * Update the values of all object in the given array according to the current * value range of the objects in the array. * @param {Object} obj An object containing a set of Edges or Nodes * The objects must have a method getValue() and * setValueRange(min, max). * @private */ Network.prototype._updateValueRange = function(obj) { var id; // determine the range of the objects var valueMin = undefined; var valueMax = undefined; var valueTotal = 0; for (id in obj) { if (obj.hasOwnProperty(id)) { var value = obj[id].getValue(); if (value !== undefined) { valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin); valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax); valueTotal += value; } } } // adjust the range of all objects if (valueMin !== undefined && valueMax !== undefined) { for (id in obj) { if (obj.hasOwnProperty(id)) { obj[id].setValueRange(valueMin, valueMax, valueTotal); } } } }; /** * Set the translation of the network * @param {Number} offsetX Horizontal offset * @param {Number} offsetY Vertical offset * @private */ Network.prototype._setTranslation = function(offsetX, offsetY) { if (this.translation === undefined) { this.translation = { x: 0, y: 0 }; } if (offsetX !== undefined) { this.translation.x = offsetX; } if (offsetY !== undefined) { this.translation.y = offsetY; } this.emit('viewChanged'); }; /** * Get the translation of the network * @return {Object} translation An object with parameters x and y, both a number * @private */ Network.prototype._getTranslation = function() { return { x: this.translation.x, y: this.translation.y }; }; /** * Scale the network * @param {Number} scale Scaling factor 1.0 is unscaled * @private */ Network.prototype._setScale = function(scale) { this.scale = scale; }; /** * Get the current scale of the network * @return {Number} scale Scaling factor 1.0 is unscaled * @private */ Network.prototype._getScale = function() { return this.scale; }; /** * Move the network according to the keyboard presses. * * @private */ Network.prototype._handleNavigation = function() { if (this.xIncrement != 0 || this.yIncrement != 0) { var translation = this._getTranslation(); this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement); } if (this.zoomIncrement != 0) { var center = { x: this.frame.canvas.clientWidth / 2, y: this.frame.canvas.clientHeight / 2 }; this._zoom(this.scale*(1 + this.zoomIncrement), center); } }; /** * Freeze the _animationStep */ Network.prototype.freezeSimulation = function(freeze) { if (freeze == true) { this.freezeSimulationEnabled = true; this.moving = false; } else { this.freezeSimulationEnabled = false; this.moving = true; this.start(); } }; /** * This function cleans the support nodes if they are not needed and adds them when they are. * * @param {boolean} [disableStart] * @private */ Network.prototype._configureSmoothCurves = function(disableStart = true) { if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { this._createBezierNodes(); // cleanup unused support nodes for (let i = 0; i < this.body.supportNodeIndices.length; i++) { let nodeId = this.body.supportNodeIndices[i]; // delete support nodes for edges that have been deleted if (this.body.edges[this.body.supportNodes[nodeId].parentEdgeId] === undefined) { delete this.body.supportNodes[nodeId]; } } } else { // delete the support nodes this.body.supportNodes = {}; for (var edgeId in this.body.edges) { if (this.body.edges.hasOwnProperty(edgeId)) { this.body.edges[edgeId].via = null; } } } this._updateNodeIndexList(); this.physics._updateCalculationNodes(); if (!disableStart) { this.moving = true; this.start(); } }; /** * Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but * are used for the force calculation. * * @private */ Network.prototype._createBezierNodes = function(specificEdges = this.body.edges) { if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { for (var edgeId in specificEdges) { if (specificEdges.hasOwnProperty(edgeId)) { var edge = specificEdges[edgeId]; if (edge.via == null) { var nodeId = "edgeId:".concat(edge.id); var node = new Node( {id:nodeId, mass:1, shape:'circle', image:"", internalMultiplier:1 },{},{},this.constants); this.body.supportNodes[nodeId] = node; edge.via = node; edge.via.parentEdgeId = edge.id; edge.positionBezierNode(); } } } this._updateNodeIndexList(); } }; /** * load the functions that load the mixins into the prototype. * * @private */ Network.prototype._initializeMixinLoaders = function () { for (var mixin in MixinLoader) { if (MixinLoader.hasOwnProperty(mixin)) { Network.prototype[mixin] = MixinLoader[mixin]; } } }; /** * Load the XY positions of the nodes into the dataset. */ Network.prototype.storePosition = function() { console.log("storePosition is depricated: use .storePositions() from now on.") this.storePositions(); }; /** * Load the XY positions of the nodes into the dataset. */ Network.prototype.storePositions = function() { var dataArray = []; for (var nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { var node = this.body.nodes[nodeId]; var allowedToMoveX = !this.body.nodes.xFixed; var allowedToMoveY = !this.body.nodes.yFixed; if (this.body.data.nodes._data[nodeId].x != Math.round(node.x) || this.body.data.nodes._data[nodeId].y != Math.round(node.y)) { dataArray.push({id:nodeId,x:Math.round(node.x),y:Math.round(node.y),allowedToMoveX:allowedToMoveX,allowedToMoveY:allowedToMoveY}); } } } this.body.data.nodes.update(dataArray); }; /** * Return the positions of the nodes. */ Network.prototype.getPositions = function(ids) { var dataArray = {}; if (ids !== undefined) { if (Array.isArray(ids) == true) { for (var i = 0; i < ids.length; i++) { if (this.body.nodes[ids[i]] !== undefined) { var node = this.body.nodes[ids[i]]; dataArray[ids[i]] = {x: Math.round(node.x), y: Math.round(node.y)}; } } } else { if (this.body.nodes[ids] !== undefined) { var node = this.body.nodes[ids]; dataArray[ids] = {x: Math.round(node.x), y: Math.round(node.y)}; } } } else { for (var nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { var node = this.body.nodes[nodeId]; dataArray[nodeId] = {x: Math.round(node.x), y: Math.round(node.y)}; } } } return dataArray; }; /** * Returns true when the Network is active. * @returns {boolean} */ Network.prototype.isActive = function () { return !this.activator || this.activator.active; }; /** * Sets the scale * @returns {Number} */ Network.prototype.setScale = function () { return this._setScale(); }; /** * Returns the scale * @returns {Number} */ Network.prototype.getScale = function () { return this._getScale(); }; /** * Check if a node is a cluster. * @param nodeId * @returns {*} */ Network.prototype.isCluster = function(nodeId) { if (this.body.nodes[nodeId] !== undefined) { return this.body.nodes[nodeId].isCluster; } else { console.log("Node does not exist.") return false; } }; /** * Returns the scale * @returns {Number} */ Network.prototype.getCenterCoordinates = function () { return this.DOMtoCanvas({x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight}); }; Network.prototype.getBoundingBox = function(nodeId) { if (this.body.nodes[nodeId] !== undefined) { return this.body.nodes[nodeId].boundingBox; } } Network.prototype.getConnectedNodes = function(nodeId) { var nodeList = []; if (this.body.nodes[nodeId] !== undefined) { var node = this.body.nodes[nodeId]; var nodeObj = {nodeId : true}; // used to quickly check if node already exists for (var i = 0; i < node.edges.length; i++) { var edge = node.edges[i]; if (edge.toId == nodeId) { if (nodeObj[edge.fromId] === undefined) { nodeList.push(edge.fromId); nodeObj[edge.fromId] = true; } } else if (edge.fromId == nodeId) { if (nodeObj[edge.toId] === undefined) { nodeList.push(edge.toId) nodeObj[edge.toId] = true; } } } } return nodeList; } Network.prototype.getEdgesFromNode = function(nodeId) { var edgesList = []; if (this.body.nodes[nodeId] !== undefined) { var node = this.body.nodes[nodeId]; for (var i = 0; i < node.edges.length; i++) { edgesList.push(node.edges[i].id); } } return edgesList; } Network.prototype.generateColorObject = function(color) { return util.parseColor(color); } module.exports = Network;