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.

523 lines
14 KiB

9 years ago
9 years ago
9 years ago
9 years ago
  1. let util = require("../../util");
  2. let DataSet = require('../../DataSet');
  3. let DataView = require('../../DataView');
  4. var Node = require("./components/Node").default;
  5. var Label = require("./components/shared/Label").default;
  6. /**
  7. * @class NodesHandler
  8. */
  9. class NodesHandler {
  10. /**
  11. * @param {Object} body
  12. * @param {Images} images
  13. * @param {Array<Group>} groups
  14. * @param {LayoutEngine} layoutEngine
  15. * @constructor NodesHandler
  16. */
  17. constructor(body, images, groups, layoutEngine) {
  18. this.body = body;
  19. this.images = images;
  20. this.groups = groups;
  21. this.layoutEngine = layoutEngine;
  22. // create the node API in the body container
  23. this.body.functions.createNode = this.create.bind(this);
  24. this.nodesListeners = {
  25. add: (event, params) => { this.add(params.items); },
  26. update: (event, params) => { this.update(params.items, params.data, params.oldData); },
  27. remove: (event, params) => { this.remove(params.items); }
  28. };
  29. this.options = {};
  30. this.defaultOptions = {
  31. borderWidth: 1,
  32. borderWidthSelected: 2,
  33. brokenImage: undefined,
  34. color: {
  35. border: '#2B7CE9',
  36. background: '#97C2FC',
  37. highlight: {
  38. border: '#2B7CE9',
  39. background: '#D2E5FF'
  40. },
  41. hover: {
  42. border: '#2B7CE9',
  43. background: '#D2E5FF'
  44. }
  45. },
  46. fixed: {
  47. x: false,
  48. y: false
  49. },
  50. font: {
  51. color: '#343434',
  52. size: 14, // px
  53. face: 'arial',
  54. background: 'none',
  55. strokeWidth: 0, // px
  56. strokeColor: '#ffffff',
  57. align: 'center',
  58. vadjust: 0,
  59. multi: false,
  60. bold: {
  61. mod: 'bold'
  62. },
  63. boldital: {
  64. mod: 'bold italic'
  65. },
  66. ital: {
  67. mod: 'italic'
  68. },
  69. mono: {
  70. mod: '',
  71. size: 15, // px
  72. face: 'monospace',
  73. vadjust: 2
  74. }
  75. },
  76. group: undefined,
  77. hidden: false,
  78. icon: {
  79. face: 'FontAwesome', //'FontAwesome',
  80. code: undefined, //'\uf007',
  81. size: 50, //50,
  82. color: '#2B7CE9' //'#aa00ff'
  83. },
  84. image: undefined, // --> URL
  85. label: undefined,
  86. labelHighlightBold: true,
  87. level: undefined,
  88. margin: {
  89. top: 5,
  90. right: 5,
  91. bottom: 5,
  92. left: 5
  93. },
  94. mass: 1,
  95. physics: true,
  96. scaling: {
  97. min: 10,
  98. max: 30,
  99. label: {
  100. enabled: false,
  101. min: 14,
  102. max: 30,
  103. maxVisible: 30,
  104. drawThreshold: 5
  105. },
  106. customScalingFunction: function (min, max, total, value) {
  107. if (max === min) {
  108. return 0.5;
  109. }
  110. else {
  111. let scale = 1 / (max - min);
  112. return Math.max(0, (value - min) * scale);
  113. }
  114. }
  115. },
  116. shadow: {
  117. enabled: false,
  118. color: 'rgba(0,0,0,0.5)',
  119. size: 10,
  120. x: 5,
  121. y: 5
  122. },
  123. shape: 'ellipse',
  124. shapeProperties: {
  125. borderDashes: false, // only for borders
  126. borderRadius: 6, // only for box shape
  127. interpolation: true, // only for image and circularImage shapes
  128. useImageSize: false, // only for image and circularImage shapes
  129. useBorderWithImage: false // only for image shape
  130. },
  131. size: 25,
  132. title: undefined,
  133. value: undefined,
  134. x: undefined,
  135. y: undefined
  136. };
  137. // Protect from idiocy
  138. if (this.defaultOptions.mass <= 0) {
  139. throw 'Internal error: mass in defaultOptions of NodesHandler may not be zero or negative';
  140. }
  141. util.extend(this.options, this.defaultOptions);
  142. this.bindEventListeners();
  143. }
  144. /**
  145. * Binds event listeners
  146. */
  147. bindEventListeners() {
  148. // refresh the nodes. Used when reverting from hierarchical layout
  149. this.body.emitter.on('refreshNodes', this.refresh.bind(this));
  150. this.body.emitter.on('refresh', this.refresh.bind(this));
  151. this.body.emitter.on('destroy', () => {
  152. util.forEach(this.nodesListeners, (callback, event) => {
  153. if (this.body.data.nodes)
  154. this.body.data.nodes.off(event, callback);
  155. });
  156. delete this.body.functions.createNode;
  157. delete this.nodesListeners.add;
  158. delete this.nodesListeners.update;
  159. delete this.nodesListeners.remove;
  160. delete this.nodesListeners;
  161. });
  162. }
  163. /**
  164. *
  165. * @param {Object} options
  166. */
  167. setOptions(options) {
  168. this.nodeOptions = options;
  169. if (options !== undefined) {
  170. Node.parseOptions(this.options, options);
  171. // update the shape in all nodes
  172. if (options.shape !== undefined) {
  173. for (let nodeId in this.body.nodes) {
  174. if (this.body.nodes.hasOwnProperty(nodeId)) {
  175. this.body.nodes[nodeId].updateShape();
  176. }
  177. }
  178. }
  179. // update the font in all nodes
  180. if (options.font !== undefined) {
  181. Label.parseOptions(this.options.font, options);
  182. for (let nodeId in this.body.nodes) {
  183. if (this.body.nodes.hasOwnProperty(nodeId)) {
  184. this.body.nodes[nodeId].updateLabelModule();
  185. this.body.nodes[nodeId].needsRefresh();
  186. }
  187. }
  188. }
  189. // update the shape size in all nodes
  190. if (options.size !== undefined) {
  191. for (let nodeId in this.body.nodes) {
  192. if (this.body.nodes.hasOwnProperty(nodeId)) {
  193. this.body.nodes[nodeId].needsRefresh();
  194. }
  195. }
  196. }
  197. // update the state of the variables if needed
  198. if (options.hidden !== undefined || options.physics !== undefined) {
  199. this.body.emitter.emit('_dataChanged');
  200. }
  201. }
  202. }
  203. /**
  204. * Set a data set with nodes for the network
  205. * @param {Array | DataSet | DataView} nodes The data containing the nodes.
  206. * @param {Boolean} [doNotEmit=false]
  207. * @private
  208. */
  209. setData(nodes, doNotEmit = false) {
  210. let oldNodesData = this.body.data.nodes;
  211. if (nodes instanceof DataSet || nodes instanceof DataView) {
  212. this.body.data.nodes = nodes;
  213. }
  214. else if (Array.isArray(nodes)) {
  215. this.body.data.nodes = new DataSet();
  216. this.body.data.nodes.add(nodes);
  217. }
  218. else if (!nodes) {
  219. this.body.data.nodes = new DataSet();
  220. }
  221. else {
  222. throw new TypeError('Array or DataSet expected');
  223. }
  224. if (oldNodesData) {
  225. // unsubscribe from old dataset
  226. util.forEach(this.nodesListeners, function (callback, event) {
  227. oldNodesData.off(event, callback);
  228. });
  229. }
  230. // remove drawn nodes
  231. this.body.nodes = {};
  232. if (this.body.data.nodes) {
  233. // subscribe to new dataset
  234. let me = this;
  235. util.forEach(this.nodesListeners, function (callback, event) {
  236. me.body.data.nodes.on(event, callback);
  237. });
  238. // draw all new nodes
  239. let ids = this.body.data.nodes.getIds();
  240. this.add(ids, true);
  241. }
  242. if (doNotEmit === false) {
  243. this.body.emitter.emit("_dataChanged");
  244. }
  245. }
  246. /**
  247. * Add nodes
  248. * @param {Number[] | String[]} ids
  249. * @param {Boolean} [doNotEmit=false]
  250. * @private
  251. */
  252. add(ids, doNotEmit = false) {
  253. let id;
  254. let newNodes = [];
  255. for (let i = 0; i < ids.length; i++) {
  256. id = ids[i];
  257. let properties = this.body.data.nodes.get(id);
  258. let node = this.create(properties);
  259. newNodes.push(node);
  260. this.body.nodes[id] = node; // note: this may replace an existing node
  261. }
  262. this.layoutEngine.positionInitially(newNodes);
  263. if (doNotEmit === false) {
  264. this.body.emitter.emit("_dataChanged");
  265. }
  266. }
  267. /**
  268. * Update existing nodes, or create them when not yet existing
  269. * @param {Number[] | String[]} ids id's of changed nodes
  270. * @param {Array} changedData array with changed data
  271. * @param {Array|undefined} oldData optional; array with previous data
  272. * @private
  273. */
  274. update(ids, changedData, oldData) {
  275. let nodes = this.body.nodes;
  276. let dataChanged = false;
  277. for (let i = 0; i < ids.length; i++) {
  278. let id = ids[i];
  279. let node = nodes[id];
  280. let data = changedData[i];
  281. if (node !== undefined) {
  282. // update node
  283. if (node.setOptions(data)) {
  284. dataChanged = true;
  285. }
  286. }
  287. else {
  288. dataChanged = true;
  289. // create node
  290. node = this.create(data);
  291. nodes[id] = node;
  292. }
  293. }
  294. if (!dataChanged && oldData !== undefined) {
  295. // Check for any changes which should trigger a layout recalculation
  296. // For now, this is just 'level' for hierarchical layout
  297. // Assumption: old and new data arranged in same order; at time of writing, this holds.
  298. dataChanged = changedData.some(function(newValue, index) {
  299. let oldValue = oldData[index];
  300. return (oldValue && oldValue.level !== newValue.level);
  301. });
  302. }
  303. if (dataChanged === true) {
  304. this.body.emitter.emit("_dataChanged");
  305. }
  306. else {
  307. this.body.emitter.emit("_dataUpdated");
  308. }
  309. }
  310. /**
  311. * Remove existing nodes. If nodes do not exist, the method will just ignore it.
  312. * @param {Number[] | String[]} ids
  313. * @private
  314. */
  315. remove(ids) {
  316. let nodes = this.body.nodes;
  317. for (let i = 0; i < ids.length; i++) {
  318. let id = ids[i];
  319. delete nodes[id];
  320. }
  321. this.body.emitter.emit("_dataChanged");
  322. }
  323. /**
  324. * create a node
  325. * @param {Object} properties
  326. * @param {class} [constructorClass=Node.default]
  327. * @returns {*}
  328. */
  329. create(properties, constructorClass = Node) {
  330. return new constructorClass(properties, this.body, this.images, this.groups, this.options, this.defaultOptions, this.nodeOptions)
  331. }
  332. /**
  333. *
  334. * @param {boolean} [clearPositions=false]
  335. */
  336. refresh(clearPositions = false) {
  337. let nodes = this.body.nodes;
  338. for (let nodeId in nodes) {
  339. let node = undefined;
  340. if (nodes.hasOwnProperty(nodeId)) {
  341. node = nodes[nodeId];
  342. }
  343. let data = this.body.data.nodes.get(nodeId);
  344. if (node !== undefined && data !== undefined) {
  345. if (clearPositions === true) {
  346. node.setOptions({x:null, y:null});
  347. }
  348. node.setOptions({ fixed: false });
  349. node.setOptions(data);
  350. }
  351. }
  352. }
  353. /**
  354. * Returns the positions of the nodes.
  355. * @param {Array<vis.Node.id>|String} [ids] --> optional, can be array of nodeIds, can be string
  356. * @returns {{}}
  357. */
  358. getPositions(ids) {
  359. let dataArray = {};
  360. if (ids !== undefined) {
  361. if (Array.isArray(ids) === true) {
  362. for (let i = 0; i < ids.length; i++) {
  363. if (this.body.nodes[ids[i]] !== undefined) {
  364. let node = this.body.nodes[ids[i]];
  365. dataArray[ids[i]] = { x: Math.round(node.x), y: Math.round(node.y) };
  366. }
  367. }
  368. }
  369. else {
  370. if (this.body.nodes[ids] !== undefined) {
  371. let node = this.body.nodes[ids];
  372. dataArray[ids] = { x: Math.round(node.x), y: Math.round(node.y) };
  373. }
  374. }
  375. }
  376. else {
  377. for (let i = 0; i < this.body.nodeIndices.length; i++) {
  378. let node = this.body.nodes[this.body.nodeIndices[i]];
  379. dataArray[this.body.nodeIndices[i]] = { x: Math.round(node.x), y: Math.round(node.y) };
  380. }
  381. }
  382. return dataArray;
  383. }
  384. /**
  385. * Load the XY positions of the nodes into the dataset.
  386. */
  387. storePositions() {
  388. // todo: add support for clusters and hierarchical.
  389. let dataArray = [];
  390. var dataset = this.body.data.nodes.getDataSet();
  391. for (let nodeId in dataset._data) {
  392. if (dataset._data.hasOwnProperty(nodeId)) {
  393. let node = this.body.nodes[nodeId];
  394. if (dataset._data[nodeId].x != Math.round(node.x) || dataset._data[nodeId].y != Math.round(node.y)) {
  395. dataArray.push({ id: node.id, x: Math.round(node.x), y: Math.round(node.y) });
  396. }
  397. }
  398. }
  399. dataset.update(dataArray);
  400. }
  401. /**
  402. * get the bounding box of a node.
  403. * @param {vis.Node.id} nodeId
  404. * @returns {j|*}
  405. */
  406. getBoundingBox(nodeId) {
  407. if (this.body.nodes[nodeId] !== undefined) {
  408. return this.body.nodes[nodeId].shape.boundingBox;
  409. }
  410. }
  411. /**
  412. * Get the Ids of nodes connected to this node.
  413. * @param {vis.Node.id} nodeId
  414. * @param {'to'|'from'|undefined} direction values 'from' and 'to' select respectively parent and child nodes only.
  415. * Any other value returns both parent and child nodes.
  416. * @returns {Array}
  417. */
  418. getConnectedNodes(nodeId, direction) {
  419. let nodeList = [];
  420. if (this.body.nodes[nodeId] !== undefined) {
  421. let node = this.body.nodes[nodeId];
  422. let nodeObj = {}; // used to quickly check if node already exists
  423. for (let i = 0; i < node.edges.length; i++) {
  424. let edge = node.edges[i];
  425. if (direction !== 'to' && edge.toId == node.id) { // these are double equals since ids can be numeric or string
  426. if (nodeObj[edge.fromId] === undefined) {
  427. nodeList.push(edge.fromId);
  428. nodeObj[edge.fromId] = true;
  429. }
  430. }
  431. else if (direction !== 'from' && edge.fromId == node.id) { // these are double equals since ids can be numeric or string
  432. if (nodeObj[edge.toId] === undefined) {
  433. nodeList.push(edge.toId);
  434. nodeObj[edge.toId] = true;
  435. }
  436. }
  437. }
  438. }
  439. return nodeList;
  440. }
  441. /**
  442. * Get the ids of the edges connected to this node.
  443. * @param {vis.Node.id} nodeId
  444. * @returns {*}
  445. */
  446. getConnectedEdges(nodeId) {
  447. let edgeList = [];
  448. if (this.body.nodes[nodeId] !== undefined) {
  449. let node = this.body.nodes[nodeId];
  450. for (let i = 0; i < node.edges.length; i++) {
  451. edgeList.push(node.edges[i].id)
  452. }
  453. }
  454. else {
  455. console.log("NodeId provided for getConnectedEdges does not exist. Provided: ", nodeId);
  456. }
  457. return edgeList;
  458. }
  459. /**
  460. * Move a node.
  461. *
  462. * @param {vis.Node.id} nodeId
  463. * @param {Number} x
  464. * @param {Number} y
  465. */
  466. moveNode(nodeId, x, y) {
  467. if (this.body.nodes[nodeId] !== undefined) {
  468. this.body.nodes[nodeId].x = Number(x);
  469. this.body.nodes[nodeId].y = Number(y);
  470. setTimeout(() => {this.body.emitter.emit("startSimulation")},0);
  471. }
  472. else {
  473. console.log("Node id supplied to moveNode does not exist. Provided: ", nodeId);
  474. }
  475. }
  476. }
  477. export default NodesHandler;