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.

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