var Emitter = require('emitter-component');
|
|
var Hammer = require('../module/hammer');
|
|
var keycharm = require('keycharm');
|
|
var util = require('../util');
|
|
var hammerUtil = require('../hammerUtil');
|
|
var DataSet = require('../DataSet');
|
|
var DataView = require('../DataView');
|
|
var dotparser = require('./dotparser');
|
|
var gephiParser = require('./gephiParser');
|
|
var Groups = require('./Groups');
|
|
var Images = require('./Images');
|
|
var Node = require('./Node');
|
|
var Edge = require('./Edge');
|
|
var Popup = require('./Popup');
|
|
var MixinLoader = require('./mixins/MixinLoader');
|
|
var Activator = require('../shared/Activator');
|
|
var locales = require('./locales');
|
|
|
|
// Load custom shapes into CanvasRenderingContext2D
|
|
require('./shapes');
|
|
|
|
import { PhysicsEngine } from './modules/PhysicsEngine'
|
|
import { ClusterEngine } from './modules/Clustering'
|
|
import { CanvasRenderer } from './modules/CanvasRenderer'
|
|
import { Canvas } from './modules/Canvas'
|
|
import { View } from './modules/View'
|
|
import { InteractionHandler } from './modules/InteractionHandler'
|
|
import { SelectionHandler } from "./modules/SelectionHandler"
|
|
|
|
/**
|
|
* @constructor Network
|
|
* Create a network visualization, displaying nodes and edges.
|
|
*
|
|
* @param {Element} container The DOM element in which the Network will
|
|
* be created. Normally a div element.
|
|
* @param {Object} data An object containing parameters
|
|
* {Array} nodes
|
|
* {Array} edges
|
|
* @param {Object} options Options
|
|
*/
|
|
function Network (container, data, options) {
|
|
if (!(this instanceof Network)) {
|
|
throw new SyntaxError('Constructor must be called with the new operator');
|
|
}
|
|
|
|
this._initializeMixinLoaders();
|
|
|
|
|
|
// render and calculation settings
|
|
this.initializing = true;
|
|
|
|
this.triggerFunctions = {add:null,edit:null,editEdge:null,connect:null,del:null};
|
|
|
|
var customScalingFunction = function (min,max,total,value) {
|
|
if (max == min) {
|
|
return 0.5;
|
|
}
|
|
else {
|
|
var scale = 1 / (max - min);
|
|
return Math.max(0,(value - min)*scale);
|
|
}
|
|
};
|
|
|
|
// set constant values
|
|
this.defaultOptions = {
|
|
nodes: {
|
|
customScalingFunction: customScalingFunction,
|
|
mass: 1,
|
|
radiusMin: 10,
|
|
radiusMax: 30,
|
|
radius: 10,
|
|
shape: 'ellipse',
|
|
image: undefined,
|
|
widthMin: 16, // px
|
|
widthMax: 64, // px
|
|
fontColor: 'black',
|
|
fontSize: 14, // px
|
|
fontFace: 'verdana',
|
|
fontFill: undefined,
|
|
fontStrokeWidth: 0, // px
|
|
fontStrokeColor: '#ffffff',
|
|
fontDrawThreshold: 3,
|
|
scaleFontWithValue: false,
|
|
fontSizeMin: 14,
|
|
fontSizeMax: 30,
|
|
fontSizeMaxVisible: 30,
|
|
value: 1,
|
|
level: -1,
|
|
color: {
|
|
border: '#2B7CE9',
|
|
background: '#97C2FC',
|
|
highlight: {
|
|
border: '#2B7CE9',
|
|
background: '#D2E5FF'
|
|
},
|
|
hover: {
|
|
border: '#2B7CE9',
|
|
background: '#D2E5FF'
|
|
}
|
|
},
|
|
group: undefined,
|
|
borderWidth: 1,
|
|
borderWidthSelected: undefined
|
|
},
|
|
edges: {
|
|
customScalingFunction: customScalingFunction,
|
|
widthMin: 1, //
|
|
widthMax: 15,//
|
|
width: 1,
|
|
widthSelectionMultiplier: 2,
|
|
hoverWidth: 1.5,
|
|
value:1,
|
|
style: 'line',
|
|
color: {
|
|
color:'#848484',
|
|
highlight:'#848484',
|
|
hover: '#848484'
|
|
},
|
|
opacity:1.0,
|
|
fontColor: '#343434',
|
|
fontSize: 14, // px
|
|
fontFace: 'arial',
|
|
fontFill: 'white',
|
|
fontStrokeWidth: 0, // px
|
|
fontStrokeColor: 'white',
|
|
labelAlignment:'horizontal',
|
|
arrowScaleFactor: 1,
|
|
dash: {
|
|
length: 10,
|
|
gap: 5,
|
|
altLength: undefined
|
|
},
|
|
inheritColor: "from", // to, from, false, true (== from)
|
|
useGradients: false // release in 4.0
|
|
},
|
|
navigation: {
|
|
enabled: false
|
|
},
|
|
dataManipulation: {
|
|
enabled: false,
|
|
initiallyVisible: false
|
|
},
|
|
hierarchicalLayout: {
|
|
enabled:false,
|
|
levelSeparation: 150,
|
|
nodeSpacing: 100,
|
|
direction: "UD", // UD, DU, LR, RL
|
|
layout: "hubsize" // hubsize, directed
|
|
},
|
|
interaction: {
|
|
dragNodes:true,
|
|
dragView: true,
|
|
zoomView: true,
|
|
hoverEnabled: false,
|
|
tooltip: {
|
|
delay: 300,
|
|
fontColor: 'black',
|
|
fontSize: 14, // px
|
|
fontFace: 'verdana',
|
|
color: {
|
|
border: '#666',
|
|
background: '#FFFFC6'
|
|
}
|
|
},
|
|
keyboard: {
|
|
enabled: false,
|
|
speed: {x: 10, y: 10, zoom: 0.02},
|
|
bindToWindow: true
|
|
}
|
|
},
|
|
selection: {
|
|
enabled: true,
|
|
selectConnectedEdges: true
|
|
},
|
|
smoothCurves: {
|
|
enabled: true,
|
|
dynamic: true,
|
|
type: "continuous",
|
|
roundness: 0.5
|
|
},
|
|
locale: 'en',
|
|
locales: locales,
|
|
useDefaultGroups: true
|
|
};
|
|
this.constants = util.extend({}, this.defaultOptions);
|
|
|
|
// containers for nodes and edges
|
|
this.body = {
|
|
nodes: {},
|
|
nodeIndices: [],
|
|
supportNodes: {},
|
|
supportNodeIndices: [],
|
|
edges: {},
|
|
data: {
|
|
nodes: null, // A DataSet or DataView
|
|
edges: null // A DataSet or DataView
|
|
},
|
|
functions:{
|
|
createNode: this._createNode.bind(this),
|
|
createEdge: this._createEdge.bind(this)
|
|
},
|
|
emitter: {
|
|
on: this.on.bind(this),
|
|
off: this.off.bind(this),
|
|
emit: this.emit.bind(this),
|
|
once: this.once.bind(this)
|
|
},
|
|
eventListeners: {
|
|
onTap: function() {},
|
|
onTouch: function() {},
|
|
onDoubleTap: function() {},
|
|
onHold: function() {},
|
|
onDragStart: function() {},
|
|
onDrag: function() {},
|
|
onDragEnd: function() {},
|
|
onMouseWheel: function() {},
|
|
onPinch: function() {},
|
|
onMouseMove: function() {},
|
|
onRelease: function() {}
|
|
},
|
|
container: container,
|
|
view: {
|
|
scale:1,
|
|
translation:{x:0,y:0}
|
|
}
|
|
};
|
|
|
|
// modules
|
|
this.canvas = new Canvas(this.body);
|
|
this.selectionHandler = new SelectionHandler(this.body, this.canvas);
|
|
this.interactionHandler = new InteractionHandler(this.body, this.canvas, this.selectionHandler);
|
|
this.view = new View(this.body, this.canvas);
|
|
this.renderer = new CanvasRenderer(this.body, this.canvas);
|
|
this.clustering = new ClusterEngine(this.body);
|
|
this.physics = new PhysicsEngine(this.body);
|
|
|
|
// create the DOM elements
|
|
this.canvas.create();
|
|
|
|
this.hoverObj = {nodes:{},edges:{}};
|
|
this.controlNodesActive = false;
|
|
this.navigationHammers = [];
|
|
this.manipulationHammers = [];
|
|
|
|
// Node variables
|
|
var me = this;
|
|
this.groups = new Groups(); // object with groups
|
|
this.images = new Images(); // object with images
|
|
this.images.setOnloadCallback(function (status) {
|
|
me._requestRedraw();
|
|
});
|
|
|
|
// keyboard navigation variables
|
|
this.xIncrement = 0;
|
|
this.yIncrement = 0;
|
|
this.zoomIncrement = 0;
|
|
|
|
// loading all the mixins:
|
|
// load the force calculation functions, grouped under the physics system.
|
|
//this._loadPhysicsSystem();
|
|
// create a frame and canvas
|
|
// load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
|
|
// load the selection system. (mandatory, required by Network)
|
|
this._loadSelectionSystem();
|
|
// load the selection system. (mandatory, required by Network)
|
|
//this._loadHierarchySystem();
|
|
|
|
// apply options
|
|
this.setOptions(options);
|
|
|
|
// position and scale variables and objects
|
|
|
|
this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
|
|
this.scale = 1; // defining the global scale variable in the constructor
|
|
|
|
// create event listeners used to subscribe on the DataSets of the nodes and edges
|
|
this.nodesListeners = {
|
|
'add': function (event, params) {
|
|
me._addNodes(params.items);
|
|
me.start();
|
|
},
|
|
'update': function (event, params) {
|
|
me._updateNodes(params.items, params.data);
|
|
me.start();
|
|
},
|
|
'remove': function (event, params) {
|
|
me._removeNodes(params.items);
|
|
me.start();
|
|
}
|
|
};
|
|
this.edgesListeners = {
|
|
'add': function (event, params) {
|
|
me._addEdges(params.items);
|
|
me.start();
|
|
},
|
|
'update': function (event, params) {
|
|
me._updateEdges(params.items);
|
|
me.start();
|
|
},
|
|
'remove': function (event, params) {
|
|
me._removeEdges(params.items);
|
|
me.start();
|
|
}
|
|
};
|
|
|
|
|
|
// properties for the animation
|
|
this.moving = true;
|
|
this.renderTimer = undefined; // Scheduling function. Is definded in this.start();
|
|
|
|
// load data (the disable start variable will be the same as the enabled clustering)
|
|
this.setData(data, this.constants.hierarchicalLayout.enabled);
|
|
|
|
// hierarchical layout
|
|
if (this.constants.hierarchicalLayout.enabled == true) {
|
|
this._setupHierarchicalLayout();
|
|
}
|
|
else {
|
|
// zoom so all data will fit on the screen, if clustering is enabled, we do not want start to be called here.
|
|
if (this.constants.stabilize == false) {
|
|
this.zoomExtent({duration:0}, true, this.constants.clustering.enabled);
|
|
}
|
|
}
|
|
|
|
if (this.constants.stabilize == false) {
|
|
this.initializing = false;
|
|
}
|
|
|
|
var me = this;
|
|
// this event will trigger a rebuilding of the cache of colors, nodes etc.
|
|
this.on("_dataChanged", function () {
|
|
me._updateNodeIndexList();
|
|
me.physics._updateCalculationNodes();
|
|
me._markAllEdgesAsDirty();
|
|
if (me.initializing !== true) {
|
|
me.moving = true;
|
|
me.start();
|
|
}
|
|
})
|
|
|
|
this.on("_newEdgesCreated", this._createBezierNodes.bind(this));
|
|
//this.on("stabilizationIterationsDone", function () {me.initializing = false; me.start();}.bind(this));
|
|
}
|
|
|
|
// Extend Network with an Emitter mixin
|
|
Emitter(Network.prototype);
|
|
|
|
|
|
Network.prototype._createNode = function(properties) {
|
|
return new Node(properties, this.images, this.groups, this.constants)
|
|
}
|
|
|
|
Network.prototype._createEdge = function(properties) {
|
|
return new Edge(properties, this.body, this.constants)
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
* Update the this.body.nodeIndices with the most recent node index list
|
|
* @private
|
|
*/
|
|
Network.prototype._updateNodeIndexList = function() {
|
|
this.body.supportNodeIndices = Object.keys(this.body.supportNodes)
|
|
this.body.nodeIndices = Object.keys(this.body.nodes);
|
|
};
|
|
|
|
|
|
/**
|
|
* Set nodes and edges, and optionally options as well.
|
|
*
|
|
* @param {Object} data Object containing parameters:
|
|
* {Array | DataSet | DataView} [nodes] Array with nodes
|
|
* {Array | DataSet | DataView} [edges] Array with edges
|
|
* {String} [dot] String containing data in DOT format
|
|
* {String} [gephi] String containing data in gephi JSON format
|
|
* {Options} [options] Object with options
|
|
* @param {Boolean} [disableStart] | optional: disable the calling of the start function.
|
|
*/
|
|
Network.prototype.setData = function(data, disableStart) {
|
|
if (disableStart === undefined) {
|
|
disableStart = false;
|
|
}
|
|
|
|
// unselect all to ensure no selections from old data are carried over.
|
|
this.selectionHandler.unselectAll();
|
|
|
|
// we set initializing to true to ensure that the hierarchical layout is not performed until both nodes and edges are added.
|
|
this.initializing = true;
|
|
|
|
if (data && data.dot && (data.nodes || data.edges)) {
|
|
throw new SyntaxError('Data must contain either parameter "dot" or ' +
|
|
' parameter pair "nodes" and "edges", but not both.');
|
|
}
|
|
|
|
// clean up in case there is anyone in an active mode of the manipulation. This is the same option as bound to the escape button.
|
|
if (this.constants.dataManipulation.enabled == true) {
|
|
this._createManipulatorBar();
|
|
}
|
|
|
|
// set options
|
|
this.setOptions(data && data.options);
|
|
// set all data
|
|
if (data && data.dot) {
|
|
// parse DOT file
|
|
if(data && data.dot) {
|
|
var dotData = dotparser.DOTToGraph(data.dot);
|
|
this.setData(dotData);
|
|
return;
|
|
}
|
|
}
|
|
else if (data && data.gephi) {
|
|
// parse DOT file
|
|
if(data && data.gephi) {
|
|
var gephiData = gephiParser.parseGephi(data.gephi);
|
|
this.setData(gephiData);
|
|
return;
|
|
}
|
|
}
|
|
else {
|
|
this._setNodes(data && data.nodes);
|
|
this._setEdges(data && data.edges);
|
|
}
|
|
|
|
if (disableStart == false) {
|
|
if (this.constants.hierarchicalLayout.enabled == true) {
|
|
this._resetLevels();
|
|
this._setupHierarchicalLayout();
|
|
}
|
|
else {
|
|
// find a stable position or start animating to a stable position
|
|
this.body.emitter.emit("stabilize");
|
|
}
|
|
}
|
|
else {
|
|
this.initializing = false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Set options
|
|
* @param {Object} options
|
|
*/
|
|
Network.prototype.setOptions = function (options) {
|
|
if (options) {
|
|
var prop;
|
|
var fields = ['nodes','edges','smoothCurves','hierarchicalLayout','navigation',
|
|
'keyboard','dataManipulation','onAdd','onEdit','onEditEdge','onConnect','onDelete','clickToUse'
|
|
];
|
|
// extend all but the values in fields
|
|
util.selectiveNotDeepExtend(fields,this.constants, options);
|
|
util.selectiveNotDeepExtend(['color'],this.constants.nodes, options.nodes);
|
|
util.selectiveNotDeepExtend(['color','length'],this.constants.edges, options.edges);
|
|
|
|
this.groups.useDefaultGroups = this.constants.useDefaultGroups;
|
|
|
|
this.physics.setOptions(options.physics);
|
|
this.canvas.setOptions(options.canvas);
|
|
this.renderer.setOptions(options.rendering);
|
|
this.interactionHandler.setOptions(options.interaction);
|
|
this.selectionHandler.setOptions(options.selection);
|
|
|
|
|
|
if (options.onAdd) {this.triggerFunctions.add = options.onAdd;}
|
|
if (options.onEdit) {this.triggerFunctions.edit = options.onEdit;}
|
|
if (options.onEditEdge) {this.triggerFunctions.editEdge = options.onEditEdge;}
|
|
if (options.onConnect) {this.triggerFunctions.connect = options.onConnect;}
|
|
if (options.onDelete) {this.triggerFunctions.del = options.onDelete;}
|
|
|
|
util.mergeOptions(this.constants, options,'smoothCurves');
|
|
util.mergeOptions(this.constants, options,'hierarchicalLayout');
|
|
util.mergeOptions(this.constants, options,'clustering');
|
|
util.mergeOptions(this.constants, options,'navigation');
|
|
util.mergeOptions(this.constants, options,'keyboard');
|
|
util.mergeOptions(this.constants, options,'dataManipulation');
|
|
|
|
|
|
if (options.dataManipulation) {
|
|
this.editMode = this.constants.dataManipulation.initiallyVisible;
|
|
}
|
|
|
|
|
|
// TODO: work out these options and document them
|
|
if (options.edges) {
|
|
if (options.edges.color !== undefined) {
|
|
if (util.isString(options.edges.color)) {
|
|
this.constants.edges.color = {};
|
|
this.constants.edges.color.color = options.edges.color;
|
|
this.constants.edges.color.highlight = options.edges.color;
|
|
this.constants.edges.color.hover = options.edges.color;
|
|
}
|
|
else {
|
|
if (options.edges.color.color !== undefined) {this.constants.edges.color.color = options.edges.color.color;}
|
|
if (options.edges.color.highlight !== undefined) {this.constants.edges.color.highlight = options.edges.color.highlight;}
|
|
if (options.edges.color.hover !== undefined) {this.constants.edges.color.hover = options.edges.color.hover;}
|
|
}
|
|
this.constants.edges.inheritColor = false;
|
|
}
|
|
|
|
if (!options.edges.fontColor) {
|
|
if (options.edges.color !== undefined) {
|
|
if (util.isString(options.edges.color)) {this.constants.edges.fontColor = options.edges.color;}
|
|
else if (options.edges.color.color !== undefined) {this.constants.edges.fontColor = options.edges.color.color;}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (options.nodes) {
|
|
if (options.nodes.color) {
|
|
var newColorObj = util.parseColor(options.nodes.color);
|
|
this.constants.nodes.color.background = newColorObj.background;
|
|
this.constants.nodes.color.border = newColorObj.border;
|
|
this.constants.nodes.color.highlight.background = newColorObj.highlight.background;
|
|
this.constants.nodes.color.highlight.border = newColorObj.highlight.border;
|
|
this.constants.nodes.color.hover.background = newColorObj.hover.background;
|
|
this.constants.nodes.color.hover.border = newColorObj.hover.border;
|
|
}
|
|
}
|
|
if (options.groups) {
|
|
for (var groupname in options.groups) {
|
|
if (options.groups.hasOwnProperty(groupname)) {
|
|
var group = options.groups[groupname];
|
|
this.groups.add(groupname, group);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (options.tooltip) {
|
|
for (prop in options.tooltip) {
|
|
if (options.tooltip.hasOwnProperty(prop)) {
|
|
this.constants.tooltip[prop] = options.tooltip[prop];
|
|
}
|
|
}
|
|
if (options.tooltip.color) {
|
|
this.constants.tooltip.color = util.parseColor(options.tooltip.color);
|
|
}
|
|
}
|
|
|
|
if ('clickToUse' in options) {
|
|
if (options.clickToUse) {
|
|
if (!this.activator) {
|
|
this.activator = new Activator(this.frame);
|
|
this.activator.on('change', this._createKeyBinds.bind(this));
|
|
}
|
|
}
|
|
else {
|
|
if (this.activator) {
|
|
this.activator.destroy();
|
|
delete this.activator;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (options.labels) {
|
|
throw new Error('Option "labels" is deprecated. Use options "locale" and "locales" instead.');
|
|
}
|
|
|
|
// (Re)loading the mixins that can be enabled or disabled in the options.
|
|
// load the force calculation functions, grouped under the physics system.
|
|
// load the navigation system.
|
|
//this._loadNavigationControls();
|
|
//// load the data manipulation system
|
|
//this._loadManipulationSystem();
|
|
//// configure the smooth curves
|
|
//this._configureSmoothCurves();
|
|
|
|
// bind hammer
|
|
//this.canvas._bindHammer();
|
|
|
|
// bind keys. If disabled, this will not do anything;
|
|
//this._createKeyBinds();
|
|
|
|
this._markAllEdgesAsDirty();
|
|
this.canvas.setSize();
|
|
if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
|
|
this._resetLevels();
|
|
this._setupHierarchicalLayout();
|
|
}
|
|
|
|
if (this.initializing !== true) {
|
|
this.moving = true;
|
|
this.start();
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
* Binding the keys for keyboard navigation. These functions are defined in the NavigationMixin
|
|
* @private
|
|
*/
|
|
Network.prototype._createKeyBinds = function() {
|
|
return;
|
|
|
|
//var me = this;
|
|
//if (this.keycharm !== undefined) {
|
|
// this.keycharm.destroy();
|
|
//}
|
|
//
|
|
//if (this.constants.keyboard.bindToWindow == true) {
|
|
// this.keycharm = keycharm({container: window, preventDefault: false});
|
|
//}
|
|
//else {
|
|
// this.keycharm = keycharm({container: this.frame, preventDefault: false});
|
|
//}
|
|
//
|
|
//this.keycharm.reset();
|
|
//
|
|
//if (this.constants.keyboard.enabled && this.isActive()) {
|
|
// this.keycharm.bind("up", this._moveUp.bind(me) , "keydown");
|
|
// this.keycharm.bind("up", this._yStopMoving.bind(me), "keyup");
|
|
// this.keycharm.bind("down", this._moveDown.bind(me) , "keydown");
|
|
// this.keycharm.bind("down", this._yStopMoving.bind(me), "keyup");
|
|
// this.keycharm.bind("left", this._moveLeft.bind(me) , "keydown");
|
|
// this.keycharm.bind("left", this._xStopMoving.bind(me), "keyup");
|
|
// this.keycharm.bind("right",this._moveRight.bind(me), "keydown");
|
|
// this.keycharm.bind("right",this._xStopMoving.bind(me), "keyup");
|
|
// this.keycharm.bind("=", this._zoomIn.bind(me), "keydown");
|
|
// this.keycharm.bind("=", this._stopZoom.bind(me), "keyup");
|
|
// this.keycharm.bind("num+", this._zoomIn.bind(me), "keydown");
|
|
// this.keycharm.bind("num+", this._stopZoom.bind(me), "keyup");
|
|
// this.keycharm.bind("num-", this._zoomOut.bind(me), "keydown");
|
|
// this.keycharm.bind("num-", this._stopZoom.bind(me), "keyup");
|
|
// this.keycharm.bind("-", this._zoomOut.bind(me), "keydown");
|
|
// this.keycharm.bind("-", this._stopZoom.bind(me), "keyup");
|
|
// this.keycharm.bind("[", this._zoomIn.bind(me), "keydown");
|
|
// this.keycharm.bind("[", this._stopZoom.bind(me), "keyup");
|
|
// this.keycharm.bind("]", this._zoomOut.bind(me), "keydown");
|
|
// this.keycharm.bind("]", this._stopZoom.bind(me), "keyup");
|
|
// this.keycharm.bind("pageup",this._zoomIn.bind(me), "keydown");
|
|
// this.keycharm.bind("pageup",this._stopZoom.bind(me), "keyup");
|
|
// this.keycharm.bind("pagedown",this._zoomOut.bind(me),"keydown");
|
|
// this.keycharm.bind("pagedown",this._stopZoom.bind(me), "keyup");
|
|
//}
|
|
//
|
|
//if (this.constants.dataManipulation.enabled == true) {
|
|
// this.keycharm.bind("esc",this._createManipulatorBar.bind(me));
|
|
// this.keycharm.bind("delete",this._deleteSelected.bind(me));
|
|
//}
|
|
};
|
|
|
|
/**
|
|
* Cleans up all bindings of the network, removing it fully from the memory IF the variable is set to null after calling this function.
|
|
* var network = new vis.Network(..);
|
|
* network.destroy();
|
|
* network = null;
|
|
*/
|
|
Network.prototype.destroy = function() {
|
|
this.start = function () {};
|
|
this.redraw = function () {};
|
|
this.renderTimer = false;
|
|
|
|
// cleanup physicsConfiguration if it exists
|
|
this._cleanupPhysicsConfiguration();
|
|
|
|
// remove keybindings
|
|
this.keycharm.reset();
|
|
|
|
// clear hammer bindings
|
|
this.hammer.destroy();
|
|
|
|
// clear events
|
|
this.off();
|
|
|
|
this._recursiveDOMDelete(this.containerElement);
|
|
};
|
|
|
|
Network.prototype._recursiveDOMDelete = function(DOMobject) {
|
|
while (DOMobject.hasChildNodes() == true) {
|
|
this._recursiveDOMDelete(DOMobject.firstChild);
|
|
DOMobject.removeChild(DOMobject.firstChild);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Check if there is an element on the given position in the network
|
|
* (a node or edge). If so, and if this element has a title,
|
|
* show a popup window with its title.
|
|
*
|
|
* @param {{x:Number, y:Number}} pointer
|
|
* @private
|
|
*/
|
|
Network.prototype._checkShowPopup = function (pointer) {
|
|
var obj = {
|
|
left: this._XconvertDOMtoCanvas(pointer.x),
|
|
top: this._YconvertDOMtoCanvas(pointer.y),
|
|
right: this._XconvertDOMtoCanvas(pointer.x),
|
|
bottom: this._YconvertDOMtoCanvas(pointer.y)
|
|
};
|
|
|
|
var id;
|
|
var previousPopupObjId = this.popupObj === undefined ? "" : this.popupObj.id;
|
|
var nodeUnderCursor = false;
|
|
var popupType = "node";
|
|
|
|
if (this.popupObj == undefined) {
|
|
// search the nodes for overlap, select the top one in case of multiple nodes
|
|
var nodes = this.body.nodes;
|
|
var overlappingNodes = [];
|
|
for (id in nodes) {
|
|
if (nodes.hasOwnProperty(id)) {
|
|
var node = nodes[id];
|
|
if (node.isOverlappingWith(obj)) {
|
|
if (node.getTitle() !== undefined) {
|
|
overlappingNodes.push(id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (overlappingNodes.length > 0) {
|
|
// if there are overlapping nodes, select the last one, this is the
|
|
// one which is drawn on top of the others
|
|
this.popupObj = this.body.nodes[overlappingNodes[overlappingNodes.length - 1]];
|
|
// if you hover over a node, the title of the edge is not supposed to be shown.
|
|
nodeUnderCursor = true;
|
|
}
|
|
}
|
|
|
|
if (this.popupObj === undefined && nodeUnderCursor == false) {
|
|
// search the edges for overlap
|
|
var edges = this.body.edges;
|
|
var overlappingEdges = [];
|
|
for (id in edges) {
|
|
if (edges.hasOwnProperty(id)) {
|
|
var edge = edges[id];
|
|
if (edge.connected === true && (edge.getTitle() !== undefined) &&
|
|
edge.isOverlappingWith(obj)) {
|
|
overlappingEdges.push(id);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (overlappingEdges.length > 0) {
|
|
this.popupObj = this.body.edges[overlappingEdges[overlappingEdges.length - 1]];
|
|
popupType = "edge";
|
|
}
|
|
}
|
|
|
|
if (this.popupObj) {
|
|
// show popup message window
|
|
if (this.popupObj.id != previousPopupObjId) {
|
|
if (this.popup === undefined) {
|
|
this.popup = new Popup(this.frame, this.constants.tooltip);
|
|
}
|
|
|
|
this.popup.popupTargetType = popupType;
|
|
this.popup.popupTargetId = this.popupObj.id;
|
|
|
|
// adjust a small offset such that the mouse cursor is located in the
|
|
// bottom left location of the popup, and you can easily move over the
|
|
// popup area
|
|
this.popup.setPosition(pointer.x + 3, pointer.y - 5);
|
|
this.popup.setText(this.popupObj.getTitle());
|
|
this.popup.show();
|
|
}
|
|
}
|
|
else {
|
|
if (this.popup) {
|
|
this.popup.hide();
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Check if the popup must be hidden, which is the case when the mouse is no
|
|
* longer hovering on the object
|
|
* @param {{x:Number, y:Number}} pointer
|
|
* @private
|
|
*/
|
|
Network.prototype._checkHidePopup = function (pointer) {
|
|
var pointerObj = {
|
|
left: this._XconvertDOMtoCanvas(pointer.x),
|
|
top: this._YconvertDOMtoCanvas(pointer.y),
|
|
right: this._XconvertDOMtoCanvas(pointer.x),
|
|
bottom: this._YconvertDOMtoCanvas(pointer.y)
|
|
};
|
|
|
|
var stillOnObj = false;
|
|
if (this.popup.popupTargetType == 'node') {
|
|
stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj);
|
|
if (stillOnObj === true) {
|
|
var overNode = this.getNodeAt(pointer);
|
|
stillOnObj = overNode.id == this.popup.popupTargetId;
|
|
}
|
|
}
|
|
else {
|
|
if (this.getNodeAt(pointer) === null) {
|
|
stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj);
|
|
}
|
|
}
|
|
|
|
|
|
if (stillOnObj === false) {
|
|
this.popupObj = undefined;
|
|
this.popup.hide();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Set a data set with nodes for the network
|
|
* @param {Array | DataSet | DataView} nodes The data containing the nodes.
|
|
* @private
|
|
*/
|
|
Network.prototype._setNodes = function(nodes) {
|
|
var oldNodesData = this.body.data.nodes;
|
|
|
|
if (nodes instanceof DataSet || nodes instanceof DataView) {
|
|
this.body.data.nodes = nodes;
|
|
}
|
|
else if (Array.isArray(nodes)) {
|
|
this.body.data.nodes = new DataSet();
|
|
this.body.data.nodes.add(nodes);
|
|
}
|
|
else if (!nodes) {
|
|
this.body.data.nodes = new DataSet();
|
|
}
|
|
else {
|
|
throw new TypeError('Array or DataSet expected');
|
|
}
|
|
|
|
if (oldNodesData) {
|
|
// unsubscribe from old dataset
|
|
util.forEach(this.nodesListeners, function (callback, event) {
|
|
oldNodesData.off(event, callback);
|
|
});
|
|
}
|
|
|
|
// remove drawn nodes
|
|
this.body.nodes = {};
|
|
|
|
if (this.body.data.nodes) {
|
|
// subscribe to new dataset
|
|
var me = this;
|
|
util.forEach(this.nodesListeners, function (callback, event) {
|
|
me.body.data.nodes.on(event, callback);
|
|
});
|
|
|
|
// draw all new nodes
|
|
var ids = this.body.data.nodes.getIds();
|
|
this._addNodes(ids);
|
|
}
|
|
this._updateSelection();
|
|
};
|
|
|
|
/**
|
|
* Add nodes
|
|
* @param {Number[] | String[]} ids
|
|
* @private
|
|
*/
|
|
Network.prototype._addNodes = function(ids) {
|
|
var id;
|
|
for (var i = 0, len = ids.length; i < len; i++) {
|
|
id = ids[i];
|
|
var data = this.body.data.nodes.get(id);
|
|
var node = new Node(data, this.images, this.groups, this.constants);
|
|
this.body.nodes[id] = node; // note: this may replace an existing node
|
|
if ((node.xFixed == false || node.yFixed == false) && (node.x === null || node.y === null)) {
|
|
var radius = 10 * 0.1*ids.length + 10;
|
|
var angle = 2 * Math.PI * Math.random();
|
|
if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
|
|
if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
|
|
}
|
|
this.moving = true;
|
|
}
|
|
|
|
this._updateNodeIndexList();
|
|
if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
|
|
this._resetLevels();
|
|
this._setupHierarchicalLayout();
|
|
}
|
|
this.physics._updateCalculationNodes();
|
|
this._reconnectEdges();
|
|
this._updateValueRange(this.body.nodes);
|
|
};
|
|
|
|
/**
|
|
* Update existing nodes, or create them when not yet existing
|
|
* @param {Number[] | String[]} ids
|
|
* @private
|
|
*/
|
|
Network.prototype._updateNodes = function(ids,changedData) {
|
|
var nodes = this.body.nodes;
|
|
for (var i = 0, len = ids.length; i < len; i++) {
|
|
var id = ids[i];
|
|
var node = nodes[id];
|
|
var data = changedData[i];
|
|
if (node) {
|
|
// update node
|
|
node.setProperties(data, this.constants);
|
|
}
|
|
else {
|
|
// create node
|
|
node = new Node(properties, this.images, this.groups, this.constants);
|
|
nodes[id] = node;
|
|
}
|
|
}
|
|
this.moving = true;
|
|
if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
|
|
this._resetLevels();
|
|
this._setupHierarchicalLayout();
|
|
}
|
|
this._updateNodeIndexList();
|
|
this._updateValueRange(nodes);
|
|
this._markAllEdgesAsDirty();
|
|
};
|
|
|
|
|
|
Network.prototype._markAllEdgesAsDirty = function() {
|
|
for (var edgeId in this.body.edges) {
|
|
this.body.edges[edgeId].colorDirty = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove existing nodes. If nodes do not exist, the method will just ignore it.
|
|
* @param {Number[] | String[]} ids
|
|
* @private
|
|
*/
|
|
Network.prototype._removeNodes = function(ids) {
|
|
var nodes = this.body.nodes;
|
|
|
|
// remove from selection
|
|
for (var i = 0, len = ids.length; i < len; i++) {
|
|
if (this.selectionObj.nodes[ids[i]] !== undefined) {
|
|
this.body.nodes[ids[i]].unselect();
|
|
this._removeFromSelection(this.body.nodes[ids[i]]);
|
|
}
|
|
}
|
|
|
|
for (var i = 0, len = ids.length; i < len; i++) {
|
|
var id = ids[i];
|
|
delete nodes[id];
|
|
}
|
|
|
|
|
|
|
|
this._updateNodeIndexList();
|
|
if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
|
|
this._resetLevels();
|
|
this._setupHierarchicalLayout();
|
|
}
|
|
this.physics._updateCalculationNodes();
|
|
this._reconnectEdges();
|
|
this._updateSelection();
|
|
this._updateValueRange(nodes);
|
|
};
|
|
|
|
/**
|
|
* Load edges by reading the data table
|
|
* @param {Array | DataSet | DataView} edges The data containing the edges.
|
|
* @private
|
|
* @private
|
|
*/
|
|
Network.prototype._setEdges = function(edges) {
|
|
var oldEdgesData = this.body.data.edges;
|
|
|
|
if (edges instanceof DataSet || edges instanceof DataView) {
|
|
this.body.data.edges = edges;
|
|
}
|
|
else if (Array.isArray(edges)) {
|
|
this.body.data.edges = new DataSet();
|
|
this.body.data.edges.add(edges);
|
|
}
|
|
else if (!edges) {
|
|
this.body.data.edges = new DataSet();
|
|
}
|
|
else {
|
|
throw new TypeError('Array or DataSet expected');
|
|
}
|
|
|
|
if (oldEdgesData) {
|
|
// unsubscribe from old dataset
|
|
util.forEach(this.edgesListeners, function (callback, event) {
|
|
oldEdgesData.off(event, callback);
|
|
});
|
|
}
|
|
|
|
// remove drawn edges
|
|
this.body.edges = {};
|
|
|
|
if (this.body.data.edges) {
|
|
// subscribe to new dataset
|
|
var me = this;
|
|
util.forEach(this.edgesListeners, function (callback, event) {
|
|
me.body.data.edges.on(event, callback);
|
|
});
|
|
|
|
// draw all new nodes
|
|
var ids = this.body.data.edges.getIds();
|
|
this._addEdges(ids);
|
|
}
|
|
|
|
this._reconnectEdges();
|
|
};
|
|
|
|
/**
|
|
* Add edges
|
|
* @param {Number[] | String[]} ids
|
|
* @private
|
|
*/
|
|
Network.prototype._addEdges = function (ids) {
|
|
var edges = this.body.edges,
|
|
edgesData = this.body.data.edges;
|
|
|
|
for (var i = 0, len = ids.length; i < len; i++) {
|
|
var id = ids[i];
|
|
|
|
var oldEdge = edges[id];
|
|
if (oldEdge) {
|
|
oldEdge.disconnect();
|
|
}
|
|
|
|
var data = edgesData.get(id, {"showInternalIds" : true});
|
|
edges[id] = new Edge(data, this.body, this.constants);
|
|
}
|
|
this.moving = true;
|
|
this._updateValueRange(edges);
|
|
this._createBezierNodes();
|
|
this.physics._updateCalculationNodes();
|
|
if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
|
|
this._resetLevels();
|
|
this._setupHierarchicalLayout();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update existing edges, or create them when not yet existing
|
|
* @param {Number[] | String[]} ids
|
|
* @private
|
|
*/
|
|
Network.prototype._updateEdges = function (ids) {
|
|
var edges = this.body.edges;
|
|
var edgesData = this.body.data.edges;
|
|
for (var i = 0, len = ids.length; i < len; i++) {
|
|
var id = ids[i];
|
|
|
|
var data = edgesData.get(id);
|
|
var edge = edges[id];
|
|
if (edge) {
|
|
// update edge
|
|
edge.disconnect();
|
|
edge.setProperties(data);
|
|
edge.connect();
|
|
}
|
|
else {
|
|
// create edge
|
|
edge = new Edge(data, this.body, this.constants);
|
|
this.body.edges[id] = edge;
|
|
}
|
|
}
|
|
|
|
this._createBezierNodes();
|
|
if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
|
|
this._resetLevels();
|
|
this._setupHierarchicalLayout();
|
|
}
|
|
this.moving = true;
|
|
this._updateValueRange(edges);
|
|
};
|
|
|
|
/**
|
|
* Remove existing edges. Non existing ids will be ignored
|
|
* @param {Number[] | String[]} ids
|
|
* @private
|
|
*/
|
|
Network.prototype._removeEdges = function (ids) {
|
|
var edges = this.body.edges;
|
|
|
|
// remove from selection
|
|
for (var i = 0, len = ids.length; i < len; i++) {
|
|
if (this.selectionObj.edges[ids[i]] !== undefined) {
|
|
edges[ids[i]].unselect();
|
|
this._removeFromSelection(edges[ids[i]]);
|
|
}
|
|
}
|
|
|
|
for (var i = 0, len = ids.length; i < len; i++) {
|
|
var id = ids[i];
|
|
var edge = edges[id];
|
|
if (edge) {
|
|
if (edge.via != null) {
|
|
delete this.body.supportNodes[edge.via.id];
|
|
}
|
|
edge.disconnect();
|
|
delete edges[id];
|
|
}
|
|
}
|
|
|
|
this.moving = true;
|
|
this._updateValueRange(edges);
|
|
if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
|
|
this._resetLevels();
|
|
this._setupHierarchicalLayout();
|
|
}
|
|
this.physics._updateCalculationNodes();
|
|
};
|
|
|
|
/**
|
|
* Reconnect all edges
|
|
* @private
|
|
*/
|
|
Network.prototype._reconnectEdges = function() {
|
|
var id,
|
|
nodes = this.body.nodes,
|
|
edges = this.body.edges;
|
|
for (id in nodes) {
|
|
if (nodes.hasOwnProperty(id)) {
|
|
nodes[id].edges = [];
|
|
}
|
|
}
|
|
|
|
for (id in edges) {
|
|
if (edges.hasOwnProperty(id)) {
|
|
var edge = edges[id];
|
|
edge.from = null;
|
|
edge.to = null;
|
|
edge.connect();
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update the values of all object in the given array according to the current
|
|
* value range of the objects in the array.
|
|
* @param {Object} obj An object containing a set of Edges or Nodes
|
|
* The objects must have a method getValue() and
|
|
* setValueRange(min, max).
|
|
* @private
|
|
*/
|
|
Network.prototype._updateValueRange = function(obj) {
|
|
var id;
|
|
|
|
// determine the range of the objects
|
|
var valueMin = undefined;
|
|
var valueMax = undefined;
|
|
var valueTotal = 0;
|
|
for (id in obj) {
|
|
if (obj.hasOwnProperty(id)) {
|
|
var value = obj[id].getValue();
|
|
if (value !== undefined) {
|
|
valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
|
|
valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
|
|
valueTotal += value;
|
|
}
|
|
}
|
|
}
|
|
|
|
// adjust the range of all objects
|
|
if (valueMin !== undefined && valueMax !== undefined) {
|
|
for (id in obj) {
|
|
if (obj.hasOwnProperty(id)) {
|
|
obj[id].setValueRange(valueMin, valueMax, valueTotal);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Set the translation of the network
|
|
* @param {Number} offsetX Horizontal offset
|
|
* @param {Number} offsetY Vertical offset
|
|
* @private
|
|
*/
|
|
Network.prototype._setTranslation = function(offsetX, offsetY) {
|
|
if (this.translation === undefined) {
|
|
this.translation = {
|
|
x: 0,
|
|
y: 0
|
|
};
|
|
}
|
|
|
|
if (offsetX !== undefined) {
|
|
this.translation.x = offsetX;
|
|
}
|
|
if (offsetY !== undefined) {
|
|
this.translation.y = offsetY;
|
|
}
|
|
|
|
this.emit('viewChanged');
|
|
};
|
|
|
|
/**
|
|
* Get the translation of the network
|
|
* @return {Object} translation An object with parameters x and y, both a number
|
|
* @private
|
|
*/
|
|
Network.prototype._getTranslation = function() {
|
|
return {
|
|
x: this.translation.x,
|
|
y: this.translation.y
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Scale the network
|
|
* @param {Number} scale Scaling factor 1.0 is unscaled
|
|
* @private
|
|
*/
|
|
Network.prototype._setScale = function(scale) {
|
|
this.scale = scale;
|
|
};
|
|
|
|
/**
|
|
* Get the current scale of the network
|
|
* @return {Number} scale Scaling factor 1.0 is unscaled
|
|
* @private
|
|
*/
|
|
Network.prototype._getScale = function() {
|
|
return this.scale;
|
|
};
|
|
|
|
|
|
/**
|
|
* Move the network according to the keyboard presses.
|
|
*
|
|
* @private
|
|
*/
|
|
Network.prototype._handleNavigation = function() {
|
|
if (this.xIncrement != 0 || this.yIncrement != 0) {
|
|
var translation = this._getTranslation();
|
|
this._setTranslation(translation.x+this.xIncrement, translation.y+this.yIncrement);
|
|
}
|
|
if (this.zoomIncrement != 0) {
|
|
var center = {
|
|
x: this.frame.canvas.clientWidth / 2,
|
|
y: this.frame.canvas.clientHeight / 2
|
|
};
|
|
this.zoom(this.scale*(1 + this.zoomIncrement), center);
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Freeze the _animationStep
|
|
*/
|
|
Network.prototype.freezeSimulation = function(freeze) {
|
|
if (freeze == true) {
|
|
this.freezeSimulationEnabled = true;
|
|
this.moving = false;
|
|
}
|
|
else {
|
|
this.freezeSimulationEnabled = false;
|
|
this.moving = true;
|
|
this.start();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* This function cleans the support nodes if they are not needed and adds them when they are.
|
|
*
|
|
* @param {boolean} [disableStart]
|
|
* @private
|
|
*/
|
|
Network.prototype._configureSmoothCurves = function(disableStart = true) {
|
|
if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
|
|
this._createBezierNodes();
|
|
// cleanup unused support nodes
|
|
for (let i = 0; i < this.body.supportNodeIndices.length; i++) {
|
|
let nodeId = this.body.supportNodeIndices[i];
|
|
// delete support nodes for edges that have been deleted
|
|
if (this.body.edges[this.body.supportNodes[nodeId].parentEdgeId] === undefined) {
|
|
delete this.body.supportNodes[nodeId];
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// delete the support nodes
|
|
this.body.supportNodes = {};
|
|
for (var edgeId in this.body.edges) {
|
|
if (this.body.edges.hasOwnProperty(edgeId)) {
|
|
this.body.edges[edgeId].via = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
this._updateNodeIndexList();
|
|
this.physics._updateCalculationNodes();
|
|
if (!disableStart) {
|
|
this.moving = true;
|
|
this.start();
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Bezier curves require an anchor point to calculate the smooth flow. These points are nodes. These nodes are invisible but
|
|
* are used for the force calculation.
|
|
*
|
|
* @private
|
|
*/
|
|
Network.prototype._createBezierNodes = function(specificEdges = this.body.edges) {
|
|
if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
|
|
for (var edgeId in specificEdges) {
|
|
if (specificEdges.hasOwnProperty(edgeId)) {
|
|
var edge = specificEdges[edgeId];
|
|
if (edge.via == null) {
|
|
var nodeId = "edgeId:".concat(edge.id);
|
|
var node = new Node(
|
|
{id:nodeId,
|
|
mass:1,
|
|
shape:'circle',
|
|
image:"",
|
|
internalMultiplier:1
|
|
},{},{},this.constants);
|
|
this.body.supportNodes[nodeId] = node;
|
|
edge.via = node;
|
|
edge.via.parentEdgeId = edge.id;
|
|
edge.positionBezierNode();
|
|
}
|
|
}
|
|
}
|
|
this._updateNodeIndexList();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* load the functions that load the mixins into the prototype.
|
|
*
|
|
* @private
|
|
*/
|
|
Network.prototype._initializeMixinLoaders = function () {
|
|
for (var mixin in MixinLoader) {
|
|
if (MixinLoader.hasOwnProperty(mixin)) {
|
|
Network.prototype[mixin] = MixinLoader[mixin];
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Load the XY positions of the nodes into the dataset.
|
|
*/
|
|
Network.prototype.storePosition = function() {
|
|
console.log("storePosition is depricated: use .storePositions() from now on.")
|
|
this.storePositions();
|
|
};
|
|
|
|
/**
|
|
* Load the XY positions of the nodes into the dataset.
|
|
*/
|
|
Network.prototype.storePositions = function() {
|
|
var dataArray = [];
|
|
for (var nodeId in this.body.nodes) {
|
|
if (this.body.nodes.hasOwnProperty(nodeId)) {
|
|
var node = this.body.nodes[nodeId];
|
|
var allowedToMoveX = !this.body.nodes.xFixed;
|
|
var allowedToMoveY = !this.body.nodes.yFixed;
|
|
if (this.body.data.nodes._data[nodeId].x != Math.round(node.x) || this.body.data.nodes._data[nodeId].y != Math.round(node.y)) {
|
|
dataArray.push({id:nodeId,x:Math.round(node.x),y:Math.round(node.y),allowedToMoveX:allowedToMoveX,allowedToMoveY:allowedToMoveY});
|
|
}
|
|
}
|
|
}
|
|
this.body.data.nodes.update(dataArray);
|
|
};
|
|
|
|
/**
|
|
* Return the positions of the nodes.
|
|
*/
|
|
Network.prototype.getPositions = function(ids) {
|
|
var dataArray = {};
|
|
if (ids !== undefined) {
|
|
if (Array.isArray(ids) == true) {
|
|
for (var i = 0; i < ids.length; i++) {
|
|
if (this.body.nodes[ids[i]] !== undefined) {
|
|
var node = this.body.nodes[ids[i]];
|
|
dataArray[ids[i]] = {x: Math.round(node.x), y: Math.round(node.y)};
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
if (this.body.nodes[ids] !== undefined) {
|
|
var node = this.body.nodes[ids];
|
|
dataArray[ids] = {x: Math.round(node.x), y: Math.round(node.y)};
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
for (var nodeId in this.body.nodes) {
|
|
if (this.body.nodes.hasOwnProperty(nodeId)) {
|
|
var node = this.body.nodes[nodeId];
|
|
dataArray[nodeId] = {x: Math.round(node.x), y: Math.round(node.y)};
|
|
}
|
|
}
|
|
}
|
|
return dataArray;
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns true when the Network is active.
|
|
* @returns {boolean}
|
|
*/
|
|
Network.prototype.isActive = function () {
|
|
return !this.activator || this.activator.active;
|
|
};
|
|
|
|
|
|
/**
|
|
* Sets the scale
|
|
* @returns {Number}
|
|
*/
|
|
Network.prototype.setScale = function () {
|
|
return this._setScale();
|
|
};
|
|
|
|
|
|
/**
|
|
* Returns the scale
|
|
* @returns {Number}
|
|
*/
|
|
Network.prototype.getScale = function () {
|
|
return this._getScale();
|
|
};
|
|
|
|
|
|
/**
|
|
* Check if a node is a cluster.
|
|
* @param nodeId
|
|
* @returns {*}
|
|
*/
|
|
Network.prototype.isCluster = function(nodeId) {
|
|
if (this.body.nodes[nodeId] !== undefined) {
|
|
return this.body.nodes[nodeId].isCluster;
|
|
}
|
|
else {
|
|
console.log("Node does not exist.")
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns the scale
|
|
* @returns {Number}
|
|
*/
|
|
Network.prototype.getCenterCoordinates = function () {
|
|
return this.DOMtoCanvas({x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight});
|
|
};
|
|
|
|
|
|
Network.prototype.getBoundingBox = function(nodeId) {
|
|
if (this.body.nodes[nodeId] !== undefined) {
|
|
return this.body.nodes[nodeId].boundingBox;
|
|
}
|
|
}
|
|
|
|
Network.prototype.getConnectedNodes = function(nodeId) {
|
|
var nodeList = [];
|
|
if (this.body.nodes[nodeId] !== undefined) {
|
|
var node = this.body.nodes[nodeId];
|
|
var nodeObj = {nodeId : true}; // used to quickly check if node already exists
|
|
for (var i = 0; i < node.edges.length; i++) {
|
|
var edge = node.edges[i];
|
|
if (edge.toId == nodeId) {
|
|
if (nodeObj[edge.fromId] === undefined) {
|
|
nodeList.push(edge.fromId);
|
|
nodeObj[edge.fromId] = true;
|
|
}
|
|
}
|
|
else if (edge.fromId == nodeId) {
|
|
if (nodeObj[edge.toId] === undefined) {
|
|
nodeList.push(edge.toId)
|
|
nodeObj[edge.toId] = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nodeList;
|
|
}
|
|
|
|
|
|
Network.prototype.getEdgesFromNode = function(nodeId) {
|
|
var edgesList = [];
|
|
if (this.body.nodes[nodeId] !== undefined) {
|
|
var node = this.body.nodes[nodeId];
|
|
for (var i = 0; i < node.edges.length; i++) {
|
|
edgesList.push(node.edges[i].id);
|
|
}
|
|
}
|
|
return edgesList;
|
|
}
|
|
|
|
Network.prototype.generateColorObject = function(color) {
|
|
return util.parseColor(color);
|
|
|
|
}
|
|
|
|
module.exports = Network;
|