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.

498 lines
14 KiB

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