diff --git a/HISTORY.md b/HISTORY.md index 00669ee7..4ff526f7 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -10,6 +10,7 @@ http://visjs.org - clarified docs, stressing importance of css inclusion for correct display of navigation an manipulation icons. - improved and expanded playing with physics (configurePhysics option). - added highlights to navigation icons if the corresponding key is pressed. +- added freezeForStabilization option to improve stabilization with cached positions. ## 2014-03-07, version 0.7.0 diff --git a/docs/graph.html b/docs/graph.html index df875131..97766870 100644 --- a/docs/graph.html +++ b/docs/graph.html @@ -352,7 +352,13 @@ var nodes = [ Url of an image. Only applicable when the shape of the node is image. - + + mass + number + 1 + When using the Barnes Hut simulation method (which is selected by default), + the mass of a node determines the gravitational repulsion during the simulation. Higher mass will push other nodes further away. + level number @@ -592,7 +598,12 @@ var edges = [ no Text label to be displayed halfway the edge. - + + length + number + physics.[method].springLength + The resting length of the edge when modeled as a spring. By default the springLength determined by the physics is used. By using this setting you can make certain edges have different resting lengths. + title string @@ -733,6 +744,17 @@ var options = { + + freezeForStabilization + Boolean + false + + With the advent of the storePosition() function, the positions of the nodes can be saved after they are stabilized. The smoothCurves require support nodes and those positions are not stored. In order + to speed up the initialization of the graph by using storePosition() and loading the nodes with the stored positions, the freezeForStabilization option freezes all nodes that have been supplied with + an x and y position in place during the stabilization. That way only the support nodes for the smooth curves have to stabilize, greatly speeding up the stabilization process with cached positions. + + + groups Object @@ -936,7 +958,13 @@ var options = { none Default image url for the nodes. only applicable to shape image. - + + mass + number + 1 + When using the Barnes Hut simulation method (which is selected by default), + the mass of a node determines the gravitational repulsion during the simulation. Higher mass will push other nodes further away. + level number @@ -1082,6 +1110,12 @@ var options = { Default length of a gap in pixels on a dashed line. Only applicable when the line style is dash-line. + + length + number + physics.[method].springLength + The resting length of the edge when modeled as a spring. By default the springLength determined by the physics is used. By using this setting you can make certain edges have different resting lengths. + style @@ -1979,6 +2013,15 @@ graph.off('select', onSelect); + + stabilized + Fired when the graph has been stabilized after initialization. This event can be used to trigger the .storePosition() function after stabilization. + + + + diff --git a/src/graph/Edge.js b/src/graph/Edge.js index f748455b..12fd5672 100644 --- a/src/graph/Edge.js +++ b/src/graph/Edge.js @@ -216,18 +216,22 @@ Edge.prototype.draw = function(ctx) { * @return {boolean} True if location is located on the edge */ Edge.prototype.isOverlappingWith = function(obj) { - var distMax = 10; - - var xFrom = this.from.x; - var yFrom = this.from.y; - var xTo = this.to.x; - var yTo = this.to.y; - var xObj = obj.left; - var yObj = obj.top; - - var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); - - return (dist < distMax); + if (this.connected == true) { + var distMax = 10; + var xFrom = this.from.x; + var yFrom = this.from.y; + var xTo = this.to.x; + var yTo = this.to.y; + var xObj = obj.left; + var yObj = obj.top; + + var dist = this._getDistanceToEdge(xFrom, yFrom, xTo, yTo, xObj, yObj); + + return (dist < distMax); + } + else { + return false + } }; diff --git a/src/graph/Graph.js b/src/graph/Graph.js index d7f6f25e..28ddfa39 100644 --- a/src/graph/Graph.js +++ b/src/graph/Graph.js @@ -146,6 +146,7 @@ function Graph (container, data, options) { nodeSpacing: 100, direction: "UD" // UD, DU, LR, RL }, + freezeForStabilization: false, smoothCurves: true, maxVelocity: 10, minVelocity: 0.1, // px/s @@ -474,6 +475,7 @@ Graph.prototype.setOptions = function (options) { if (options.stabilize !== undefined) {this.stabilize = options.stabilize;} if (options.selectable !== undefined) {this.selectable = options.selectable;} if (options.smoothCurves !== undefined) {this.constants.smoothCurves = options.smoothCurves;} + if (options.freezeForStabilization !== undefined) {this.constants.freezeForStabilization = options.freezeForStabilization;} if (options.configurePhysics !== undefined){this.constants.configurePhysics = options.configurePhysics;} if (options.stabilizationIterations !== undefined) {this.constants.stabilizationIterations = options.stabilizationIterations;} @@ -819,26 +821,24 @@ Graph.prototype._handleDragStart = function() { } // create an array with the selected nodes and their original location and status - for (var objectId in this.selectionObj) { - if (this.selectionObj.hasOwnProperty(objectId)) { - var object = this.selectionObj[objectId]; - if (object instanceof Node) { - 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; - - drag.selection.push(s); - } + 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; + + drag.selection.push(s); } } } @@ -1324,6 +1324,7 @@ Graph.prototype._removeNodes = function(ids) { delete nodes[id]; } this._updateNodeIndexList(); + this._updateCalculationNodes(); this._reconnectEdges(); this._updateSelection(); this._updateValueRange(nodes); @@ -1717,17 +1718,49 @@ Graph.prototype._drawEdges = function(ctx) { * @private */ Graph.prototype._stabilize = function() { + if (this.constants.freezeForStabilization == true) { + this._freezeDefinedNodes(); + } + // find stable position var count = 0; while (this.moving && count < this.constants.stabilizationIterations) { this._physicsTick(); count++; } - this.zoomExtent(false,true); + if (this.constants.freezeForStabilization == true) { + this._restoreFrozenNodes(); + } + this.emit("stabilized",{iterations:count}); }; +Graph.prototype._freezeDefinedNodes = function() { + var nodes = this.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + if (nodes[id].x != null && nodes[id].y != null) { + nodes[id].fixedData.x = nodes[id].xFixed; + nodes[id].fixedData.y = nodes[id].yFixed; + nodes[id].xFixed = true; + nodes[id].yFixed = true; + } + } + } +}; + +Graph.prototype._restoreFrozenNodes = function() { + var nodes = this.nodes; + for (var id in nodes) { + if (nodes.hasOwnProperty(id)) { + if (nodes[id].fixedData.x != null) { + nodes[id].xFixed = nodes[id].fixedData.x; + nodes[id].yFixed = nodes[id].fixedData.y; + } + } + } +}; /** @@ -1786,10 +1819,10 @@ Graph.prototype._physicsTick = function() { if (!this.freezeSimulation) { if (this.moving) { this._doInAllActiveSectors("_initializeForceCalculation"); + this._doInAllActiveSectors("_discreteStepNodes"); if (this.constants.smoothCurves) { this._doInSupportSector("_discreteStepNodes"); } - this._doInAllActiveSectors("_discreteStepNodes"); this._findCenter(this._getRange()) } } @@ -1923,6 +1956,7 @@ Graph.prototype._createBezierNodes = function() { {id:nodeId, mass:1, shape:'circle', + image:"", internalMultiplier:1 },{},{},this.constants); edge.via = this.sectors['support']['nodes'][nodeId]; diff --git a/src/graph/Node.js b/src/graph/Node.js index 741e9c37..3e2f2869 100644 --- a/src/graph/Node.js +++ b/src/graph/Node.js @@ -66,6 +66,7 @@ function Node(properties, imagelist, grouplist, constants) { this.minForce = constants.minForce; this.damping = constants.physics.damping; this.mass = 1; // kg + this.fixedData = {x:null,y:null}; this.setProperties(properties, constants); @@ -149,8 +150,6 @@ Node.prototype.setProperties = function(properties, constants) { // physics - if (properties.internalMultiplier !== undefined) {this.internalMultiplier = properties.internalMultiplier;} - if (properties.damping !== undefined) {this.dampingBase = properties.damping;} if (properties.mass !== undefined) {this.mass = properties.mass;} // navigation controls properties @@ -182,7 +181,7 @@ Node.prototype.setProperties = function(properties, constants) { if (properties.fontSize !== undefined) {this.fontSize = properties.fontSize;} if (properties.fontFace !== undefined) {this.fontFace = properties.fontFace;} - if (this.image !== undefined) { + if (this.image !== undefined && this.image != "") { if (this.imagelist) { this.imageObj = this.imagelist.load(this.image); } @@ -421,6 +420,9 @@ Node.prototype.discreteStepLimited = function(interval, maxVelocity) { this.vx = (Math.abs(this.vx) > maxVelocity) ? ((this.vx > 0) ? maxVelocity : -maxVelocity) : this.vx; this.x += this.vx * interval; // position } + else { + this.fx = 0; + } if (!this.yFixed) { var dy = this.damping * this.vy; // damping force @@ -429,6 +431,9 @@ Node.prototype.discreteStepLimited = function(interval, maxVelocity) { this.vy = (Math.abs(this.vy) > maxVelocity) ? ((this.vy > 0) ? maxVelocity : -maxVelocity) : this.vy; this.y += this.vy * interval; // position } + else { + this.fy = 0; + } }; /** diff --git a/src/graph/graphMixins/MixinLoader.js b/src/graph/graphMixins/MixinLoader.js index 6cf4f2dd..c4000fdb 100644 --- a/src/graph/graphMixins/MixinLoader.js +++ b/src/graph/graphMixins/MixinLoader.js @@ -96,7 +96,7 @@ var graphMixinLoaders = { * @private */ _loadSelectionSystem : function() { - this.selectionObj = { }; + this.selectionObj = {nodes:{},edges:{}}; this._loadMixin(SelectionMixin); }, diff --git a/src/graph/graphMixins/SelectionMixin.js b/src/graph/graphMixins/SelectionMixin.js index 194a99ae..9efaa064 100644 --- a/src/graph/graphMixins/SelectionMixin.js +++ b/src/graph/graphMixins/SelectionMixin.js @@ -131,7 +131,13 @@ var SelectionMixin = { * @private */ _addToSelection : function(obj) { - this.selectionObj[obj.id] = obj; + if (obj instanceof Node) { + this.selectionObj.nodes[obj.id] = obj; + } + else { + this.selectionObj.edges[obj.id] = obj; + } + }, @@ -142,7 +148,12 @@ var SelectionMixin = { * @private */ _removeFromSelection : function(obj) { - delete this.selectionObj[obj.id]; + if (obj instanceof Node) { + delete this.selectionObj.nodes[obj.id]; + } + else { + delete this.selectionObj.edges[obj.id]; + } }, @@ -156,13 +167,18 @@ var SelectionMixin = { if (doNotTrigger === undefined) { doNotTrigger = false; } - - for (var objectId in this.selectionObj) { - if (this.selectionObj.hasOwnProperty(objectId)) { - this.selectionObj[objectId].unselect(); + for(var nodeId in this.selectionObj.nodes) { + if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { + this.selectionObj.nodes[nodeId].unselect(); + } + } + for(var edgeId in this.selectionObj.edges) { + if(this.selectionObj.edges.hasOwnProperty(edgeId)) { + this.selectionObj.edges[edgeId].unselect();; } } - this.selectionObj = {}; + + this.selectionObj = {nodes:{},edges:{}}; if (doNotTrigger == false) { this.emit('select', this.getSelection()); @@ -180,13 +196,11 @@ var SelectionMixin = { doNotTrigger = false; } - for (var objectId in this.selectionObj) { - if (this.selectionObj.hasOwnProperty(objectId)) { - if (this.selectionObj[objectId] instanceof Node) { - if (this.selectionObj[objectId].clusterSize > 1) { - this.selectionObj[objectId].unselect(); - this._removeFromSelection(this.selectionObj[objectId]); - } + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (this.selectionObj.nodes[nodeId].clusterSize > 1) { + this.selectionObj.nodes[nodeId].unselect(); + this._removeFromSelection(this.selectionObj.nodes[nodeId]); } } } @@ -205,11 +219,9 @@ var SelectionMixin = { */ _getSelectedNodeCount : function() { var count = 0; - for (var objectId in this.selectionObj) { - if (this.selectionObj.hasOwnProperty(objectId)) { - if (this.selectionObj[objectId] instanceof Node) { - count += 1; - } + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; } } return count; @@ -222,11 +234,9 @@ var SelectionMixin = { * @private */ _getSelectedNode : function() { - for (var objectId in this.selectionObj) { - if (this.selectionObj.hasOwnProperty(objectId)) { - if (this.selectionObj[objectId] instanceof Node) { - return this.selectionObj[objectId]; - } + for (var nodeId in this.selectionObj.nodes) { + if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return this.selectionObj.nodes[nodeId]; } } return null; @@ -241,11 +251,9 @@ var SelectionMixin = { */ _getSelectedEdgeCount : function() { var count = 0; - for (var objectId in this.selectionObj) { - if (this.selectionObj.hasOwnProperty(objectId)) { - if (this.selectionObj[objectId] instanceof Edge) { - count += 1; - } + for (var edgeId in this.selectionObj.edges) { + if (this.selectionObj.edges.hasOwnProperty(edgeId)) { + count += 1; } } return count; @@ -260,8 +268,13 @@ var SelectionMixin = { */ _getSelectedObjectCount : function() { var count = 0; - for (var objectId in this.selectionObj) { - if (this.selectionObj.hasOwnProperty(objectId)) { + for(var nodeId in this.selectionObj.nodes) { + if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { + count += 1; + } + } + for(var edgeId in this.selectionObj.edges) { + if(this.selectionObj.edges.hasOwnProperty(edgeId)) { count += 1; } } @@ -275,8 +288,13 @@ var SelectionMixin = { * @private */ _selectionIsEmpty : function() { - for(var objectId in this.selectionObj) { - if(this.selectionObj.hasOwnProperty(objectId)) { + for(var nodeId in this.selectionObj.nodes) { + if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { + return false; + } + } + for(var edgeId in this.selectionObj.edges) { + if(this.selectionObj.edges.hasOwnProperty(edgeId)) { return false; } } @@ -291,12 +309,10 @@ var SelectionMixin = { * @private */ _clusterInSelection : function() { - for(var objectId in this.selectionObj) { - if(this.selectionObj.hasOwnProperty(objectId)) { - if (this.selectionObj[objectId] instanceof Node) { - if (this.selectionObj[objectId].clusterSize > 1) { - return true; - } + for(var nodeId in this.selectionObj.nodes) { + if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (this.selectionObj.nodes[nodeId].clusterSize > 1) { + return true; } } } @@ -477,11 +493,9 @@ var SelectionMixin = { */ getSelectedNodes : function() { var idArray = []; - for(var objectId in this.selectionObj) { - if(this.selectionObj.hasOwnProperty(objectId)) { - if (this.selectionObj[objectId] instanceof Node) { - idArray.push(objectId); - } + for(var nodeId in this.selectionObj.nodes) { + if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { + idArray.push(nodeId); } } return idArray @@ -495,14 +509,12 @@ var SelectionMixin = { */ getSelectedEdges : function() { var idArray = []; - for(var objectId in this.selectionObj) { - if(this.selectionObj.hasOwnProperty(objectId)) { - if (this.selectionObj[objectId] instanceof Edge) { - idArray.push(objectId); - } + for(var edgeId in this.selectionObj.edges) { + if(this.selectionObj.edges.hasOwnProperty(edgeId)) { + idArray.push(edgeId); } } - return idArray + return idArray; }, @@ -538,17 +550,17 @@ var SelectionMixin = { * @private */ _updateSelection : function () { - for(var objectId in this.selectionObj) { - if(this.selectionObj.hasOwnProperty(objectId)) { - if (this.selectionObj[objectId] instanceof Node) { - if (!this.nodes.hasOwnProperty(objectId)) { - delete this.selectionObj[objectId]; - } + for(var nodeId in this.selectionObj.nodes) { + if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { + if (!this.nodes.hasOwnProperty(nodeId)) { + delete this.selectionObj.nodes[nodeId]; } - else { // assuming only edges and nodes are selected - if (!this.edges.hasOwnProperty(objectId)) { - delete this.selectionObj[objectId]; - } + } + } + for(var edgeId in this.selectionObj.edges) { + if(this.selectionObj.edges.hasOwnProperty(edgeId)) { + if (!this.edges.hasOwnProperty(edgeId)) { + delete this.selectionObj.edges[edgeId]; } } } diff --git a/src/graph/graphMixins/physics/BarnesHut.js b/src/graph/graphMixins/physics/BarnesHut.js index 5a06e26d..1e21670d 100644 --- a/src/graph/graphMixins/physics/BarnesHut.js +++ b/src/graph/graphMixins/physics/BarnesHut.js @@ -133,6 +133,7 @@ var barnesHutMixin = { mass:0, range: {minX:centerX-halfRootSize,maxX:centerX+halfRootSize, minY:centerY-halfRootSize,maxY:centerY+halfRootSize}, + size: rootSize, calcSize: 1 / rootSize, children: {data:null}, @@ -209,7 +210,6 @@ var barnesHutMixin = { parentBranch.children[region].children.data.y == node.y) { node.x += Math.random(); node.y += Math.random(); - this._placeInTree(parentBranch,node, true); } else { this._splitBranch(parentBranch.children[region]);