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.

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