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.

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