// 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) => { // 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"); }); // this is called when options of EXISTING nodes or edges have changed. this.body.emitter.on("_dataUpdated", () => { // 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"); }); } /** * 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;