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.

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