// Load custom shapes into CanvasRenderingContext2D
|
|
require('./shapes');
|
|
|
|
var Emitter = require('emitter-component');
|
|
var Hammer = require('../module/hammer');
|
|
var util = require('../util');
|
|
var DataSet = require('../DataSet');
|
|
var DataView = require('../DataView');
|
|
var dotparser = require('./dotparser');
|
|
var gephiParser = require('./gephiParser');
|
|
var Images = require('./Images');
|
|
var Activator = require('../shared/Activator');
|
|
|
|
import Groups from './modules/Groups';
|
|
import NodesHandler from './modules/NodesHandler';
|
|
import EdgesHandler from './modules/EdgesHandler';
|
|
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";
|
|
import LayoutEngine from "./modules/LayoutEngine";
|
|
import ManipulationSystem from "./modules/ManipulationSystem";
|
|
import ConfigurationSystem from "./modules/ConfigurationSystem";
|
|
|
|
/**
|
|
* @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');
|
|
}
|
|
|
|
// set constant values
|
|
this.options = {};
|
|
this.defaultOptions = {
|
|
clickToUse: false
|
|
};
|
|
util.extend(this.options, this.defaultOptions);
|
|
|
|
// containers for nodes and edges
|
|
this.body = {
|
|
nodes: {},
|
|
nodeIndices: [],
|
|
edges: {},
|
|
edgeIndices: [],
|
|
data: {
|
|
nodes: null, // A DataSet or DataView
|
|
edges: null // A DataSet or DataView
|
|
},
|
|
functions:{
|
|
createNode: () => {},
|
|
createEdge: () => {},
|
|
getPointer: () => {}
|
|
},
|
|
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}
|
|
}
|
|
};
|
|
|
|
// bind the event listeners
|
|
this.bindEventListeners();
|
|
|
|
// setting up all modules
|
|
var images = new Images(() => this.body.emitter.emit("_requestRedraw")); // object with images
|
|
this.groups = new Groups(); // object with groups
|
|
this.canvas = new Canvas(this.body); // DOM handler
|
|
this.selectionHandler = new SelectionHandler(this.body, this.canvas); // Selection handler
|
|
this.interactionHandler = new InteractionHandler(this.body, this.canvas, this.selectionHandler); // Interaction handler handles all the hammer bindings (that are bound by canvas), key
|
|
this.view = new View(this.body, this.canvas); // camera handler, does animations and zooms
|
|
this.renderer = new CanvasRenderer(this.body, this.canvas); // renderer, starts renderloop, has events that modules can hook into
|
|
this.physics = new PhysicsEngine(this.body); // physics engine, does all the simulations
|
|
this.layoutEngine = new LayoutEngine(this.body); // layout engine for inital layout and hierarchical layout
|
|
this.clustering = new ClusterEngine(this.body); // clustering api
|
|
this.manipulation = new ManipulationSystem(this.body, this.canvas, this.selectionHandler); // data manipulation system
|
|
|
|
this.nodesHandler = new NodesHandler(this.body, images, this.groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options
|
|
this.edgesHandler = new EdgesHandler(this.body, images, this.groups); // Handle adding, deleting and updating of edges as well as global options
|
|
|
|
this.configurationSystem = new ConfigurationSystem(this);
|
|
|
|
// create the DOM elements
|
|
this.canvas.create();
|
|
|
|
// apply options
|
|
this.setOptions(options);
|
|
|
|
// load data (the disable start variable will be the same as the enabled clustering)
|
|
this.setData(data);
|
|
|
|
}
|
|
|
|
// Extend Network with an Emitter mixin
|
|
Emitter(Network.prototype);
|
|
|
|
|
|
|
|
/**
|
|
* Set options
|
|
* @param {Object} options
|
|
*/
|
|
Network.prototype.setOptions = function (options) {
|
|
if (options !== undefined) {
|
|
// the hierarchical system can adapt the edges and the physics to it's own options because not all combinations work with the hierarichical system.
|
|
options = this.layoutEngine.setOptions(options.layout, options);
|
|
|
|
|
|
// pass the options to the modules
|
|
this.groups.setOptions(options.groups);
|
|
this.nodesHandler.setOptions(options.nodes);
|
|
this.edgesHandler.setOptions(options.edges);
|
|
this.physics.setOptions(options.physics);
|
|
this.canvas.setOptions(options.canvas);
|
|
this.renderer.setOptions(options.rendering);
|
|
this.view.setOptions(options.view);
|
|
this.interactionHandler.setOptions(options.interaction);
|
|
this.selectionHandler.setOptions(options.selection);
|
|
this.clustering.setOptions(options.clustering);
|
|
this.manipulation.setOptions(options.manipulation);
|
|
|
|
this.configurationSystem.setOptions(options);
|
|
|
|
// handle network global options
|
|
if (options.clickToUse !== undefined) {
|
|
if (options.clickToUse === true) {
|
|
if (this.activator === undefined) {
|
|
this.activator = new Activator(this.frame);
|
|
this.activator.on('change', this._createKeyBinds.bind(this));
|
|
}
|
|
}
|
|
else {
|
|
if (this.activator !== undefined) {
|
|
this.activator.destroy();
|
|
delete this.activator;
|
|
}
|
|
this.body.emitter.emit("activate");
|
|
}
|
|
}
|
|
else {
|
|
this.body.emitter.emit("activate");
|
|
}
|
|
|
|
this.canvas.setSize();
|
|
|
|
// start the physics simulation. Can be safely called multiple times.
|
|
this.body.emitter.emit("startSimulation");
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Update the this.body.nodeIndices with the most recent node index list
|
|
* @private
|
|
*/
|
|
Network.prototype._updateVisibleIndices = function() {
|
|
let nodes = this.body.nodes;
|
|
let edges = this.body.edges;
|
|
this.body.nodeIndices = [];
|
|
this.body.edgeIndices = [];
|
|
|
|
for (let nodeId in nodes) {
|
|
if (nodes.hasOwnProperty(nodeId)) {
|
|
if (nodes[nodeId].options.hidden === false) {
|
|
this.body.nodeIndices.push(nodeId);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let edgeId in edges) {
|
|
if (edges.hasOwnProperty(edgeId)) {
|
|
if (edges[edgeId].options.hidden === false) {
|
|
this.body.edgeIndices.push(edgeId);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
Network.prototype.bindEventListeners = function() {
|
|
// this event will trigger a rebuilding of the cache everything. Used when nodes or edges have been added or removed.
|
|
this.body.emitter.on("_dataChanged", (params) => {
|
|
var t0 = new Date().valueOf();
|
|
// update shortcut lists
|
|
this._updateVisibleIndices();
|
|
this.physics.updatePhysicsIndices();
|
|
|
|
// call the dataUpdated event because the only difference between the two is the updating of the indices
|
|
this.body.emitter.emit("_dataUpdated");
|
|
|
|
console.log("_dataChanged took:", new Date().valueOf() - t0);
|
|
});
|
|
|
|
// this is called when options of EXISTING nodes or edges have changed.
|
|
this.body.emitter.on("_dataUpdated", () => {
|
|
var t0 = new Date().valueOf();
|
|
// update values
|
|
this._updateValueRange(this.body.nodes);
|
|
this._updateValueRange(this.body.edges);
|
|
// start simulation (can be called safely, even if already running)
|
|
this.body.emitter.emit("startSimulation");
|
|
|
|
console.log("_dataUpdated took:", new Date().valueOf() - t0);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
// reset the physics engine.
|
|
this.body.emitter.emit("resetPhysics");
|
|
this.body.emitter.emit("_resetData");
|
|
|
|
// unselect all to ensure no selections from old data are carried over.
|
|
this.selectionHandler.unselectAll();
|
|
|
|
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.');
|
|
}
|
|
|
|
// 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.nodesHandler.setData(data && data.nodes, true);
|
|
this.edgesHandler.setData(data && data.edges, true);
|
|
}
|
|
|
|
// emit change in data
|
|
this.body.emitter.emit("_dataChanged");
|
|
|
|
// find a stable position or start animating to a stable position
|
|
this.body.emitter.emit("initPhysics");
|
|
};
|
|
|
|
|
|
/**
|
|
* 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.body.emitter.emit("destroy");
|
|
|
|
// clear events
|
|
this.body.emitter.off();
|
|
|
|
// remove the container and everything inside it recursively
|
|
util.recursiveDOMDelete(this.body.container);
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/**
|
|
* Scale the network
|
|
* @param {Number} scale Scaling factor 1.0 is unscaled
|
|
* @private
|
|
*/
|
|
Network.prototype._setScale = function(scale) {
|
|
this.body.view.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.body.view.scale;
|
|
};
|
|
|
|
|
|
/**
|
|
* Load the XY positions of the nodes into the dataset.
|
|
*/
|
|
Network.prototype.storePositions = function() {
|
|
// todo: incorporate fixed instead of allowedtomove, add support for clusters and hierarchical.
|
|
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;
|