vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

808 lines
24 KiB

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 Groups = require('./Groups');
var Images = require('./Images');
var Popup = require('./Popup');
var Activator = require('../shared/Activator');
var locales = require('./locales');
// Load custom shapes into CanvasRenderingContext2D
require('./shapes');
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";
/**
* @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.remainingOptions = {
dataManipulation: {
enabled: false,
initiallyVisible: false
},
hierarchicalLayout: {
enabled:false,
levelSeparation: 150,
nodeSpacing: 100,
direction: "UD", // UD, DU, LR, RL
layout: "hubsize" // hubsize, directed
},
locale: 'en',
locales: locales,
useDefaultGroups: true
};
// 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: () => {}
},
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}
}
};
// todo think of good comment for this set
var groups = new Groups(); // object with groups
var images = new Images(() => this.body.emitter.emit("_requestRedraw")); // object with images
// data handling modules
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);
this.clustering = new ClusterEngine(this.body); // clustering api
this.nodesHandler = new NodesHandler(this.body, images, groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options
this.edgesHandler = new EdgesHandler(this.body, images, groups); // Handle adding, deleting and updating of edges as well as global options
// 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();
// update values
this._updateValueRange(this.body.nodes);
this._updateValueRange(this.body.edges);
// update edges
this._reconnectEdges();
this._markAllEdgesAsDirty();
// start simulation (can be called safely, even if already running)
this.body.emitter.emit("startSimulation");
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);
// update edges
this._reconnectEdges();
this._markAllEdgesAsDirty();
// start simulation (can be called safely, even if already running)
this.body.emitter.emit("startSimulation");
console.log("_dataUpdated took:", new Date().valueOf() - t0);
});
// 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);
/**
* 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);
}
}
}
};
/**
* 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();
// 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.nodesHandler.setData(data && data.nodes);
this.edgesHandler.setData(data && data.edges);
}
// find a stable position or start animating to a stable position
this.body.emitter.emit("initPhysics");
};
/**
* Set options
* @param {Object} options
*/
Network.prototype.setOptions = function (options) {
if (options) {
//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.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.layoutEngine.setOptions(options.layout);
//this.clustering.setOptions(options.clustering);
//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 === 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();
}
};
/**
* 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
this.util.recursiveDOMDelete(this.body.container);
};
/**
* 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();
}
};
Network.prototype._markAllEdgesAsDirty = function() {
for (var edgeId in this.body.edges) {
this.body.edges[edgeId].colorDirty = true;
}
}
/**
* 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;
};
/**
* 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.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;