| @ -1,613 +0,0 @@ | |||||
| var Node = require('../Node'); | |||||
| var Edge = require('../Edge'); | |||||
| var util = require('../../util'); | |||||
| /** | |||||
| * | |||||
| * @param hubsize | |||||
| * @param options | |||||
| */ | |||||
| exports.clusterByConnectionCount = function(hubsize, options) { | |||||
| if (hubsize === undefined) { | |||||
| hubsize = this._getHubSize(); | |||||
| } | |||||
| else if (tyepof(hubsize) == "object") { | |||||
| options = this._checkOptions(hubsize); | |||||
| hubsize = this._getHubSize(); | |||||
| } | |||||
| var nodesToCluster = []; | |||||
| for (var i = 0; i < this.nodeIndices.length; i++) { | |||||
| var node = this.nodes[this.nodeIndices[i]]; | |||||
| if (node.edges.length >= hubsize) { | |||||
| nodesToCluster.push(node.id); | |||||
| } | |||||
| } | |||||
| for (var i = 0; i < nodesToCluster.length; i++) { | |||||
| var node = this.nodes[nodesToCluster[i]]; | |||||
| this.clusterByConnection(node,options,{},{},true); | |||||
| } | |||||
| this._wrapUp(); | |||||
| } | |||||
| /** | |||||
| * loop over all nodes, check if they adhere to the condition and cluster if needed. | |||||
| * @param options | |||||
| * @param doNotUpdateCalculationNodes | |||||
| */ | |||||
| exports.clusterByNodeData = function(options, doNotUpdateCalculationNodes) { | |||||
| if (options === undefined) {throw new Error("Cannot call clusterByNodeData without options.");} | |||||
| if (options.joinCondition === undefined) {throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options.");} | |||||
| // check if the options object is fine, append if needed | |||||
| options = this._checkOptions(options); | |||||
| var childNodesObj = {}; | |||||
| var childEdgesObj = {} | |||||
| // collect the nodes that will be in the cluster | |||||
| for (var i = 0; i < this.nodeIndices.length; i++) { | |||||
| var nodeId = this.nodeIndices[i]; | |||||
| var clonedOptions = this._cloneOptions(nodeId); | |||||
| if (options.joinCondition(clonedOptions) == true) { | |||||
| childNodesObj[nodeId] = this.nodes[nodeId]; | |||||
| } | |||||
| } | |||||
| this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes); | |||||
| } | |||||
| /** | |||||
| * Cluster all nodes in the network that have only 1 edge | |||||
| * @param options | |||||
| * @param doNotUpdateCalculationNodes | |||||
| */ | |||||
| exports.clusterOutliers = function(options, doNotUpdateCalculationNodes) { | |||||
| options = this._checkOptions(options); | |||||
| var clusters = [] | |||||
| // collect the nodes that will be in the cluster | |||||
| for (var i = 0; i < this.nodeIndices.length; i++) { | |||||
| var childNodesObj = {}; | |||||
| var childEdgesObj = {}; | |||||
| var nodeId = this.nodeIndices[i]; | |||||
| if (this.nodes[nodeId].edges.length == 1) { | |||||
| var edge = this.nodes[nodeId].edges[0]; | |||||
| var childNodeId = this._getConnectedId(edge, nodeId); | |||||
| if (childNodeId != nodeId) { | |||||
| if (options.joinCondition === undefined) { | |||||
| childNodesObj[nodeId] = this.nodes[nodeId]; | |||||
| childNodesObj[childNodeId] = this.nodes[childNodeId]; | |||||
| } | |||||
| else { | |||||
| var clonedOptions = this._cloneOptions(nodeId); | |||||
| if (options.joinCondition(clonedOptions) == true) { | |||||
| childNodesObj[nodeId] = this.nodes[nodeId]; | |||||
| } | |||||
| clonedOptions = this._cloneOptions(childNodeId); | |||||
| if (options.joinCondition(clonedOptions) == true) { | |||||
| childNodesObj[childNodeId] = this.nodes[childNodeId]; | |||||
| } | |||||
| } | |||||
| clusters.push({nodes:childNodesObj, edges:childEdgesObj}) | |||||
| } | |||||
| } | |||||
| } | |||||
| for (var i = 0; i < clusters.length; i++) { | |||||
| this._cluster(clusters[i].nodes, clusters[i].edges, options, true) | |||||
| } | |||||
| if (doNotUpdateCalculationNodes !== true) { | |||||
| this._wrapUp(); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * | |||||
| * @param nodeId | |||||
| * @param options | |||||
| * @param doNotUpdateCalculationNodes | |||||
| */ | |||||
| exports.clusterByConnection = function(nodeId, options, doNotUpdateCalculationNodes) { | |||||
| // kill conditions | |||||
| if (nodeId === undefined) {throw new Error("No nodeId supplied to clusterByConnection!");} | |||||
| if (this.nodes[nodeId] === undefined) {throw new Error("The nodeId given to clusterByConnection does not exist!");} | |||||
| var node = this.nodes[nodeId]; | |||||
| options = this._checkOptions(options, node); | |||||
| if (options.clusterNodeProperties.x === undefined) {options.clusterNodeProperties.x = node.x; options.clusterNodeProperties.allowedToMoveX = !node.xFixed;} | |||||
| if (options.clusterNodeProperties.y === undefined) {options.clusterNodeProperties.y = node.y; options.clusterNodeProperties.allowedToMoveY = !node.yFixed;} | |||||
| var childNodesObj = {}; | |||||
| var childEdgesObj = {} | |||||
| var parentNodeId = node.id; | |||||
| var parentClonedOptions = this._cloneOptions(parentNodeId); | |||||
| childNodesObj[parentNodeId] = node; | |||||
| // collect the nodes that will be in the cluster | |||||
| for (var i = 0; i < node.edges.length; i++) { | |||||
| var edge = node.edges[i]; | |||||
| var childNodeId = this._getConnectedId(edge, parentNodeId); | |||||
| if (childNodeId !== parentNodeId) { | |||||
| if (options.joinCondition === undefined) { | |||||
| childEdgesObj[edge.id] = edge; | |||||
| childNodesObj[childNodeId] = this.nodes[childNodeId]; | |||||
| } | |||||
| else { | |||||
| // clone the options and insert some additional parameters that could be interesting. | |||||
| var childClonedOptions = this._cloneOptions(childNodeId); | |||||
| if (options.joinCondition(parentClonedOptions, childClonedOptions) == true) { | |||||
| childEdgesObj[edge.id] = edge; | |||||
| childNodesObj[childNodeId] = this.nodes[childNodeId]; | |||||
| } | |||||
| } | |||||
| } | |||||
| else { | |||||
| childEdgesObj[edge.id] = edge; | |||||
| } | |||||
| } | |||||
| this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes); | |||||
| } | |||||
| /** | |||||
| * This returns a clone of the options or properties of the edge or node to be used for construction of new edges or check functions for new nodes. | |||||
| * @param objId | |||||
| * @param type | |||||
| * @returns {{}} | |||||
| * @private | |||||
| */ | |||||
| exports._cloneOptions = function(objId, type) { | |||||
| var clonedOptions = {}; | |||||
| if (type === undefined || type == 'node') { | |||||
| util.deepExtend(clonedOptions, this.nodes[objId].options, true); | |||||
| util.deepExtend(clonedOptions, this.nodes[objId].properties, true); | |||||
| clonedOptions.amountOfConnections = this.nodes[objId].edges.length; | |||||
| } | |||||
| else { | |||||
| util.deepExtend(clonedOptions, this.edges[objId].properties, true); | |||||
| } | |||||
| return clonedOptions; | |||||
| } | |||||
| /** | |||||
| * This function creates the edges that will be attached to the cluster. | |||||
| * | |||||
| * @param childNodesObj | |||||
| * @param childEdgesObj | |||||
| * @param newEdges | |||||
| * @param options | |||||
| * @private | |||||
| */ | |||||
| exports._createClusterEdges = function (childNodesObj, childEdgesObj, newEdges, options) { | |||||
| var edge, childNodeId, childNode; | |||||
| var childKeys = Object.keys(childNodesObj); | |||||
| for (var i = 0; i < childKeys.length; i++) { | |||||
| childNodeId = childKeys[i]; | |||||
| childNode = childNodesObj[childNodeId]; | |||||
| // mark all edges for removal from global and construct new edges from the cluster to others | |||||
| for (var j = 0; j < childNode.edges.length; j++) { | |||||
| edge = childNode.edges[j]; | |||||
| childEdgesObj[edge.id] = edge; | |||||
| var otherNodeId = edge.toId; | |||||
| var otherOnTo = true; | |||||
| if (edge.toId != childNodeId) { | |||||
| otherNodeId = edge.toId; | |||||
| otherOnTo = true; | |||||
| } | |||||
| else if (edge.fromId != childNodeId) { | |||||
| otherNodeId = edge.fromId; | |||||
| otherOnTo = false; | |||||
| } | |||||
| if (childNodesObj[otherNodeId] === undefined) { | |||||
| var clonedOptions = this._cloneOptions(edge.id, 'edge'); | |||||
| util.deepExtend(clonedOptions, options.clusterEdgeProperties); | |||||
| // avoid forcing the default color on edges that inherit color | |||||
| if (edge.properties.color === undefined) { | |||||
| delete clonedOptions.color; | |||||
| } | |||||
| if (otherOnTo === true) { | |||||
| clonedOptions.from = options.clusterNodeProperties.id; | |||||
| clonedOptions.to = otherNodeId; | |||||
| } | |||||
| else { | |||||
| clonedOptions.from = otherNodeId; | |||||
| clonedOptions.to = options.clusterNodeProperties.id; | |||||
| } | |||||
| clonedOptions.id = 'clusterEdge:' + util.randomUUID(); | |||||
| newEdges.push(new Edge(clonedOptions,this,this.constants)) | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| /** | |||||
| * This function checks the options that can be supplied to the different cluster functions | |||||
| * for certain fields and inserts defaults if needed | |||||
| * @param options | |||||
| * @returns {*} | |||||
| * @private | |||||
| */ | |||||
| exports._checkOptions = function(options) { | |||||
| if (options === undefined) {options = {};} | |||||
| if (options.clusterEdgeProperties === undefined) {options.clusterEdgeProperties = {};} | |||||
| if (options.clusterNodeProperties === undefined) {options.clusterNodeProperties = {};} | |||||
| return options; | |||||
| } | |||||
| /** | |||||
| * | |||||
| * @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node | |||||
| * @param {Object} childEdgesObj | object with edge objects, id as keys | |||||
| * @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties} | |||||
| * @param {Boolean} doNotUpdateCalculationNodes | when true, do not wrap up | |||||
| * @private | |||||
| */ | |||||
| exports._cluster = function(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes) { | |||||
| // kill condition: no children so cant cluster | |||||
| if (Object.keys(childNodesObj).length == 0) {return;} | |||||
| // check if we have an unique id; | |||||
| if (options.clusterNodeProperties.id === undefined) {options.clusterNodeProperties.id = 'cluster:' + util.randomUUID();} | |||||
| var clusterId = options.clusterNodeProperties.id; | |||||
| // create the new edges that will connect to the cluster | |||||
| var newEdges = []; | |||||
| this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, options); | |||||
| // construct the clusterNodeProperties | |||||
| var clusterNodeProperties = options.clusterNodeProperties; | |||||
| if (options.processProperties !== undefined) { | |||||
| // get the childNode options | |||||
| var childNodesOptions = []; | |||||
| for (var nodeId in childNodesObj) { | |||||
| var clonedOptions = this._cloneOptions(nodeId); | |||||
| childNodesOptions.push(clonedOptions); | |||||
| } | |||||
| // get clusterproperties based on childNodes | |||||
| var childEdgesOptions = []; | |||||
| for (var edgeId in childEdgesObj) { | |||||
| var clonedOptions = this._cloneOptions(edgeId, 'edge'); | |||||
| childEdgesOptions.push(clonedOptions); | |||||
| } | |||||
| clusterNodeProperties = options.processProperties(clusterNodeProperties, childNodesOptions, childEdgesOptions); | |||||
| if (!clusterNodeProperties) { | |||||
| throw new Error("The processClusterProperties function does not return properties!"); | |||||
| } | |||||
| } | |||||
| if (clusterNodeProperties.label === undefined) { | |||||
| clusterNodeProperties.label = 'cluster'; | |||||
| } | |||||
| // give the clusterNode a postion if it does not have one. | |||||
| var pos = undefined | |||||
| if (clusterNodeProperties.x === undefined) { | |||||
| pos = this._getClusterPosition(childNodesObj); | |||||
| clusterNodeProperties.x = pos.x; | |||||
| clusterNodeProperties.allowedToMoveX = true; | |||||
| } | |||||
| if (clusterNodeProperties.x === undefined) { | |||||
| if (pos === undefined) { | |||||
| pos = this._getClusterPosition(childNodesObj); | |||||
| } | |||||
| clusterNodeProperties.y = pos.y; | |||||
| clusterNodeProperties.allowedToMoveY = true; | |||||
| } | |||||
| // force the ID to remain the same | |||||
| clusterNodeProperties.id = clusterId; | |||||
| // create the clusterNode | |||||
| var clusterNode = new Node(clusterNodeProperties, this.images, this.groups, this.constants); | |||||
| clusterNode.isCluster = true; | |||||
| clusterNode.containedNodes = childNodesObj; | |||||
| clusterNode.containedEdges = childEdgesObj; | |||||
| // delete contained edges from global | |||||
| for (var edgeId in childEdgesObj) { | |||||
| if (childEdgesObj.hasOwnProperty(edgeId)) { | |||||
| if (this.edges[edgeId] !== undefined) { | |||||
| if (this.edges[edgeId].via !== null) { | |||||
| var viaId = this.edges[edgeId].via.id; | |||||
| if (viaId) { | |||||
| this.edges[edgeId].via = null | |||||
| delete this.sectors['support']['nodes'][viaId]; | |||||
| } | |||||
| } | |||||
| this.edges[edgeId].disconnect(); | |||||
| delete this.edges[edgeId]; | |||||
| } | |||||
| } | |||||
| } | |||||
| // remove contained nodes from global | |||||
| for (var nodeId in childNodesObj) { | |||||
| if (childNodesObj.hasOwnProperty(nodeId)) { | |||||
| this.clusteredNodes[nodeId] = {clusterId:clusterNodeProperties.id, node: this.nodes[nodeId]}; | |||||
| delete this.nodes[nodeId]; | |||||
| } | |||||
| } | |||||
| // finally put the cluster node into global | |||||
| this.nodes[clusterNodeProperties.id] = clusterNode; | |||||
| // push new edges to global | |||||
| for (var i = 0; i < newEdges.length; i++) { | |||||
| this.edges[newEdges[i].id] = newEdges[i]; | |||||
| this.edges[newEdges[i].id].connect(); | |||||
| } | |||||
| // create bezier nodes for smooth curves if needed | |||||
| this._createBezierNodes(newEdges); | |||||
| // set ID to undefined so no duplicates arise | |||||
| clusterNodeProperties.id = undefined; | |||||
| // wrap up | |||||
| if (doNotUpdateCalculationNodes !== true) { | |||||
| this._wrapUp(); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * get the position of the cluster node based on what's inside | |||||
| * @param {object} childNodesObj | object with node objects, id as keys | |||||
| * @returns {{x: number, y: number}} | |||||
| * @private | |||||
| */ | |||||
| exports._getClusterPosition = function(childNodesObj) { | |||||
| var childKeys = Object.keys(childNodesObj); | |||||
| var minX = childNodesObj[childKeys[0]].x; | |||||
| var maxX = childNodesObj[childKeys[0]].x; | |||||
| var minY = childNodesObj[childKeys[0]].y; | |||||
| var maxY = childNodesObj[childKeys[0]].y; | |||||
| var node; | |||||
| for (var i = 0; i < childKeys.lenght; i++) { | |||||
| node = childNodesObj[childKeys[0]]; | |||||
| minX = node.x < minX ? node.x : minX; | |||||
| maxX = node.x > maxX ? node.x : maxX; | |||||
| minY = node.y < minY ? node.y : minY; | |||||
| maxY = node.y > maxY ? node.y : maxY; | |||||
| } | |||||
| return {x: 0.5*(minX + maxX), y: 0.5*(minY + maxY)}; | |||||
| } | |||||
| /** | |||||
| * Open a cluster by calling this function. | |||||
| * @param {String} clusterNodeId | the ID of the cluster node | |||||
| * @param {Boolean} doNotUpdateCalculationNodes | wrap up afterwards if not true | |||||
| */ | |||||
| exports.openCluster = function(clusterNodeId, doNotUpdateCalculationNodes) { | |||||
| // kill conditions | |||||
| if (clusterNodeId === undefined) {throw new Error("No clusterNodeId supplied to openCluster.");} | |||||
| if (this.nodes[clusterNodeId] === undefined) {throw new Error("The clusterNodeId supplied to openCluster does not exist.");} | |||||
| if (this.nodes[clusterNodeId].containedNodes === undefined) {console.log("The node:" + clusterNodeId + " is not a cluster."); return}; | |||||
| var node = this.nodes[clusterNodeId]; | |||||
| var containedNodes = node.containedNodes; | |||||
| var containedEdges = node.containedEdges; | |||||
| // release nodes | |||||
| for (var nodeId in containedNodes) { | |||||
| if (containedNodes.hasOwnProperty(nodeId)) { | |||||
| this.nodes[nodeId] = containedNodes[nodeId]; | |||||
| // inherit position | |||||
| this.nodes[nodeId].x = node.x; | |||||
| this.nodes[nodeId].y = node.y; | |||||
| // inherit speed | |||||
| this.nodes[nodeId].vx = node.vx; | |||||
| this.nodes[nodeId].vy = node.vy; | |||||
| delete this.clusteredNodes[nodeId]; | |||||
| } | |||||
| } | |||||
| // release edges | |||||
| for (var edgeId in containedEdges) { | |||||
| if (containedEdges.hasOwnProperty(edgeId)) { | |||||
| this.edges[edgeId] = containedEdges[edgeId]; | |||||
| this.edges[edgeId].connect(); | |||||
| var edge = this.edges[edgeId]; | |||||
| if (edge.connected === false) { | |||||
| if (this.clusteredNodes[edge.fromId] !== undefined) { | |||||
| this._connectEdge(edge, edge.fromId, true); | |||||
| } | |||||
| if (this.clusteredNodes[edge.toId] !== undefined) { | |||||
| this._connectEdge(edge, edge.toId, false); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| this._createBezierNodes(containedEdges); | |||||
| var edgeIds = []; | |||||
| for (var i = 0; i < node.edges.length; i++) { | |||||
| edgeIds.push(node.edges[i].id); | |||||
| } | |||||
| // remove edges in clusterNode | |||||
| for (var i = 0; i < edgeIds.length; i++) { | |||||
| var edge = this.edges[edgeIds[i]]; | |||||
| // if the edge should have been connected to a contained node | |||||
| if (edge.fromArray.length > 0 && edge.fromId == clusterNodeId) { | |||||
| // the node in the from array was contained in the cluster | |||||
| if (this.nodes[edge.fromArray[0].id] !== undefined) { | |||||
| this._connectEdge(edge, edge.fromArray[0].id, true); | |||||
| } | |||||
| } | |||||
| else if (edge.toArray.length > 0 && edge.toId == clusterNodeId) { | |||||
| // the node in the to array was contained in the cluster | |||||
| if (this.nodes[edge.toArray[0].id] !== undefined) { | |||||
| this._connectEdge(edge, edge.toArray[0].id, false); | |||||
| } | |||||
| } | |||||
| else { | |||||
| var edgeId = edgeIds[i]; | |||||
| var viaId = this.edges[edgeId].via.id; | |||||
| if (viaId) { | |||||
| this.edges[edgeId].via = null | |||||
| delete this.sectors['support']['nodes'][viaId]; | |||||
| } | |||||
| // this removes the edge from node.edges, which is why edgeIds is formed | |||||
| this.edges[edgeId].disconnect(); | |||||
| delete this.edges[edgeId]; | |||||
| } | |||||
| } | |||||
| // remove clusterNode | |||||
| delete this.nodes[clusterNodeId]; | |||||
| if (doNotUpdateCalculationNodes !== true) { | |||||
| this._wrapUp(); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Recalculate navigation nodes, color edges dirty, update nodes list etc. | |||||
| * @private | |||||
| */ | |||||
| exports._wrapUp = function() { | |||||
| this._updateNodeIndexList(); | |||||
| this._updateCalculationNodes(); | |||||
| this._markAllEdgesAsDirty(); | |||||
| if (this.initializing !== true) { | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to | |||||
| * is currently residing in cluster B | |||||
| * @param edge | |||||
| * @param nodeId | |||||
| * @param from | |||||
| * @private | |||||
| */ | |||||
| exports._connectEdge = function(edge, nodeId, from) { | |||||
| var clusterStack = this._getClusterStack(nodeId); | |||||
| if (from == true) { | |||||
| edge.from = clusterStack[clusterStack.length - 1]; | |||||
| edge.fromId = clusterStack[clusterStack.length - 1].id; | |||||
| clusterStack.pop() | |||||
| edge.fromArray = clusterStack; | |||||
| } | |||||
| else { | |||||
| edge.to = clusterStack[clusterStack.length - 1]; | |||||
| edge.toId = clusterStack[clusterStack.length - 1].id; | |||||
| clusterStack.pop(); | |||||
| edge.toArray = clusterStack; | |||||
| } | |||||
| edge.connect(); | |||||
| } | |||||
| /** | |||||
| * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node | |||||
| * @param nodeId | |||||
| * @returns {Array} | |||||
| * @private | |||||
| */ | |||||
| exports._getClusterStack = function(nodeId) { | |||||
| var stack = []; | |||||
| var max = 100; | |||||
| var counter = 0; | |||||
| while (this.clusteredNodes[nodeId] !== undefined && counter < max) { | |||||
| stack.push(this.clusteredNodes[nodeId].node); | |||||
| nodeId = this.clusteredNodes[nodeId].clusterId; | |||||
| counter++; | |||||
| } | |||||
| stack.push(this.nodes[nodeId]); | |||||
| return stack; | |||||
| } | |||||
| /** | |||||
| * Get the Id the node is connected to | |||||
| * @param edge | |||||
| * @param nodeId | |||||
| * @returns {*} | |||||
| * @private | |||||
| */ | |||||
| exports._getConnectedId = function(edge, nodeId) { | |||||
| if (edge.toId != nodeId) { | |||||
| return edge.toId; | |||||
| } | |||||
| else if (edge.fromId != nodeId) { | |||||
| return edge.fromId; | |||||
| } | |||||
| else { | |||||
| return edge.fromId; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * We determine how many connections denote an important hub. | |||||
| * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%) | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._getHubSize = function() { | |||||
| var average = 0; | |||||
| var averageSquared = 0; | |||||
| var hubCounter = 0; | |||||
| var largestHub = 0; | |||||
| for (var i = 0; i < this.nodeIndices.length; i++) { | |||||
| var node = this.nodes[this.nodeIndices[i]]; | |||||
| if (node.edges.length > largestHub) { | |||||
| largestHub = node.edges.length; | |||||
| } | |||||
| average += node.edges.length; | |||||
| averageSquared += Math.pow(node.edges.length,2); | |||||
| hubCounter += 1; | |||||
| } | |||||
| average = average / hubCounter; | |||||
| averageSquared = averageSquared / hubCounter; | |||||
| var variance = averageSquared - Math.pow(average,2); | |||||
| var standardDeviation = Math.sqrt(variance); | |||||
| var hubThreshold = Math.floor(average + 2*standardDeviation); | |||||
| // always have at least one to cluster | |||||
| if (hubThreshold > largestHub) { | |||||
| hubThreshold = largestHub; | |||||
| } | |||||
| return hubThreshold; | |||||
| }; | |||||
| @ -1,399 +0,0 @@ | |||||
| /** | |||||
| * This function calculates the forces the nodes apply on eachother based on a gravitational model. | |||||
| * The Barnes Hut method is used to speed up this N-body simulation. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._calculateNodeForces = function() { | |||||
| if (this.constants.physics.barnesHut.gravitationalConstant != 0) { | |||||
| var node; | |||||
| var nodes = this.calculationNodes; | |||||
| var nodeIndices = this.calculationNodeIndices; | |||||
| var nodeCount = nodeIndices.length; | |||||
| this._formBarnesHutTree(nodes,nodeIndices); | |||||
| var barnesHutTree = this.barnesHutTree; | |||||
| // place the nodes one by one recursively | |||||
| for (var i = 0; i < nodeCount; i++) { | |||||
| node = nodes[nodeIndices[i]]; | |||||
| if (node.options.mass > 0) { | |||||
| // starting with root is irrelevant, it never passes the BarnesHutSolver condition | |||||
| this._getForceContribution(barnesHutTree.root.children.NW,node); | |||||
| this._getForceContribution(barnesHutTree.root.children.NE,node); | |||||
| this._getForceContribution(barnesHutTree.root.children.SW,node); | |||||
| this._getForceContribution(barnesHutTree.root.children.SE,node); | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. | |||||
| * If a region contains a single node, we check if it is not itself, then we apply the force. | |||||
| * | |||||
| * @param parentBranch | |||||
| * @param node | |||||
| * @private | |||||
| */ | |||||
| exports._getForceContribution = function(parentBranch,node) { | |||||
| // we get no force contribution from an empty region | |||||
| if (parentBranch.childrenCount > 0) { | |||||
| var dx,dy,distance; | |||||
| // get the distance from the center of mass to the node. | |||||
| dx = parentBranch.centerOfMass.x - node.x; | |||||
| dy = parentBranch.centerOfMass.y - node.y; | |||||
| distance = Math.sqrt(dx * dx + dy * dy); | |||||
| // BarnesHutSolver condition | |||||
| // original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed | |||||
| // calcSize = 1/s --> d * 1/s > 1/theta = passed | |||||
| if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.thetaInverted) { | |||||
| // duplicate code to reduce function calls to speed up program | |||||
| if (distance == 0) { | |||||
| distance = 0.1*Math.random(); | |||||
| dx = distance; | |||||
| } | |||||
| var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); | |||||
| var fx = dx * gravityForce; | |||||
| var fy = dy * gravityForce; | |||||
| node.fx += fx; | |||||
| node.fy += fy; | |||||
| } | |||||
| else { | |||||
| // Did not pass the condition, go into children if available | |||||
| if (parentBranch.childrenCount == 4) { | |||||
| this._getForceContribution(parentBranch.children.NW,node); | |||||
| this._getForceContribution(parentBranch.children.NE,node); | |||||
| this._getForceContribution(parentBranch.children.SW,node); | |||||
| this._getForceContribution(parentBranch.children.SE,node); | |||||
| } | |||||
| else { // parentBranch must have only one node, if it was empty we wouldnt be here | |||||
| if (parentBranch.children.data.id != node.id) { // if it is not self | |||||
| // duplicate code to reduce function calls to speed up program | |||||
| if (distance == 0) { | |||||
| distance = 0.5*Math.random(); | |||||
| dx = distance; | |||||
| } | |||||
| var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); | |||||
| var fx = dx * gravityForce; | |||||
| var fy = dy * gravityForce; | |||||
| node.fx += fx; | |||||
| node.fy += fy; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. | |||||
| * | |||||
| * @param nodes | |||||
| * @param nodeIndices | |||||
| * @private | |||||
| */ | |||||
| exports._formBarnesHutTree = function(nodes,nodeIndices) { | |||||
| var node; | |||||
| var nodeCount = nodeIndices.length; | |||||
| var minX = Number.MAX_VALUE, | |||||
| minY = Number.MAX_VALUE, | |||||
| maxX =-Number.MAX_VALUE, | |||||
| maxY =-Number.MAX_VALUE; | |||||
| // get the range of the nodes | |||||
| for (var i = 0; i < nodeCount; i++) { | |||||
| var x = nodes[nodeIndices[i]].x; | |||||
| var y = nodes[nodeIndices[i]].y; | |||||
| if (nodes[nodeIndices[i]].options.mass > 0) { | |||||
| if (x < minX) { minX = x; } | |||||
| if (x > maxX) { maxX = x; } | |||||
| if (y < minY) { minY = y; } | |||||
| if (y > maxY) { maxY = y; } | |||||
| } | |||||
| } | |||||
| // make the range a square | |||||
| var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y | |||||
| if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize | |||||
| else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize | |||||
| var minimumTreeSize = 1e-5; | |||||
| var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX)); | |||||
| var halfRootSize = 0.5 * rootSize; | |||||
| var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY); | |||||
| // construct the barnesHutTree | |||||
| var barnesHutTree = { | |||||
| root:{ | |||||
| centerOfMass: {x:0, y:0}, | |||||
| mass:0, | |||||
| range: { | |||||
| minX: centerX-halfRootSize,maxX:centerX+halfRootSize, | |||||
| minY: centerY-halfRootSize,maxY:centerY+halfRootSize | |||||
| }, | |||||
| size: rootSize, | |||||
| calcSize: 1 / rootSize, | |||||
| children: { data:null}, | |||||
| maxWidth: 0, | |||||
| level: 0, | |||||
| childrenCount: 4 | |||||
| } | |||||
| }; | |||||
| this._splitBranch(barnesHutTree.root); | |||||
| // place the nodes one by one recursively | |||||
| for (i = 0; i < nodeCount; i++) { | |||||
| node = nodes[nodeIndices[i]]; | |||||
| if (node.options.mass > 0) { | |||||
| this._placeInTree(barnesHutTree.root,node); | |||||
| } | |||||
| } | |||||
| // make global | |||||
| this.barnesHutTree = barnesHutTree | |||||
| }; | |||||
| /** | |||||
| * this updates the mass of a branch. this is increased by adding a node. | |||||
| * | |||||
| * @param parentBranch | |||||
| * @param node | |||||
| * @private | |||||
| */ | |||||
| exports._updateBranchMass = function(parentBranch, node) { | |||||
| var totalMass = parentBranch.mass + node.options.mass; | |||||
| var totalMassInv = 1/totalMass; | |||||
| parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; | |||||
| parentBranch.centerOfMass.x *= totalMassInv; | |||||
| parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; | |||||
| parentBranch.centerOfMass.y *= totalMassInv; | |||||
| parentBranch.mass = totalMass; | |||||
| var biggestSize = Math.max(Math.max(node.height,node.radius),node.width); | |||||
| parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth; | |||||
| }; | |||||
| /** | |||||
| * determine in which branch the node will be placed. | |||||
| * | |||||
| * @param parentBranch | |||||
| * @param node | |||||
| * @param skipMassUpdate | |||||
| * @private | |||||
| */ | |||||
| exports._placeInTree = function(parentBranch,node,skipMassUpdate) { | |||||
| if (skipMassUpdate != true || skipMassUpdate === undefined) { | |||||
| // update the mass of the branch. | |||||
| this._updateBranchMass(parentBranch,node); | |||||
| } | |||||
| if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW | |||||
| if (parentBranch.children.NW.range.maxY > node.y) { // in NW | |||||
| this._placeInRegion(parentBranch,node,"NW"); | |||||
| } | |||||
| else { // in SW | |||||
| this._placeInRegion(parentBranch,node,"SW"); | |||||
| } | |||||
| } | |||||
| else { // in NE or SE | |||||
| if (parentBranch.children.NW.range.maxY > node.y) { // in NE | |||||
| this._placeInRegion(parentBranch,node,"NE"); | |||||
| } | |||||
| else { // in SE | |||||
| this._placeInRegion(parentBranch,node,"SE"); | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * actually place the node in a region (or branch) | |||||
| * | |||||
| * @param parentBranch | |||||
| * @param node | |||||
| * @param region | |||||
| * @private | |||||
| */ | |||||
| exports._placeInRegion = function(parentBranch,node,region) { | |||||
| switch (parentBranch.children[region].childrenCount) { | |||||
| case 0: // place node here | |||||
| parentBranch.children[region].children.data = node; | |||||
| parentBranch.children[region].childrenCount = 1; | |||||
| this._updateBranchMass(parentBranch.children[region],node); | |||||
| break; | |||||
| case 1: // convert into children | |||||
| // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) | |||||
| // we move one node a pixel and we do not put it in the tree. | |||||
| if (parentBranch.children[region].children.data.x == node.x && | |||||
| parentBranch.children[region].children.data.y == node.y) { | |||||
| node.x += Math.random(); | |||||
| node.y += Math.random(); | |||||
| } | |||||
| else { | |||||
| this._splitBranch(parentBranch.children[region]); | |||||
| this._placeInTree(parentBranch.children[region],node); | |||||
| } | |||||
| break; | |||||
| case 4: // place in branch | |||||
| this._placeInTree(parentBranch.children[region],node); | |||||
| break; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch | |||||
| * after the split is complete. | |||||
| * | |||||
| * @param parentBranch | |||||
| * @private | |||||
| */ | |||||
| exports._splitBranch = function(parentBranch) { | |||||
| // if the branch is shaded with a node, replace the node in the new subset. | |||||
| var containedNode = null; | |||||
| if (parentBranch.childrenCount == 1) { | |||||
| containedNode = parentBranch.children.data; | |||||
| parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0; | |||||
| } | |||||
| parentBranch.childrenCount = 4; | |||||
| parentBranch.children.data = null; | |||||
| this._insertRegion(parentBranch,"NW"); | |||||
| this._insertRegion(parentBranch,"NE"); | |||||
| this._insertRegion(parentBranch,"SW"); | |||||
| this._insertRegion(parentBranch,"SE"); | |||||
| if (containedNode != null) { | |||||
| this._placeInTree(parentBranch,containedNode); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This function subdivides the region into four new segments. | |||||
| * Specifically, this inserts a single new segment. | |||||
| * It fills the children section of the parentBranch | |||||
| * | |||||
| * @param parentBranch | |||||
| * @param region | |||||
| * @param parentRange | |||||
| * @private | |||||
| */ | |||||
| exports._insertRegion = function(parentBranch, region) { | |||||
| var minX,maxX,minY,maxY; | |||||
| var childSize = 0.5 * parentBranch.size; | |||||
| switch (region) { | |||||
| case "NW": | |||||
| minX = parentBranch.range.minX; | |||||
| maxX = parentBranch.range.minX + childSize; | |||||
| minY = parentBranch.range.minY; | |||||
| maxY = parentBranch.range.minY + childSize; | |||||
| break; | |||||
| case "NE": | |||||
| minX = parentBranch.range.minX + childSize; | |||||
| maxX = parentBranch.range.maxX; | |||||
| minY = parentBranch.range.minY; | |||||
| maxY = parentBranch.range.minY + childSize; | |||||
| break; | |||||
| case "SW": | |||||
| minX = parentBranch.range.minX; | |||||
| maxX = parentBranch.range.minX + childSize; | |||||
| minY = parentBranch.range.minY + childSize; | |||||
| maxY = parentBranch.range.maxY; | |||||
| break; | |||||
| case "SE": | |||||
| minX = parentBranch.range.minX + childSize; | |||||
| maxX = parentBranch.range.maxX; | |||||
| minY = parentBranch.range.minY + childSize; | |||||
| maxY = parentBranch.range.maxY; | |||||
| break; | |||||
| } | |||||
| parentBranch.children[region] = { | |||||
| centerOfMass:{x:0,y:0}, | |||||
| mass:0, | |||||
| range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY}, | |||||
| size: 0.5 * parentBranch.size, | |||||
| calcSize: 2 * parentBranch.calcSize, | |||||
| children: {data:null}, | |||||
| maxWidth: 0, | |||||
| level: parentBranch.level+1, | |||||
| childrenCount: 0 | |||||
| }; | |||||
| }; | |||||
| /** | |||||
| * This function is for debugging purposed, it draws the tree. | |||||
| * | |||||
| * @param ctx | |||||
| * @param color | |||||
| * @private | |||||
| */ | |||||
| exports._drawTree = function(ctx,color) { | |||||
| if (this.barnesHutTree !== undefined) { | |||||
| ctx.lineWidth = 1; | |||||
| this._drawBranch(this.barnesHutTree.root,ctx,color); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This function is for debugging purposes. It draws the branches recursively. | |||||
| * | |||||
| * @param branch | |||||
| * @param ctx | |||||
| * @param color | |||||
| * @private | |||||
| */ | |||||
| exports._drawBranch = function(branch,ctx,color) { | |||||
| if (color === undefined) { | |||||
| color = "#FF0000"; | |||||
| } | |||||
| if (branch.childrenCount == 4) { | |||||
| this._drawBranch(branch.children.NW,ctx); | |||||
| this._drawBranch(branch.children.NE,ctx); | |||||
| this._drawBranch(branch.children.SE,ctx); | |||||
| this._drawBranch(branch.children.SW,ctx); | |||||
| } | |||||
| ctx.strokeStyle = color; | |||||
| ctx.beginPath(); | |||||
| ctx.moveTo(branch.range.minX,branch.range.minY); | |||||
| ctx.lineTo(branch.range.maxX,branch.range.minY); | |||||
| ctx.stroke(); | |||||
| ctx.beginPath(); | |||||
| ctx.moveTo(branch.range.maxX,branch.range.minY); | |||||
| ctx.lineTo(branch.range.maxX,branch.range.maxY); | |||||
| ctx.stroke(); | |||||
| ctx.beginPath(); | |||||
| ctx.moveTo(branch.range.maxX,branch.range.maxY); | |||||
| ctx.lineTo(branch.range.minX,branch.range.maxY); | |||||
| ctx.stroke(); | |||||
| ctx.beginPath(); | |||||
| ctx.moveTo(branch.range.minX,branch.range.maxY); | |||||
| ctx.lineTo(branch.range.minX,branch.range.minY); | |||||
| ctx.stroke(); | |||||
| /* | |||||
| if (branch.mass > 0) { | |||||
| ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); | |||||
| ctx.stroke(); | |||||
| } | |||||
| */ | |||||
| }; | |||||
| @ -1,154 +0,0 @@ | |||||
| /** | |||||
| * Calculate the forces the nodes apply on eachother based on a repulsion field. | |||||
| * This field is linearly approximated. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._calculateNodeForces = function () { | |||||
| var dx, dy, distance, fx, fy, | |||||
| repulsingForce, node1, node2, i, j; | |||||
| var nodes = this.calculationNodes; | |||||
| var nodeIndices = this.calculationNodeIndices; | |||||
| // repulsing forces between nodes | |||||
| var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance; | |||||
| // we loop from i over all but the last entree in the array | |||||
| // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j | |||||
| for (i = 0; i < nodeIndices.length - 1; i++) { | |||||
| node1 = nodes[nodeIndices[i]]; | |||||
| for (j = i + 1; j < nodeIndices.length; j++) { | |||||
| node2 = nodes[nodeIndices[j]]; | |||||
| // nodes only affect nodes on their level | |||||
| if (node1.level == node2.level) { | |||||
| dx = node2.x - node1.x; | |||||
| dy = node2.y - node1.y; | |||||
| distance = Math.sqrt(dx * dx + dy * dy); | |||||
| var steepness = 0.05; | |||||
| if (distance < nodeDistance) { | |||||
| repulsingForce = -Math.pow(steepness*distance,2) + Math.pow(steepness*nodeDistance,2); | |||||
| } | |||||
| else { | |||||
| repulsingForce = 0; | |||||
| } | |||||
| // normalize force with | |||||
| if (distance == 0) { | |||||
| distance = 0.01; | |||||
| } | |||||
| else { | |||||
| repulsingForce = repulsingForce / distance; | |||||
| } | |||||
| fx = dx * repulsingForce; | |||||
| fy = dy * repulsingForce; | |||||
| node1.fx -= fx; | |||||
| node1.fy -= fy; | |||||
| node2.fx += fx; | |||||
| node2.fy += fy; | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * this function calculates the effects of the springs in the case of unsmooth curves. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._calculateHierarchicalSpringForces = function () { | |||||
| var edgeLength, edge, edgeId; | |||||
| var dx, dy, fx, fy, springForce, distance; | |||||
| var edges = this.edges; | |||||
| var nodes = this.calculationNodes; | |||||
| var nodeIndices = this.calculationNodeIndices; | |||||
| for (var i = 0; i < nodeIndices.length; i++) { | |||||
| var node1 = nodes[nodeIndices[i]]; | |||||
| node1.springFx = 0; | |||||
| node1.springFy = 0; | |||||
| } | |||||
| // forces caused by the edges, modelled as springs | |||||
| for (edgeId in edges) { | |||||
| if (edges.hasOwnProperty(edgeId)) { | |||||
| edge = edges[edgeId]; | |||||
| if (edge.connected === true) { | |||||
| // only calculate forces if nodes are in the same sector | |||||
| if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { | |||||
| edgeLength = edge.physics.springLength; | |||||
| // this implies that the edges between big clusters are longer | |||||
| edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth; | |||||
| dx = (edge.from.x - edge.to.x); | |||||
| dy = (edge.from.y - edge.to.y); | |||||
| distance = Math.sqrt(dx * dx + dy * dy); | |||||
| if (distance == 0) { | |||||
| distance = 0.01; | |||||
| } | |||||
| // the 1/distance is so the fx and fy can be calculated without sine or cosine. | |||||
| springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; | |||||
| fx = dx * springForce; | |||||
| fy = dy * springForce; | |||||
| if (edge.to.level != edge.from.level) { | |||||
| edge.to.springFx -= fx; | |||||
| edge.to.springFy -= fy; | |||||
| edge.from.springFx += fx; | |||||
| edge.from.springFy += fy; | |||||
| } | |||||
| else { | |||||
| var factor = 0.5; | |||||
| edge.to.fx -= factor*fx; | |||||
| edge.to.fy -= factor*fy; | |||||
| edge.from.fx += factor*fx; | |||||
| edge.from.fy += factor*fy; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| // normalize spring forces | |||||
| var springForce = 1; | |||||
| var springFx, springFy; | |||||
| for (i = 0; i < nodeIndices.length; i++) { | |||||
| var node = nodes[nodeIndices[i]]; | |||||
| springFx = Math.min(springForce,Math.max(-springForce,node.springFx)); | |||||
| springFy = Math.min(springForce,Math.max(-springForce,node.springFy)); | |||||
| node.fx += springFx; | |||||
| node.fy += springFy; | |||||
| } | |||||
| // retain energy balance | |||||
| var totalFx = 0; | |||||
| var totalFy = 0; | |||||
| for (i = 0; i < nodeIndices.length; i++) { | |||||
| var node = nodes[nodeIndices[i]]; | |||||
| totalFx += node.fx; | |||||
| totalFy += node.fy; | |||||
| } | |||||
| var correctionFx = totalFx / nodeIndices.length; | |||||
| var correctionFy = totalFy / nodeIndices.length; | |||||
| for (i = 0; i < nodeIndices.length; i++) { | |||||
| var node = nodes[nodeIndices[i]]; | |||||
| node.fx -= correctionFx; | |||||
| node.fy -= correctionFy; | |||||
| } | |||||
| }; | |||||
| @ -1,710 +0,0 @@ | |||||
| var util = require('../../../util'); | |||||
| var RepulsionMixin = require('./RepulsionMixin'); | |||||
| var HierarchialRepulsionMixin = require('./HierarchialRepulsionMixin'); | |||||
| var BarnesHutMixin = require('./BarnesHutMixin'); | |||||
| /** | |||||
| * Toggling barnes Hut calculation on and off. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._toggleBarnesHut = function () { | |||||
| this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled; | |||||
| this._loadSelectedForceSolver(); | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| }; | |||||
| /** | |||||
| * This loads the node force solver based on the barnes hut or repulsion algorithm | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._loadSelectedForceSolver = function () { | |||||
| // this overloads the this._calculateNodeForces | |||||
| if (this.constants.physics.barnesHut.enabled == true) { | |||||
| this._clearMixin(RepulsionMixin); | |||||
| this._clearMixin(HierarchialRepulsionMixin); | |||||
| this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity; | |||||
| this.constants.physics.springLength = this.constants.physics.barnesHut.springLength; | |||||
| this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant; | |||||
| this.constants.physics.damping = this.constants.physics.barnesHut.damping; | |||||
| this._loadMixin(BarnesHutMixin); | |||||
| } | |||||
| else if (this.constants.physics.hierarchicalRepulsion.enabled == true) { | |||||
| this._clearMixin(BarnesHutMixin); | |||||
| this._clearMixin(RepulsionMixin); | |||||
| this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity; | |||||
| this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength; | |||||
| this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant; | |||||
| this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping; | |||||
| this._loadMixin(HierarchialRepulsionMixin); | |||||
| } | |||||
| else { | |||||
| this._clearMixin(BarnesHutMixin); | |||||
| this._clearMixin(HierarchialRepulsionMixin); | |||||
| this.barnesHutTree = undefined; | |||||
| this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity; | |||||
| this.constants.physics.springLength = this.constants.physics.repulsion.springLength; | |||||
| this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant; | |||||
| this.constants.physics.damping = this.constants.physics.repulsion.damping; | |||||
| this._loadMixin(RepulsionMixin); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Before calculating the forces, we check if we need to cluster to keep up performance and we check | |||||
| * if there is more than one node. If it is just one node, we dont calculate anything. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._initializeForceCalculation = function () { | |||||
| // stop calculation if there is only one node | |||||
| if (this.calculationNodeIndices.length == 1) { | |||||
| this.body.nodes[this.calculationNodeIndices[0]]._setForce(0, 0); | |||||
| } | |||||
| else { | |||||
| // we now start the force calculation | |||||
| this._calculateForces(); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Calculate the external forces acting on the nodes | |||||
| * Forces are caused by: edges, repulsing forces between nodes, gravity | |||||
| * @private | |||||
| */ | |||||
| exports._calculateForces = function () { | |||||
| // Gravity is required to keep separated groups from floating off | |||||
| // the forces are reset to zero in this loop by using _setForce instead | |||||
| // of _addForce | |||||
| this._calculateGravitationalForces(); | |||||
| this._calculateNodeForces(); | |||||
| if (this.constants.physics.springConstant > 0) { | |||||
| if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { | |||||
| this._calculateSpringForcesWithSupport(); | |||||
| } | |||||
| else { | |||||
| if (this.constants.physics.hierarchicalRepulsion.enabled == true) { | |||||
| this._calculateHierarchicalSpringForces(); | |||||
| } | |||||
| else { | |||||
| this._calculateSpringForces(); | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also | |||||
| * handled in the calculateForces function. We then use a quadratic curve with the center node as control. | |||||
| * This function joins the datanodes and invisible (called support) nodes into one object. | |||||
| * We do this so we do not contaminate this.body.nodes with the support nodes. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._updateCalculationNodes = function () { | |||||
| if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { | |||||
| this.calculationNodes = {}; | |||||
| this.calculationNodeIndices = []; | |||||
| for (var nodeId in this.body.nodes) { | |||||
| if (this.body.nodes.hasOwnProperty(nodeId)) { | |||||
| this.calculationNodes[nodeId] = this.body.nodes[nodeId]; | |||||
| } | |||||
| } | |||||
| var supportNodes = this.body.sectors['support']['nodes']; | |||||
| for (var supportNodeId in supportNodes) { | |||||
| if (supportNodes.hasOwnProperty(supportNodeId)) { | |||||
| if (this.body.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) { | |||||
| this.calculationNodes[supportNodeId] = supportNodes[supportNodeId]; | |||||
| } | |||||
| else { | |||||
| supportNodes[supportNodeId]._setForce(0, 0); | |||||
| } | |||||
| } | |||||
| } | |||||
| this.calculationNodeIndices = Object.keys(this.calculationNodes); | |||||
| } | |||||
| else { | |||||
| this.calculationNodes = this.body.nodes; | |||||
| this.calculationNodeIndices = this.body.nodeIndices; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * this function applies the central gravity effect to keep groups from floating off | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._calculateGravitationalForces = function () { | |||||
| var dx, dy, distance, node, i; | |||||
| var nodes = this.calculationNodes; | |||||
| var gravity = this.constants.physics.centralGravity; | |||||
| var gravityForce = 0; | |||||
| for (i = 0; i < this.calculationNodeIndices.length; i++) { | |||||
| node = nodes[this.calculationNodeIndices[i]]; | |||||
| node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters. | |||||
| // gravity does not apply when we are in a pocket sector | |||||
| if (this._sector() == "default" && gravity != 0) { | |||||
| dx = -node.x; | |||||
| dy = -node.y; | |||||
| distance = Math.sqrt(dx * dx + dy * dy); | |||||
| gravityForce = (distance == 0) ? 0 : (gravity / distance); | |||||
| node.fx = dx * gravityForce; | |||||
| node.fy = dy * gravityForce; | |||||
| } | |||||
| else { | |||||
| node.fx = 0; | |||||
| node.fy = 0; | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * this function calculates the effects of the springs in the case of unsmooth curves. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._calculateSpringForces = function () { | |||||
| var edgeLength, edge, edgeId; | |||||
| var dx, dy, fx, fy, springForce, distance; | |||||
| var edges = this.body.edges; | |||||
| // forces caused by the edges, modelled as springs | |||||
| for (edgeId in edges) { | |||||
| if (edges.hasOwnProperty(edgeId)) { | |||||
| edge = edges[edgeId]; | |||||
| if (edge.connected === true) { | |||||
| // only calculate forces if nodes are in the same sector | |||||
| if (this.body.nodes.hasOwnProperty(edge.toId) && this.body.nodes.hasOwnProperty(edge.fromId)) { | |||||
| edgeLength = edge.physics.springLength; | |||||
| dx = (edge.from.x - edge.to.x); | |||||
| dy = (edge.from.y - edge.to.y); | |||||
| distance = Math.sqrt(dx * dx + dy * dy); | |||||
| if (distance == 0) { | |||||
| distance = 0.01; | |||||
| } | |||||
| // the 1/distance is so the fx and fy can be calculated without sine or cosine. | |||||
| springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; | |||||
| fx = dx * springForce; | |||||
| fy = dy * springForce; | |||||
| edge.from.fx += fx; | |||||
| edge.from.fy += fy; | |||||
| edge.to.fx -= fx; | |||||
| edge.to.fy -= fy; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This function calculates the springforces on the nodes, accounting for the support nodes. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._calculateSpringForcesWithSupport = function () { | |||||
| var edgeLength, edge, edgeId; | |||||
| var edges = this.body.edges; | |||||
| var calculationNodes = this.calculationNodes; | |||||
| // forces caused by the edges, modelled as springs | |||||
| for (edgeId in edges) { | |||||
| if (edges.hasOwnProperty(edgeId)) { | |||||
| edge = edges[edgeId]; | |||||
| if (edge.connected === true) { | |||||
| // only calculate forces if nodes are in the same sector | |||||
| if (calculationNodes[edge.toId] !== undefined && calculationNodes[edge.fromId] !== undefined) { | |||||
| if (edge.via != null) { | |||||
| var node1 = edge.to; | |||||
| var node2 = edge.via; | |||||
| var node3 = edge.from; | |||||
| edgeLength = edge.physics.springLength; | |||||
| this._calculateSpringForce(node1, node2, 0.5 * edgeLength); | |||||
| this._calculateSpringForce(node2, node3, 0.5 * edgeLength); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This is the code actually performing the calculation for the function above. It is split out to avoid repetition. | |||||
| * | |||||
| * @param node1 | |||||
| * @param node2 | |||||
| * @param edgeLength | |||||
| * @private | |||||
| */ | |||||
| exports._calculateSpringForce = function (node1, node2, edgeLength) { | |||||
| var dx, dy, fx, fy, springForce, distance; | |||||
| dx = (node1.x - node2.x); | |||||
| dy = (node1.y - node2.y); | |||||
| distance = Math.sqrt(dx * dx + dy * dy); | |||||
| if (distance == 0) { | |||||
| distance = 0.01; | |||||
| } | |||||
| // the 1/distance is so the fx and fy can be calculated without sine or cosine. | |||||
| springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; | |||||
| fx = dx * springForce; | |||||
| fy = dy * springForce; | |||||
| node1.fx += fx; | |||||
| node1.fy += fy; | |||||
| node2.fx -= fx; | |||||
| node2.fy -= fy; | |||||
| }; | |||||
| exports._cleanupPhysicsConfiguration = function() { | |||||
| if (this.physicsConfiguration !== undefined) { | |||||
| while (this.physicsConfiguration.hasChildNodes()) { | |||||
| this.physicsConfiguration.removeChild(this.physicsConfiguration.firstChild); | |||||
| } | |||||
| this.physicsConfiguration.parentNode.removeChild(this.physicsConfiguration); | |||||
| this.physicsConfiguration = undefined; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Load the HTML for the physics config and bind it | |||||
| * @private | |||||
| */ | |||||
| exports._loadPhysicsConfiguration = function () { | |||||
| if (this.physicsConfiguration === undefined) { | |||||
| this.backupConstants = {}; | |||||
| util.deepExtend(this.backupConstants,this.constants); | |||||
| var maxGravitational = Math.max(20000, (-1 * this.constants.physics.barnesHut.gravitationalConstant) * 10); | |||||
| var maxSpring = Math.min(0.05, this.constants.physics.barnesHut.springConstant * 10) | |||||
| var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"]; | |||||
| this.physicsConfiguration = document.createElement('div'); | |||||
| this.physicsConfiguration.className = "PhysicsConfiguration"; | |||||
| this.physicsConfiguration.innerHTML = '' + | |||||
| '<table><tr><td><b>Simulation Mode:</b></td></tr>' + | |||||
| '<tr>' + | |||||
| '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod1" value="BH" checked="checked">Barnes Hut</td>' + | |||||
| '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod2" value="R">Repulsion</td>' + | |||||
| '<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod3" value="H">Hierarchical</td>' + | |||||
| '</tr>' + | |||||
| '</table>' + | |||||
| '<table id="graph_BH_table" style="display:none">' + | |||||
| '<tr><td><b>Barnes Hut</b></td></tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">gravitationalConstant</td><td>0</td><td><input type="range" min="0" max="'+maxGravitational+'" value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" step="25" style="width:300px" id="graph_BH_gc"></td><td width="50px">-'+maxGravitational+'</td><td><input value="' + (this.constants.physics.barnesHut.gravitationalConstant) + '" id="graph_BH_gc_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="6" value="' + this.constants.physics.barnesHut.centralGravity + '" step="0.05" style="width:300px" id="graph_BH_cg"></td><td>3</td><td><input value="' + this.constants.physics.barnesHut.centralGravity + '" id="graph_BH_cg_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.barnesHut.springLength + '" step="1" style="width:300px" id="graph_BH_sl"></td><td>500</td><td><input value="' + this.constants.physics.barnesHut.springLength + '" id="graph_BH_sl_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="'+maxSpring+'" value="' + this.constants.physics.barnesHut.springConstant + '" step="0.0001" style="width:300px" id="graph_BH_sc"></td><td>'+maxSpring+'</td><td><input value="' + this.constants.physics.barnesHut.springConstant + '" id="graph_BH_sc_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.barnesHut.damping + '" step="0.005" style="width:300px" id="graph_BH_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.barnesHut.damping + '" id="graph_BH_damp_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '</table>' + | |||||
| '<table id="graph_R_table" style="display:none">' + | |||||
| '<tr><td><b>Repulsion</b></td></tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.repulsion.nodeDistance + '" step="1" style="width:300px" id="graph_R_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.repulsion.nodeDistance + '" id="graph_R_nd_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.repulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_R_cg"></td><td>3</td><td><input value="' + this.constants.physics.repulsion.centralGravity + '" id="graph_R_cg_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.repulsion.springLength + '" step="1" style="width:300px" id="graph_R_sl"></td><td>500</td><td><input value="' + this.constants.physics.repulsion.springLength + '" id="graph_R_sl_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.repulsion.springConstant + '" step="0.001" style="width:300px" id="graph_R_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.repulsion.springConstant + '" id="graph_R_sc_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.repulsion.damping + '" step="0.005" style="width:300px" id="graph_R_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.repulsion.damping + '" id="graph_R_damp_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '</table>' + | |||||
| '<table id="graph_H_table" style="display:none">' + | |||||
| '<tr><td width="150"><b>Hierarchical</b></td></tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" step="1" style="width:300px" id="graph_H_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" id="graph_H_nd_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_H_cg"></td><td>3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" id="graph_H_cg_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" step="1" style="width:300px" id="graph_H_sl"></td><td>500</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" id="graph_H_sl_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" step="0.001" style="width:300px" id="graph_H_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" id="graph_H_sc_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.hierarchicalRepulsion.damping + '" step="0.005" style="width:300px" id="graph_H_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.damping + '" id="graph_H_damp_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">direction</td><td>1</td><td><input type="range" min="0" max="3" value="' + hierarchicalLayoutDirections.indexOf(this.constants.hierarchicalLayout.direction) + '" step="1" style="width:300px" id="graph_H_direction"></td><td>4</td><td><input value="' + this.constants.hierarchicalLayout.direction + '" id="graph_H_direction_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">levelSeparation</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.levelSeparation + '" step="1" style="width:300px" id="graph_H_levsep"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.levelSeparation + '" id="graph_H_levsep_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '<tr>' + | |||||
| '<td width="150px">nodeSpacing</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.nodeSpacing + '" step="1" style="width:300px" id="graph_H_nspac"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.nodeSpacing + '" id="graph_H_nspac_value" style="width:60px"></td>' + | |||||
| '</tr>' + | |||||
| '</table>' + | |||||
| '<table><tr><td><b>Options:</b></td></tr>' + | |||||
| '<tr>' + | |||||
| '<td width="180px"><input type="button" id="graph_toggleSmooth" value="Toggle smoothCurves" style="width:150px"></td>' + | |||||
| '<td width="180px"><input type="button" id="graph_repositionNodes" value="Reinitialize" style="width:150px"></td>' + | |||||
| '<td width="180px"><input type="button" id="graph_generateOptions" value="Generate Options" style="width:150px"></td>' + | |||||
| '</tr>' + | |||||
| '</table>' | |||||
| this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement); | |||||
| this.optionsDiv = document.createElement("div"); | |||||
| this.optionsDiv.style.fontSize = "14px"; | |||||
| this.optionsDiv.style.fontFamily = "verdana"; | |||||
| this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement); | |||||
| var rangeElement; | |||||
| rangeElement = document.getElementById('graph_BH_gc'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant"); | |||||
| rangeElement = document.getElementById('graph_BH_cg'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity"); | |||||
| rangeElement = document.getElementById('graph_BH_sc'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant"); | |||||
| rangeElement = document.getElementById('graph_BH_sl'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength"); | |||||
| rangeElement = document.getElementById('graph_BH_damp'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping"); | |||||
| rangeElement = document.getElementById('graph_R_nd'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance"); | |||||
| rangeElement = document.getElementById('graph_R_cg'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity"); | |||||
| rangeElement = document.getElementById('graph_R_sc'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant"); | |||||
| rangeElement = document.getElementById('graph_R_sl'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength"); | |||||
| rangeElement = document.getElementById('graph_R_damp'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping"); | |||||
| rangeElement = document.getElementById('graph_H_nd'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance"); | |||||
| rangeElement = document.getElementById('graph_H_cg'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity"); | |||||
| rangeElement = document.getElementById('graph_H_sc'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant"); | |||||
| rangeElement = document.getElementById('graph_H_sl'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength"); | |||||
| rangeElement = document.getElementById('graph_H_damp'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping"); | |||||
| rangeElement = document.getElementById('graph_H_direction'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction"); | |||||
| rangeElement = document.getElementById('graph_H_levsep'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation"); | |||||
| rangeElement = document.getElementById('graph_H_nspac'); | |||||
| rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing"); | |||||
| var radioButton1 = document.getElementById("graph_physicsMethod1"); | |||||
| var radioButton2 = document.getElementById("graph_physicsMethod2"); | |||||
| var radioButton3 = document.getElementById("graph_physicsMethod3"); | |||||
| radioButton2.checked = true; | |||||
| if (this.constants.physics.barnesHut.enabled) { | |||||
| radioButton1.checked = true; | |||||
| } | |||||
| if (this.constants.hierarchicalLayout.enabled) { | |||||
| radioButton3.checked = true; | |||||
| } | |||||
| var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); | |||||
| var graph_repositionNodes = document.getElementById("graph_repositionNodes"); | |||||
| var graph_generateOptions = document.getElementById("graph_generateOptions"); | |||||
| graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this); | |||||
| graph_repositionNodes.onclick = graphRepositionNodes.bind(this); | |||||
| graph_generateOptions.onclick = graphGenerateOptions.bind(this); | |||||
| if (this.constants.smoothCurves == true && this.constants.dynamicSmoothCurves == false) { | |||||
| graph_toggleSmooth.style.background = "#A4FF56"; | |||||
| } | |||||
| else { | |||||
| graph_toggleSmooth.style.background = "#FF8532"; | |||||
| } | |||||
| switchConfigurations.apply(this); | |||||
| radioButton1.onchange = switchConfigurations.bind(this); | |||||
| radioButton2.onchange = switchConfigurations.bind(this); | |||||
| radioButton3.onchange = switchConfigurations.bind(this); | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * This overwrites the this.constants. | |||||
| * | |||||
| * @param constantsVariableName | |||||
| * @param value | |||||
| * @private | |||||
| */ | |||||
| exports._overWriteGraphConstants = function (constantsVariableName, value) { | |||||
| var nameArray = constantsVariableName.split("_"); | |||||
| if (nameArray.length == 1) { | |||||
| this.constants[nameArray[0]] = value; | |||||
| } | |||||
| else if (nameArray.length == 2) { | |||||
| this.constants[nameArray[0]][nameArray[1]] = value; | |||||
| } | |||||
| else if (nameArray.length == 3) { | |||||
| this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value; | |||||
| } | |||||
| }; | |||||
| /** | |||||
| * this function is bound to the toggle smooth curves button. That is also why it is not in the prototype. | |||||
| */ | |||||
| function graphToggleSmoothCurves () { | |||||
| this.constants.smoothCurves.enabled = !this.constants.smoothCurves.enabled; | |||||
| var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); | |||||
| if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";} | |||||
| else {graph_toggleSmooth.style.background = "#FF8532";} | |||||
| this._configureSmoothCurves(false); | |||||
| } | |||||
| /** | |||||
| * this function is used to scramble the nodes | |||||
| * | |||||
| */ | |||||
| function graphRepositionNodes () { | |||||
| for (var nodeId in this.calculationNodes) { | |||||
| if (this.calculationNodes.hasOwnProperty(nodeId)) { | |||||
| this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0; | |||||
| this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0; | |||||
| } | |||||
| } | |||||
| if (this.constants.hierarchicalLayout.enabled == true) { | |||||
| this._setupHierarchicalLayout(); | |||||
| showValueOfRange.call(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance"); | |||||
| showValueOfRange.call(this, 'graph_H_cg', 1, "physics_centralGravity"); | |||||
| showValueOfRange.call(this, 'graph_H_sc', 1, "physics_springConstant"); | |||||
| showValueOfRange.call(this, 'graph_H_sl', 1, "physics_springLength"); | |||||
| showValueOfRange.call(this, 'graph_H_damp', 1, "physics_damping"); | |||||
| } | |||||
| else { | |||||
| this.repositionNodes(); | |||||
| } | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| /** | |||||
| * this is used to generate an options file from the playing with physics system. | |||||
| */ | |||||
| function graphGenerateOptions () { | |||||
| var options = "No options are required, default values used."; | |||||
| var optionsSpecific = []; | |||||
| var radioButton1 = document.getElementById("graph_physicsMethod1"); | |||||
| var radioButton2 = document.getElementById("graph_physicsMethod2"); | |||||
| if (radioButton1.checked == true) { | |||||
| if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);} | |||||
| if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} | |||||
| if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} | |||||
| if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} | |||||
| if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} | |||||
| if (optionsSpecific.length != 0) { | |||||
| options = "var options = {"; | |||||
| options += "physics: {barnesHut: {"; | |||||
| for (var i = 0; i < optionsSpecific.length; i++) { | |||||
| options += optionsSpecific[i]; | |||||
| if (i < optionsSpecific.length - 1) { | |||||
| options += ", " | |||||
| } | |||||
| } | |||||
| options += '}}' | |||||
| } | |||||
| if (this.constants.smoothCurves.enabled != this.backupConstants.smoothCurves.enabled) { | |||||
| if (optionsSpecific.length == 0) {options = "var options = {";} | |||||
| else {options += ", "} | |||||
| options += "smoothCurves: " + this.constants.smoothCurves.enabled; | |||||
| } | |||||
| if (options != "No options are required, default values used.") { | |||||
| options += '};' | |||||
| } | |||||
| } | |||||
| else if (radioButton2.checked == true) { | |||||
| options = "var options = {"; | |||||
| options += "physics: {barnesHut: {enabled: false}"; | |||||
| if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);} | |||||
| if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} | |||||
| if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} | |||||
| if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} | |||||
| if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} | |||||
| if (optionsSpecific.length != 0) { | |||||
| options += ", repulsion: {"; | |||||
| for (var i = 0; i < optionsSpecific.length; i++) { | |||||
| options += optionsSpecific[i]; | |||||
| if (i < optionsSpecific.length - 1) { | |||||
| options += ", " | |||||
| } | |||||
| } | |||||
| options += '}}' | |||||
| } | |||||
| if (optionsSpecific.length == 0) {options += "}"} | |||||
| if (this.constants.smoothCurves != this.backupConstants.smoothCurves) { | |||||
| options += ", smoothCurves: " + this.constants.smoothCurves; | |||||
| } | |||||
| options += '};' | |||||
| } | |||||
| else { | |||||
| options = "var options = {"; | |||||
| if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);} | |||||
| if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} | |||||
| if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} | |||||
| if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} | |||||
| if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} | |||||
| if (optionsSpecific.length != 0) { | |||||
| options += "physics: {hierarchicalRepulsion: {"; | |||||
| for (var i = 0; i < optionsSpecific.length; i++) { | |||||
| options += optionsSpecific[i]; | |||||
| if (i < optionsSpecific.length - 1) { | |||||
| options += ", "; | |||||
| } | |||||
| } | |||||
| options += '}},'; | |||||
| } | |||||
| options += 'hierarchicalLayout: {'; | |||||
| optionsSpecific = []; | |||||
| if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);} | |||||
| if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);} | |||||
| if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);} | |||||
| if (optionsSpecific.length != 0) { | |||||
| for (var i = 0; i < optionsSpecific.length; i++) { | |||||
| options += optionsSpecific[i]; | |||||
| if (i < optionsSpecific.length - 1) { | |||||
| options += ", " | |||||
| } | |||||
| } | |||||
| options += '}' | |||||
| } | |||||
| else { | |||||
| options += "enabled:true}"; | |||||
| } | |||||
| options += '};' | |||||
| } | |||||
| this.optionsDiv.innerHTML = options; | |||||
| } | |||||
| /** | |||||
| * this is used to switch between barnesHut, repulsion and hierarchical. | |||||
| * | |||||
| */ | |||||
| function switchConfigurations () { | |||||
| var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"]; | |||||
| var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value; | |||||
| var tableId = "graph_" + radioButton + "_table"; | |||||
| var table = document.getElementById(tableId); | |||||
| table.style.display = "block"; | |||||
| for (var i = 0; i < ids.length; i++) { | |||||
| if (ids[i] != tableId) { | |||||
| table = document.getElementById(ids[i]); | |||||
| table.style.display = "none"; | |||||
| } | |||||
| } | |||||
| this._restoreNodes(); | |||||
| if (radioButton == "R") { | |||||
| this.constants.hierarchicalLayout.enabled = false; | |||||
| this.constants.physics.hierarchicalRepulsion.enabled = false; | |||||
| this.constants.physics.barnesHut.enabled = false; | |||||
| } | |||||
| else if (radioButton == "H") { | |||||
| if (this.constants.hierarchicalLayout.enabled == false) { | |||||
| this.constants.hierarchicalLayout.enabled = true; | |||||
| this.constants.physics.hierarchicalRepulsion.enabled = true; | |||||
| this.constants.physics.barnesHut.enabled = false; | |||||
| this.constants.smoothCurves.enabled = false; | |||||
| this._setupHierarchicalLayout(); | |||||
| } | |||||
| } | |||||
| else { | |||||
| this.constants.hierarchicalLayout.enabled = false; | |||||
| this.constants.physics.hierarchicalRepulsion.enabled = false; | |||||
| this.constants.physics.barnesHut.enabled = true; | |||||
| } | |||||
| this._loadSelectedForceSolver(); | |||||
| var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); | |||||
| if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";} | |||||
| else {graph_toggleSmooth.style.background = "#FF8532";} | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| /** | |||||
| * this generates the ranges depending on the iniital values. | |||||
| * | |||||
| * @param id | |||||
| * @param map | |||||
| * @param constantsVariableName | |||||
| */ | |||||
| function showValueOfRange (id,map,constantsVariableName) { | |||||
| var valueId = id + "_value"; | |||||
| var rangeValue = document.getElementById(id).value; | |||||
| if (Array.isArray(map)) { | |||||
| document.getElementById(valueId).value = map[parseInt(rangeValue)]; | |||||
| this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]); | |||||
| } | |||||
| else { | |||||
| document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue); | |||||
| this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue)); | |||||
| } | |||||
| if (constantsVariableName == "hierarchicalLayout_direction" || | |||||
| constantsVariableName == "hierarchicalLayout_levelSeparation" || | |||||
| constantsVariableName == "hierarchicalLayout_nodeSpacing") { | |||||
| this._setupHierarchicalLayout(); | |||||
| } | |||||
| this.moving = true; | |||||
| this.start(); | |||||
| } | |||||
| @ -1,64 +0,0 @@ | |||||
| /** | |||||
| * Calculate the forces the nodes apply on each other based on a repulsion field. | |||||
| * This field is linearly approximated. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| exports._calculateNodeForces = function () { | |||||
| var dx, dy, angle, distance, fx, fy, combinedClusterSize, | |||||
| repulsingForce, node1, node2, i, j; | |||||
| var nodes = this.calculationNodes; | |||||
| var nodeIndices = this.calculationNodeIndices; | |||||
| // approximation constants | |||||
| var a_base = -2 / 3; | |||||
| var b = 4 / 3; | |||||
| // repulsing forces between nodes | |||||
| var nodeDistance = this.constants.physics.repulsion.nodeDistance; | |||||
| var minimumDistance = nodeDistance; | |||||
| // we loop from i over all but the last entree in the array | |||||
| // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j | |||||
| for (i = 0; i < nodeIndices.length - 1; i++) { | |||||
| node1 = nodes[nodeIndices[i]]; | |||||
| for (j = i + 1; j < nodeIndices.length; j++) { | |||||
| node2 = nodes[nodeIndices[j]]; | |||||
| combinedClusterSize = node1.clusterSize + node2.clusterSize - 2; | |||||
| dx = node2.x - node1.x; | |||||
| dy = node2.y - node1.y; | |||||
| distance = Math.sqrt(dx * dx + dy * dy); | |||||
| // same condition as BarnesHutSolver, making sure nodes are never 100% overlapping. | |||||
| if (distance == 0) { | |||||
| distance = 0.1*Math.random(); | |||||
| dx = distance; | |||||
| } | |||||
| minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification)); | |||||
| var a = a_base / minimumDistance; | |||||
| if (distance < 2 * minimumDistance) { | |||||
| if (distance < 0.5 * minimumDistance) { | |||||
| repulsingForce = 1.0; | |||||
| } | |||||
| else { | |||||
| repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)) | |||||
| } | |||||
| // amplify the repulsion for clusters. | |||||
| repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification; | |||||
| repulsingForce = repulsingForce / Math.max(distance,0.01*minimumDistance); | |||||
| fx = dx * repulsingForce; | |||||
| fy = dy * repulsingForce; | |||||
| node1.fx -= fx; | |||||
| node1.fy -= fy; | |||||
| node2.fx += fx; | |||||
| node2.fy += fy; | |||||
| } | |||||
| } | |||||
| } | |||||
| }; | |||||
| @ -0,0 +1,71 @@ | |||||
| /** | |||||
| * Created by Alex on 2/23/2015. | |||||
| */ | |||||
| class HierarchicalRepulsionSolver { | |||||
| constructor(body, physicsBody, options) { | |||||
| this.body = body; | |||||
| this.physicsBody = physicsBody; | |||||
| this.options = options; | |||||
| } | |||||
| /** | |||||
| * Calculate the forces the nodes apply on each other based on a repulsion field. | |||||
| * This field is linearly approximated. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| solve() { | |||||
| var dx, dy, distance, fx, fy, | |||||
| repulsingForce, node1, node2, i, j; | |||||
| var nodes = this.physicsBody.calculationNodes; | |||||
| var nodeIndices = this.physicsBody.calculationNodeIndices; | |||||
| // repulsing forces between nodes | |||||
| var nodeDistance = this.options.nodeDistance; | |||||
| // we loop from i over all but the last entree in the array | |||||
| // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j | |||||
| for (i = 0; i < nodeIndices.length - 1; i++) { | |||||
| node1 = nodes[nodeIndices[i]]; | |||||
| for (j = i + 1; j < nodeIndices.length; j++) { | |||||
| node2 = nodes[nodeIndices[j]]; | |||||
| // nodes only affect nodes on their level | |||||
| if (node1.level == node2.level) { | |||||
| dx = node2.x - node1.x; | |||||
| dy = node2.y - node1.y; | |||||
| distance = Math.sqrt(dx * dx + dy * dy); | |||||
| var steepness = 0.05; | |||||
| if (distance < nodeDistance) { | |||||
| repulsingForce = -Math.pow(steepness * distance, 2) + Math.pow(steepness * nodeDistance, 2); | |||||
| } | |||||
| else { | |||||
| repulsingForce = 0; | |||||
| } | |||||
| // normalize force with | |||||
| if (distance == 0) { | |||||
| distance = 0.01; | |||||
| } | |||||
| else { | |||||
| repulsingForce = repulsingForce / distance; | |||||
| } | |||||
| fx = dx * repulsingForce; | |||||
| fy = dy * repulsingForce; | |||||
| node1.fx -= fx; | |||||
| node1.fy -= fy; | |||||
| node2.fx += fx; | |||||
| node2.fy += fy; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| export {HierarchicalRepulsionSolver}; | |||||
| @ -0,0 +1,103 @@ | |||||
| /** | |||||
| * Created by Alex on 2/25/2015. | |||||
| */ | |||||
| class HierarchicalSpringSolver { | |||||
| constructor(body, physicsBody, options) { | |||||
| this.body = body; | |||||
| this.physicsBody = physicsBody; | |||||
| this.options = options; | |||||
| } | |||||
| /** | |||||
| * This function calculates the springforces on the nodes, accounting for the support nodes. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| solve() { | |||||
| var edgeLength, edge, edgeId; | |||||
| var dx, dy, fx, fy, springForce, distance; | |||||
| var edges = this.body.edges; | |||||
| var nodes = this.physicsBody.calculationNodes; | |||||
| var nodeIndices = this.physicsBody.calculationNodeIndices; | |||||
| // initialize the spring force counters | |||||
| for (let i = 0; i < nodeIndices.length; i++) { | |||||
| let node1 = nodes[nodeIndices[i]]; | |||||
| node1.springFx = 0; | |||||
| node1.springFy = 0; | |||||
| } | |||||
| // forces caused by the edges, modelled as springs | |||||
| for (edgeId in edges) { | |||||
| if (edges.hasOwnProperty(edgeId)) { | |||||
| edge = edges[edgeId]; | |||||
| if (edge.connected === true) { | |||||
| // only calculate forces if nodes are in the same sector | |||||
| if (this.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) { | |||||
| edgeLength = edge.properties.length === undefined ? this.options.springLength : edge.properties.length; | |||||
| dx = (edge.from.x - edge.to.x); | |||||
| dy = (edge.from.y - edge.to.y); | |||||
| distance = Math.sqrt(dx * dx + dy * dy); | |||||
| distance = distance == 0 ? 0.01 : distance; | |||||
| // the 1/distance is so the fx and fy can be calculated without sine or cosine. | |||||
| springForce = this.options.springConstant * (edgeLength - distance) / distance; | |||||
| fx = dx * springForce; | |||||
| fy = dy * springForce; | |||||
| if (edge.to.level != edge.from.level) { | |||||
| edge.to.springFx -= fx; | |||||
| edge.to.springFy -= fy; | |||||
| edge.from.springFx += fx; | |||||
| edge.from.springFy += fy; | |||||
| } | |||||
| else { | |||||
| let factor = 0.5; | |||||
| edge.to.fx -= factor*fx; | |||||
| edge.to.fy -= factor*fy; | |||||
| edge.from.fx += factor*fx; | |||||
| edge.from.fy += factor*fy; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| // normalize spring forces | |||||
| var springForce = 1; | |||||
| var springFx, springFy; | |||||
| for (let i = 0; i < nodeIndices.length; i++) { | |||||
| var node = nodes[nodeIndices[i]]; | |||||
| springFx = Math.min(springForce,Math.max(-springForce,node.springFx)); | |||||
| springFy = Math.min(springForce,Math.max(-springForce,node.springFy)); | |||||
| node.fx += springFx; | |||||
| node.fy += springFy; | |||||
| } | |||||
| // retain energy balance | |||||
| var totalFx = 0; | |||||
| var totalFy = 0; | |||||
| for (let i = 0; i < nodeIndices.length; i++) { | |||||
| var node = nodes[nodeIndices[i]]; | |||||
| totalFx += node.fx; | |||||
| totalFy += node.fy; | |||||
| } | |||||
| var correctionFx = totalFx / nodeIndices.length; | |||||
| var correctionFy = totalFy / nodeIndices.length; | |||||
| for (let i = 0; i < nodeIndices.length; i++) { | |||||
| var node = nodes[nodeIndices[i]]; | |||||
| node.fx -= correctionFx; | |||||
| node.fy -= correctionFy; | |||||
| } | |||||
| } | |||||
| } | |||||
| export {HierarchicalSpringSolver}; | |||||
| @ -0,0 +1,70 @@ | |||||
| /** | |||||
| * Created by Alex on 2/23/2015. | |||||
| */ | |||||
| class RepulsionSolver { | |||||
| constructor(body, physicsBody, options) { | |||||
| this.body = body; | |||||
| this.physicsBody = physicsBody; | |||||
| this.options = options; | |||||
| } | |||||
| /** | |||||
| * Calculate the forces the nodes apply on each other based on a repulsion field. | |||||
| * This field is linearly approximated. | |||||
| * | |||||
| * @private | |||||
| */ | |||||
| solve() { | |||||
| var dx, dy, distance, fx, fy, repulsingForce, node1, node2; | |||||
| var nodes = this.physicsBody.calculationNodes; | |||||
| var nodeIndices = this.physicsBody.calculationNodeIndices; | |||||
| // repulsing forces between nodes | |||||
| var nodeDistance = this.options.nodeDistance; | |||||
| // approximation constants | |||||
| var a = (-2 / 3) /nodeDistance; | |||||
| var b = 4 / 3; | |||||
| // we loop from i over all but the last entree in the array | |||||
| // j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j | |||||
| for (let i = 0; i < nodeIndices.length - 1; i++) { | |||||
| node1 = nodes[nodeIndices[i]]; | |||||
| for (let j = i + 1; j < nodeIndices.length; j++) { | |||||
| node2 = nodes[nodeIndices[j]]; | |||||
| dx = node2.x - node1.x; | |||||
| dy = node2.y - node1.y; | |||||
| distance = Math.sqrt(dx * dx + dy * dy); | |||||
| // same condition as BarnesHutSolver, making sure nodes are never 100% overlapping. | |||||
| if (distance == 0) { | |||||
| distance = 0.1*Math.random(); | |||||
| dx = distance; | |||||
| } | |||||
| if (distance < 2 * nodeDistance) { | |||||
| if (distance < 0.5 * nodeDistance) { | |||||
| repulsingForce = 1.0; | |||||
| } | |||||
| else { | |||||
| repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / nodeDistance - 1) * steepness)) | |||||
| } | |||||
| repulsingForce = repulsingForce / distance; | |||||
| fx = dx * repulsingForce; | |||||
| fy = dy * repulsingForce; | |||||
| node1.fx -= fx; | |||||
| node1.fy -= fy; | |||||
| node2.fx += fx; | |||||
| node2.fy += fy; | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| export {RepulsionSolver}; | |||||