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.

718 lines
21 KiB

10 years ago
10 years ago
10 years ago
10 years ago
  1. // Load custom shapes into CanvasRenderingContext2D
  2. require('./shapes');
  3. var Emitter = require('emitter-component');
  4. var Hammer = require('../module/hammer');
  5. var util = require('../util');
  6. var DataSet = require('../DataSet');
  7. var DataView = require('../DataView');
  8. var dotparser = require('./dotparser');
  9. var gephiParser = require('./gephiParser');
  10. var Groups = require('./Groups');
  11. var Images = require('./Images');
  12. var Popup = require('./Popup');
  13. var Activator = require('../shared/Activator');
  14. var locales = require('./locales');
  15. import NodesHandler from './modules/NodesHandler';
  16. import EdgesHandler from './modules/EdgesHandler';
  17. import PhysicsEngine from './modules/PhysicsEngine';
  18. import ClusterEngine from './modules/Clustering';
  19. import CanvasRenderer from './modules/CanvasRenderer';
  20. import Canvas from './modules/Canvas';
  21. import View from './modules/View';
  22. import InteractionHandler from './modules/InteractionHandler';
  23. import SelectionHandler from "./modules/SelectionHandler";
  24. import LayoutEngine from "./modules/LayoutEngine";
  25. /**
  26. * @constructor Network
  27. * Create a network visualization, displaying nodes and edges.
  28. *
  29. * @param {Element} container The DOM element in which the Network will
  30. * be created. Normally a div element.
  31. * @param {Object} data An object containing parameters
  32. * {Array} nodes
  33. * {Array} edges
  34. * @param {Object} options Options
  35. */
  36. function Network (container, data, options) {
  37. if (!(this instanceof Network)) {
  38. throw new SyntaxError('Constructor must be called with the new operator');
  39. }
  40. // set constant values
  41. this.remainingOptions = {
  42. dataManipulation: {
  43. enabled: false,
  44. initiallyVisible: false
  45. },
  46. locale: 'en',
  47. locales: locales,
  48. useDefaultGroups: true
  49. };
  50. // containers for nodes and edges
  51. this.body = {
  52. nodes: {},
  53. nodeIndices: [],
  54. edges: {},
  55. edgeIndices: [],
  56. data: {
  57. nodes: null, // A DataSet or DataView
  58. edges: null // A DataSet or DataView
  59. },
  60. functions:{
  61. createNode: () => {},
  62. createEdge: () => {}
  63. },
  64. emitter: {
  65. on: this.on.bind(this),
  66. off: this.off.bind(this),
  67. emit: this.emit.bind(this),
  68. once: this.once.bind(this)
  69. },
  70. eventListeners: {
  71. onTap: function() {},
  72. onTouch: function() {},
  73. onDoubleTap: function() {},
  74. onHold: function() {},
  75. onDragStart: function() {},
  76. onDrag: function() {},
  77. onDragEnd: function() {},
  78. onMouseWheel: function() {},
  79. onPinch: function() {},
  80. onMouseMove: function() {},
  81. onRelease: function() {}
  82. },
  83. container: container,
  84. view: {
  85. scale:1,
  86. translation:{x:0,y:0}
  87. }
  88. };
  89. // this event will trigger a rebuilding of the cache everything. Used when nodes or edges have been added or removed.
  90. this.body.emitter.on("_dataChanged", (params) => {
  91. var t0 = new Date().valueOf();
  92. // update shortcut lists
  93. this._updateVisibleIndices();
  94. this.physics.updatePhysicsIndices();
  95. // update values
  96. this._updateValueRange(this.body.nodes);
  97. this._updateValueRange(this.body.edges);
  98. // update edges
  99. this._reconnectEdges();
  100. this._markAllEdgesAsDirty();
  101. // start simulation (can be called safely, even if already running)
  102. this.body.emitter.emit("startSimulation");
  103. console.log("_dataChanged took:", new Date().valueOf() - t0);
  104. })
  105. // this is called when options of EXISTING nodes or edges have changed.
  106. this.body.emitter.on("_dataUpdated", () => {
  107. var t0 = new Date().valueOf();
  108. // update values
  109. this._updateValueRange(this.body.nodes);
  110. this._updateValueRange(this.body.edges);
  111. // update edges
  112. this._reconnectEdges();
  113. this._markAllEdgesAsDirty();
  114. // start simulation (can be called safely, even if already running)
  115. this.body.emitter.emit("startSimulation");
  116. console.log("_dataUpdated took:", new Date().valueOf() - t0);
  117. });
  118. // todo think of good comment for this set
  119. var groups = new Groups(); // object with groups
  120. var images = new Images(() => this.body.emitter.emit("_requestRedraw")); // object with images
  121. // data handling modules
  122. this.canvas = new Canvas(this.body); // DOM handler
  123. this.selectionHandler = new SelectionHandler(this.body, this.canvas); // Selection handler
  124. this.interactionHandler = new InteractionHandler(this.body, this.canvas, this.selectionHandler); // Interaction handler handles all the hammer bindings (that are bound by canvas), key
  125. this.view = new View(this.body, this.canvas); // camera handler, does animations and zooms
  126. this.renderer = new CanvasRenderer(this.body, this.canvas); // renderer, starts renderloop, has events that modules can hook into
  127. this.physics = new PhysicsEngine(this.body); // physics engine, does all the simulations
  128. this.layoutEngine = new LayoutEngine(this.body);
  129. this.clustering = new ClusterEngine(this.body); // clustering api
  130. this.nodesHandler = new NodesHandler(this.body, images, groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options
  131. this.edgesHandler = new EdgesHandler(this.body, images, groups); // Handle adding, deleting and updating of edges as well as global options
  132. // create the DOM elements
  133. this.canvas.create();
  134. // apply options
  135. this.setOptions(options);
  136. // load data (the disable start variable will be the same as the enabled clustering)
  137. this.setData(data);
  138. }
  139. // Extend Network with an Emitter mixin
  140. Emitter(Network.prototype);
  141. /**
  142. * Update the this.body.nodeIndices with the most recent node index list
  143. * @private
  144. */
  145. Network.prototype._updateVisibleIndices = function() {
  146. let nodes = this.body.nodes;
  147. let edges = this.body.edges;
  148. this.body.nodeIndices = [];
  149. this.body.edgeIndices = [];
  150. for (let nodeId in nodes) {
  151. if (nodes.hasOwnProperty(nodeId)) {
  152. if (nodes[nodeId].options.hidden === false) {
  153. this.body.nodeIndices.push(nodeId);
  154. }
  155. }
  156. }
  157. for (let edgeId in edges) {
  158. if (edges.hasOwnProperty(edgeId)) {
  159. if (edges[edgeId].options.hidden === false) {
  160. this.body.edgeIndices.push(edgeId);
  161. }
  162. }
  163. }
  164. };
  165. /**
  166. * Set nodes and edges, and optionally options as well.
  167. *
  168. * @param {Object} data Object containing parameters:
  169. * {Array | DataSet | DataView} [nodes] Array with nodes
  170. * {Array | DataSet | DataView} [edges] Array with edges
  171. * {String} [dot] String containing data in DOT format
  172. * {String} [gephi] String containing data in gephi JSON format
  173. * {Options} [options] Object with options
  174. * @param {Boolean} [disableStart] | optional: disable the calling of the start function.
  175. */
  176. Network.prototype.setData = function(data) {
  177. // reset the physics engine.
  178. this.body.emitter.emit("resetPhysics");
  179. this.body.emitter.emit("_resetData");
  180. // unselect all to ensure no selections from old data are carried over.
  181. this.selectionHandler.unselectAll();
  182. if (data && data.dot && (data.nodes || data.edges)) {
  183. throw new SyntaxError('Data must contain either parameter "dot" or ' +
  184. ' parameter pair "nodes" and "edges", but not both.');
  185. }
  186. // set options
  187. this.setOptions(data && data.options);
  188. // set all data
  189. if (data && data.dot) {
  190. // parse DOT file
  191. if(data && data.dot) {
  192. var dotData = dotparser.DOTToGraph(data.dot);
  193. this.setData(dotData);
  194. return;
  195. }
  196. }
  197. else if (data && data.gephi) {
  198. // parse DOT file
  199. if(data && data.gephi) {
  200. var gephiData = gephiParser.parseGephi(data.gephi);
  201. this.setData(gephiData);
  202. return;
  203. }
  204. }
  205. else {
  206. this.nodesHandler.setData(data && data.nodes, true);
  207. this.edgesHandler.setData(data && data.edges, true);
  208. }
  209. // emit change in data
  210. this.body.emitter.emit("_dataChanged");
  211. // find a stable position or start animating to a stable position
  212. this.body.emitter.emit("initPhysics");
  213. };
  214. /**
  215. * Set options
  216. * @param {Object} options
  217. */
  218. Network.prototype.setOptions = function (options) {
  219. if (options !== undefined) {
  220. //var fields = ['nodes','edges','smoothCurves','hierarchicalLayout','navigation',
  221. // 'keyboard','dataManipulation','onAdd','onEdit','onEditEdge','onConnect','onDelete','clickToUse'
  222. //];
  223. // extend all but the values in fields
  224. //util.selectiveNotDeepExtend(fields,this.constants, options);
  225. //util.selectiveNotDeepExtend(['color'],this.constants.nodes, options.nodes);
  226. //util.selectiveNotDeepExtend(['color','length'],this.constants.edges, options.edges);
  227. //this.groups.useDefaultGroups = this.constants.useDefaultGroups;
  228. // the hierarchical system can adapt the edges and the physics to it's own options because not all combinations work with the hierarichical system.
  229. options = this.layoutEngine.setOptions(options.layout, options);
  230. this.nodesHandler.setOptions(options.nodes);
  231. this.edgesHandler.setOptions(options.edges);
  232. this.physics.setOptions(options.physics);
  233. this.canvas.setOptions(options.canvas);
  234. this.renderer.setOptions(options.rendering);
  235. this.view.setOptions(options.view);
  236. this.interactionHandler.setOptions(options.interaction);
  237. this.selectionHandler.setOptions(options.selection);
  238. this.clustering.setOptions(options.clustering);
  239. //util.mergeOptions(this.constants, options,'smoothCurves');
  240. //util.mergeOptions(this.constants, options,'hierarchicalLayout');
  241. //util.mergeOptions(this.constants, options,'clustering');
  242. //util.mergeOptions(this.constants, options,'navigation');
  243. //util.mergeOptions(this.constants, options,'keyboard');
  244. //util.mergeOptions(this.constants, options,'dataManipulation');
  245. //if (options.dataManipulation) {
  246. // this.editMode = this.constants.dataManipulation.initiallyVisible;
  247. //}
  248. //// TODO: work out these options and document them
  249. //
  250. //
  251. //
  252. //if (options.groups) {
  253. // for (var groupname in options.groups) {
  254. // if (options.groups.hasOwnProperty(groupname)) {
  255. // var group = options.groups[groupname];
  256. // this.groups.add(groupname, group);
  257. // }
  258. // }
  259. //}
  260. //
  261. //if (options.tooltip) {
  262. // for (prop in options.tooltip) {
  263. // if (options.tooltip.hasOwnProperty(prop)) {
  264. // this.constants.tooltip[prop] = options.tooltip[prop];
  265. // }
  266. // }
  267. // if (options.tooltip.color) {
  268. // this.constants.tooltip.color = util.parseColor(options.tooltip.color);
  269. // }
  270. //}
  271. if ('clickToUse' in options) {
  272. if (options.clickToUse === true) {
  273. if (this.activator === undefined) {
  274. this.activator = new Activator(this.frame);
  275. this.activator.on('change', this._createKeyBinds.bind(this));
  276. }
  277. }
  278. else {
  279. if (this.activator !== undefined) {
  280. this.activator.destroy();
  281. delete this.activator;
  282. }
  283. this.body.emitter.emit("activate");
  284. }
  285. }
  286. else {
  287. this.body.emitter.emit("activate");
  288. }
  289. this.canvas.setSize();
  290. }
  291. };
  292. /**
  293. * Cleans up all bindings of the network, removing it fully from the memory IF the variable is set to null after calling this function.
  294. * var network = new vis.Network(..);
  295. * network.destroy();
  296. * network = null;
  297. */
  298. Network.prototype.destroy = function() {
  299. this.body.emitter.emit("destroy");
  300. // clear events
  301. this.body.emitter.off();
  302. // remove the container and everything inside it recursively
  303. util.recursiveDOMDelete(this.body.container);
  304. };
  305. /**
  306. * Check if there is an element on the given position in the network
  307. * (a node or edge). If so, and if this element has a title,
  308. * show a popup window with its title.
  309. *
  310. * @param {{x:Number, y:Number}} pointer
  311. * @private
  312. */
  313. Network.prototype._checkShowPopup = function (pointer) {
  314. var obj = {
  315. left: this._XconvertDOMtoCanvas(pointer.x),
  316. top: this._YconvertDOMtoCanvas(pointer.y),
  317. right: this._XconvertDOMtoCanvas(pointer.x),
  318. bottom: this._YconvertDOMtoCanvas(pointer.y)
  319. };
  320. var id;
  321. var previousPopupObjId = this.popupObj === undefined ? "" : this.popupObj.id;
  322. var nodeUnderCursor = false;
  323. var popupType = "node";
  324. if (this.popupObj == undefined) {
  325. // search the nodes for overlap, select the top one in case of multiple nodes
  326. var nodes = this.body.nodes;
  327. var overlappingNodes = [];
  328. for (id in nodes) {
  329. if (nodes.hasOwnProperty(id)) {
  330. var node = nodes[id];
  331. if (node.isOverlappingWith(obj)) {
  332. if (node.getTitle() !== undefined) {
  333. overlappingNodes.push(id);
  334. }
  335. }
  336. }
  337. }
  338. if (overlappingNodes.length > 0) {
  339. // if there are overlapping nodes, select the last one, this is the
  340. // one which is drawn on top of the others
  341. this.popupObj = this.body.nodes[overlappingNodes[overlappingNodes.length - 1]];
  342. // if you hover over a node, the title of the edge is not supposed to be shown.
  343. nodeUnderCursor = true;
  344. }
  345. }
  346. if (this.popupObj === undefined && nodeUnderCursor == false) {
  347. // search the edges for overlap
  348. var edges = this.body.edges;
  349. var overlappingEdges = [];
  350. for (id in edges) {
  351. if (edges.hasOwnProperty(id)) {
  352. var edge = edges[id];
  353. if (edge.connected === true && (edge.getTitle() !== undefined) &&
  354. edge.isOverlappingWith(obj)) {
  355. overlappingEdges.push(id);
  356. }
  357. }
  358. }
  359. if (overlappingEdges.length > 0) {
  360. this.popupObj = this.body.edges[overlappingEdges[overlappingEdges.length - 1]];
  361. popupType = "edge";
  362. }
  363. }
  364. if (this.popupObj) {
  365. // show popup message window
  366. if (this.popupObj.id != previousPopupObjId) {
  367. if (this.popup === undefined) {
  368. this.popup = new Popup(this.frame, this.constants.tooltip);
  369. }
  370. this.popup.popupTargetType = popupType;
  371. this.popup.popupTargetId = this.popupObj.id;
  372. // adjust a small offset such that the mouse cursor is located in the
  373. // bottom left location of the popup, and you can easily move over the
  374. // popup area
  375. this.popup.setPosition(pointer.x + 3, pointer.y - 5);
  376. this.popup.setText(this.popupObj.getTitle());
  377. this.popup.show();
  378. }
  379. }
  380. else {
  381. if (this.popup) {
  382. this.popup.hide();
  383. }
  384. }
  385. };
  386. /**
  387. * Check if the popup must be hidden, which is the case when the mouse is no
  388. * longer hovering on the object
  389. * @param {{x:Number, y:Number}} pointer
  390. * @private
  391. */
  392. Network.prototype._checkHidePopup = function (pointer) {
  393. var pointerObj = {
  394. left: this._XconvertDOMtoCanvas(pointer.x),
  395. top: this._YconvertDOMtoCanvas(pointer.y),
  396. right: this._XconvertDOMtoCanvas(pointer.x),
  397. bottom: this._YconvertDOMtoCanvas(pointer.y)
  398. };
  399. var stillOnObj = false;
  400. if (this.popup.popupTargetType == 'node') {
  401. stillOnObj = this.body.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj);
  402. if (stillOnObj === true) {
  403. var overNode = this.getNodeAt(pointer);
  404. stillOnObj = overNode.id == this.popup.popupTargetId;
  405. }
  406. }
  407. else {
  408. if (this.getNodeAt(pointer) === null) {
  409. stillOnObj = this.body.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj);
  410. }
  411. }
  412. if (stillOnObj === false) {
  413. this.popupObj = undefined;
  414. this.popup.hide();
  415. }
  416. };
  417. Network.prototype._markAllEdgesAsDirty = function() {
  418. for (var edgeId in this.body.edges) {
  419. this.body.edges[edgeId].colorDirty = true;
  420. }
  421. }
  422. /**
  423. * Reconnect all edges
  424. * @private
  425. */
  426. Network.prototype._reconnectEdges = function() {
  427. var id;
  428. var nodes = this.body.nodes;
  429. var edges = this.body.edges;
  430. for (id in nodes) {
  431. if (nodes.hasOwnProperty(id)) {
  432. nodes[id].edges = [];
  433. }
  434. }
  435. for (id in edges) {
  436. if (edges.hasOwnProperty(id)) {
  437. var edge = edges[id];
  438. edge.from = null;
  439. edge.to = null;
  440. edge.connect();
  441. }
  442. }
  443. };
  444. /**
  445. * Update the values of all object in the given array according to the current
  446. * value range of the objects in the array.
  447. * @param {Object} obj An object containing a set of Edges or Nodes
  448. * The objects must have a method getValue() and
  449. * setValueRange(min, max).
  450. * @private
  451. */
  452. Network.prototype._updateValueRange = function(obj) {
  453. var id;
  454. // determine the range of the objects
  455. var valueMin = undefined;
  456. var valueMax = undefined;
  457. var valueTotal = 0;
  458. for (id in obj) {
  459. if (obj.hasOwnProperty(id)) {
  460. var value = obj[id].getValue();
  461. if (value !== undefined) {
  462. valueMin = (valueMin === undefined) ? value : Math.min(value, valueMin);
  463. valueMax = (valueMax === undefined) ? value : Math.max(value, valueMax);
  464. valueTotal += value;
  465. }
  466. }
  467. }
  468. // adjust the range of all objects
  469. if (valueMin !== undefined && valueMax !== undefined) {
  470. for (id in obj) {
  471. if (obj.hasOwnProperty(id)) {
  472. obj[id].setValueRange(valueMin, valueMax, valueTotal);
  473. }
  474. }
  475. }
  476. };
  477. /**
  478. * Scale the network
  479. * @param {Number} scale Scaling factor 1.0 is unscaled
  480. * @private
  481. */
  482. Network.prototype._setScale = function(scale) {
  483. this.body.view.scale = scale;
  484. };
  485. /**
  486. * Get the current scale of the network
  487. * @return {Number} scale Scaling factor 1.0 is unscaled
  488. * @private
  489. */
  490. Network.prototype._getScale = function() {
  491. return this.body.view.scale;
  492. };
  493. /**
  494. * Load the XY positions of the nodes into the dataset.
  495. */
  496. Network.prototype.storePositions = function() {
  497. // todo: incorporate fixed instead of allowedtomove, add support for clusters and hierarchical.
  498. var dataArray = [];
  499. for (var nodeId in this.body.nodes) {
  500. if (this.body.nodes.hasOwnProperty(nodeId)) {
  501. var node = this.body.nodes[nodeId];
  502. var allowedToMoveX = !this.body.nodes.xFixed;
  503. var allowedToMoveY = !this.body.nodes.yFixed;
  504. if (this.body.data.nodes._data[nodeId].x != Math.round(node.x) || this.body.data.nodes._data[nodeId].y != Math.round(node.y)) {
  505. dataArray.push({id:nodeId,x:Math.round(node.x),y:Math.round(node.y),allowedToMoveX:allowedToMoveX,allowedToMoveY:allowedToMoveY});
  506. }
  507. }
  508. }
  509. this.body.data.nodes.update(dataArray);
  510. };
  511. /**
  512. * Return the positions of the nodes.
  513. */
  514. Network.prototype.getPositions = function(ids) {
  515. var dataArray = {};
  516. if (ids !== undefined) {
  517. if (Array.isArray(ids) == true) {
  518. for (var i = 0; i < ids.length; i++) {
  519. if (this.body.nodes[ids[i]] !== undefined) {
  520. var node = this.body.nodes[ids[i]];
  521. dataArray[ids[i]] = {x: Math.round(node.x), y: Math.round(node.y)};
  522. }
  523. }
  524. }
  525. else {
  526. if (this.body.nodes[ids] !== undefined) {
  527. var node = this.body.nodes[ids];
  528. dataArray[ids] = {x: Math.round(node.x), y: Math.round(node.y)};
  529. }
  530. }
  531. }
  532. else {
  533. for (var nodeId in this.body.nodes) {
  534. if (this.body.nodes.hasOwnProperty(nodeId)) {
  535. var node = this.body.nodes[nodeId];
  536. dataArray[nodeId] = {x: Math.round(node.x), y: Math.round(node.y)};
  537. }
  538. }
  539. }
  540. return dataArray;
  541. };
  542. /**
  543. * Returns true when the Network is active.
  544. * @returns {boolean}
  545. */
  546. Network.prototype.isActive = function () {
  547. return !this.activator || this.activator.active;
  548. };
  549. /**
  550. * Sets the scale
  551. * @returns {Number}
  552. */
  553. Network.prototype.setScale = function () {
  554. return this._setScale();
  555. };
  556. /**
  557. * Returns the scale
  558. * @returns {Number}
  559. */
  560. Network.prototype.getScale = function () {
  561. return this._getScale();
  562. };
  563. /**
  564. * Check if a node is a cluster.
  565. * @param nodeId
  566. * @returns {*}
  567. */
  568. Network.prototype.isCluster = function(nodeId) {
  569. if (this.body.nodes[nodeId] !== undefined) {
  570. return this.body.nodes[nodeId].isCluster;
  571. }
  572. else {
  573. console.log("Node does not exist.")
  574. return false;
  575. }
  576. };
  577. /**
  578. * Returns the scale
  579. * @returns {Number}
  580. */
  581. Network.prototype.getCenterCoordinates = function () {
  582. return this.DOMtoCanvas({x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight});
  583. };
  584. Network.prototype.getBoundingBox = function(nodeId) {
  585. if (this.body.nodes[nodeId] !== undefined) {
  586. return this.body.nodes[nodeId].boundingBox;
  587. }
  588. }
  589. Network.prototype.getConnectedNodes = function(nodeId) {
  590. var nodeList = [];
  591. if (this.body.nodes[nodeId] !== undefined) {
  592. var node = this.body.nodes[nodeId];
  593. var nodeObj = {nodeId : true}; // used to quickly check if node already exists
  594. for (var i = 0; i < node.edges.length; i++) {
  595. var edge = node.edges[i];
  596. if (edge.toId == nodeId) {
  597. if (nodeObj[edge.fromId] === undefined) {
  598. nodeList.push(edge.fromId);
  599. nodeObj[edge.fromId] = true;
  600. }
  601. }
  602. else if (edge.fromId == nodeId) {
  603. if (nodeObj[edge.toId] === undefined) {
  604. nodeList.push(edge.toId)
  605. nodeObj[edge.toId] = true;
  606. }
  607. }
  608. }
  609. }
  610. return nodeList;
  611. }
  612. Network.prototype.getEdgesFromNode = function(nodeId) {
  613. var edgesList = [];
  614. if (this.body.nodes[nodeId] !== undefined) {
  615. var node = this.body.nodes[nodeId];
  616. for (var i = 0; i < node.edges.length; i++) {
  617. edgesList.push(node.edges[i].id);
  618. }
  619. }
  620. return edgesList;
  621. }
  622. Network.prototype.generateColorObject = function(color) {
  623. return util.parseColor(color);
  624. }
  625. module.exports = Network;