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.

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